js-style-kit 0.6.1 → 0.7.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 +5 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +48 -7
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/src/eslint/base/README.md +186 -0
- package/src/eslint/base/config.ts +37 -0
- package/src/eslint/base/rules.ts +444 -0
- package/src/eslint/base/types.ts +20 -0
- package/src/eslint/constants.ts +52 -0
- package/src/eslint/convex/README.md +30 -0
- package/src/eslint/convex/config.ts +34 -0
- package/src/eslint/convex/rules.ts +8 -0
- package/src/eslint/convex/types.ts +8 -0
- package/src/eslint/ignores.ts +31 -0
- package/src/eslint/import/README.md +397 -0
- package/src/eslint/import/config.ts +48 -0
- package/src/eslint/import/rules.ts +81 -0
- package/src/eslint/index.ts +259 -0
- package/src/eslint/jsdoc/README.md +399 -0
- package/src/eslint/jsdoc/config.ts +29 -0
- package/src/eslint/jsdoc/rules.ts +81 -0
- package/src/eslint/jsdoc/types.ts +56 -0
- package/src/eslint/nextjs/config.ts +25 -0
- package/src/eslint/nextjs/rules.ts +25 -0
- package/src/eslint/nextjs/types.ts +27 -0
- package/src/eslint/perfectionist/README.md +454 -0
- package/src/eslint/perfectionist/config.ts +25 -0
- package/src/eslint/perfectionist/rules.ts +39 -0
- package/src/eslint/prefer-arrow-function/config.ts +33 -0
- package/src/eslint/prefer-arrow-function/types.ts +13 -0
- package/src/eslint/process-custom-rules.ts +72 -0
- package/src/eslint/query/README.md +254 -0
- package/src/eslint/query/config.ts +27 -0
- package/src/eslint/query/rules.ts +11 -0
- package/src/eslint/query/types.ts +11 -0
- package/src/eslint/react/README.md +416 -0
- package/src/eslint/react/config.ts +65 -0
- package/src/eslint/react/rules.ts +188 -0
- package/src/eslint/react/types.ts +26 -0
- package/src/eslint/react-refresh/config.ts +28 -0
- package/src/eslint/react-refresh/rules.ts +48 -0
- package/src/eslint/storybook/README.md +424 -0
- package/src/eslint/storybook/config.ts +57 -0
- package/src/eslint/testing/README.md +436 -0
- package/src/eslint/testing/config.ts +90 -0
- package/src/eslint/testing/jest-rules.ts +47 -0
- package/src/eslint/testing/vitest-rules.ts +42 -0
- package/src/eslint/turbo/README.md +380 -0
- package/src/eslint/turbo/config.ts +26 -0
- package/src/eslint/turbo/types.ts +7 -0
- package/src/eslint/types.ts +29 -0
- package/src/eslint/typescript/README.md +229 -0
- package/src/eslint/typescript/config.ts +48 -0
- package/src/eslint/typescript/rules.ts +137 -0
- package/src/eslint/typescript/types.ts +35 -0
- package/src/eslint/unicorn/README.md +497 -0
- package/src/eslint/unicorn/config.ts +36 -0
- package/src/eslint/unicorn/rules.ts +86 -0
- package/src/index.ts +3 -0
- package/src/modules.d.ts +5 -0
- package/src/prettier/README.md +413 -0
- package/src/prettier/index.ts +110 -0
- package/src/utils/is-type.ts +60 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import reactRefresh from "eslint-plugin-react-refresh";
|
|
2
|
+
|
|
3
|
+
import type { EslintConfigObject, EslintRuleConfig } from "../types.js";
|
|
4
|
+
|
|
5
|
+
import { configNames } from "../constants.js";
|
|
6
|
+
import { reactRefreshRules } from "./rules.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generates ESLint configuration for React Refresh.
|
|
10
|
+
*
|
|
11
|
+
* This plugin validates that components can safely be updated with Fast Refresh.
|
|
12
|
+
* It enforces that components are structured in a way that integrations like
|
|
13
|
+
* react-refresh expect.
|
|
14
|
+
*
|
|
15
|
+
* @param customRules - Optional object containing custom rules to override or add to the React Refresh configuration.
|
|
16
|
+
* @returns An ESLint configuration object for React Refresh.
|
|
17
|
+
*/
|
|
18
|
+
export const reactRefreshEslintConfig = (
|
|
19
|
+
customRules?: Record<string, EslintRuleConfig>,
|
|
20
|
+
): EslintConfigObject => {
|
|
21
|
+
return {
|
|
22
|
+
name: configNames.reactRefresh,
|
|
23
|
+
plugins: {
|
|
24
|
+
"react-refresh": reactRefresh,
|
|
25
|
+
},
|
|
26
|
+
rules: customRules ?? reactRefreshRules,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EslintRuleConfig } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export type ReactRefreshRules = Record<string, EslintRuleConfig> & {
|
|
4
|
+
"react-refresh/only-export-components"?: EslintRuleConfig<{
|
|
5
|
+
/**
|
|
6
|
+
* Don't warn when a constant (string, number, boolean, templateLiteral)
|
|
7
|
+
* is exported aside one or more components.
|
|
8
|
+
*/
|
|
9
|
+
allowConstantExport?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* If you use a framework that handles HMR of some specific exports,
|
|
12
|
+
* you can use this option to avoid warning for them.
|
|
13
|
+
*/
|
|
14
|
+
allowExportNames?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* If you're using JSX inside .js files, you can enable this option.
|
|
17
|
+
* To reduce false positives, only files importing 'react' are checked.
|
|
18
|
+
*/
|
|
19
|
+
checkJS?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* If you're exporting a component wrapped in a custom HOC,
|
|
22
|
+
* you can use this option to avoid false positives.
|
|
23
|
+
*/
|
|
24
|
+
customHOCs?: string[];
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generates ESLint rules configuration for React Refresh.
|
|
30
|
+
*
|
|
31
|
+
* This plugin validates that components can safely be updated with Fast Refresh.
|
|
32
|
+
*
|
|
33
|
+
* @returns Configuration object containing ESLint rules for React Refresh
|
|
34
|
+
*/
|
|
35
|
+
export const reactRefreshRules: ReactRefreshRules = {
|
|
36
|
+
/**
|
|
37
|
+
* Validate that your components can safely be updated with Fast Refresh.
|
|
38
|
+
*
|
|
39
|
+
* This rule enforces that components are structured in a way that integrations
|
|
40
|
+
* like react-refresh expect.
|
|
41
|
+
*
|
|
42
|
+
* 🚫 Not fixable - https://github.com/ArnaudBarre/eslint-plugin-react-refresh
|
|
43
|
+
*/
|
|
44
|
+
"react-refresh/only-export-components": [
|
|
45
|
+
"warn",
|
|
46
|
+
{ allowConstantExport: true },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Storybook Configuration
|
|
2
|
+
|
|
3
|
+
ESLint rules for Storybook stories and configuration files.
|
|
4
|
+
|
|
5
|
+
[← Back to main README](../../../README.md)
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Storybook configuration is **disabled by default** and provides:
|
|
10
|
+
|
|
11
|
+
- Story file best practices
|
|
12
|
+
- CSF (Component Story Format) validation
|
|
13
|
+
- Story naming and structure rules
|
|
14
|
+
- Testing library integration
|
|
15
|
+
- Storybook addon validation
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { eslintConfig } from "js-style-kit";
|
|
21
|
+
|
|
22
|
+
export default eslintConfig({
|
|
23
|
+
storybook: true, // Enable Storybook rules
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## File Scope
|
|
28
|
+
|
|
29
|
+
Storybook rules apply to:
|
|
30
|
+
|
|
31
|
+
- **Story files**: `**/*.stories.{ts,tsx,js,jsx,mjs,cjs}` and `**/*.story.{ts,tsx,js,jsx,mjs,cjs}`
|
|
32
|
+
- **Config files**: `.storybook/main.{js,cjs,mjs,ts}`
|
|
33
|
+
|
|
34
|
+
## Key Features
|
|
35
|
+
|
|
36
|
+
### CSF (Component Story Format)
|
|
37
|
+
|
|
38
|
+
Ensures proper CSF structure:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// ✅ Good - proper CSF format
|
|
42
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
43
|
+
import { Button } from "./Button";
|
|
44
|
+
|
|
45
|
+
const meta = {
|
|
46
|
+
title: "Components/Button",
|
|
47
|
+
component: Button,
|
|
48
|
+
} satisfies Meta<typeof Button>;
|
|
49
|
+
|
|
50
|
+
export default meta;
|
|
51
|
+
type Story = StoryObj<typeof meta>;
|
|
52
|
+
|
|
53
|
+
export const Primary: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
label: "Click me",
|
|
56
|
+
variant: "primary",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ❌ Bad - missing default export
|
|
61
|
+
export const Primary: Story = {
|
|
62
|
+
/* ... */
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ❌ Bad - missing component in meta
|
|
66
|
+
export default {
|
|
67
|
+
title: "Components/Button",
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Rules:**
|
|
72
|
+
|
|
73
|
+
- `storybook/default-exports` - Requires default export with meta
|
|
74
|
+
- `storybook/csf-component` - Ensures `component` is defined in meta
|
|
75
|
+
|
|
76
|
+
### Story Naming
|
|
77
|
+
|
|
78
|
+
Enforces consistent story naming:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// ✅ Good - PascalCase story names
|
|
82
|
+
export const Primary: Story = {
|
|
83
|
+
/* ... */
|
|
84
|
+
};
|
|
85
|
+
export const Secondary: Story = {
|
|
86
|
+
/* ... */
|
|
87
|
+
};
|
|
88
|
+
export const WithIcon: Story = {
|
|
89
|
+
/* ... */
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ❌ Bad - incorrect casing
|
|
93
|
+
export const primary: Story = {
|
|
94
|
+
/* ... */
|
|
95
|
+
};
|
|
96
|
+
export const with_icon: Story = {
|
|
97
|
+
/* ... */
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ❌ Bad - redundant story name
|
|
101
|
+
export const ButtonPrimary: Story = {
|
|
102
|
+
name: "Primary", // Redundant - matches export name
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Rules:**
|
|
107
|
+
|
|
108
|
+
- `storybook/prefer-pascal-case` - PascalCase for story names
|
|
109
|
+
- `storybook/no-redundant-story-name` - No redundant `name` property
|
|
110
|
+
|
|
111
|
+
### Story Structure
|
|
112
|
+
|
|
113
|
+
Ensures clean story organization:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
// ✅ Good - meta properties as properties
|
|
117
|
+
const meta = {
|
|
118
|
+
title: "Components/Button",
|
|
119
|
+
component: Button,
|
|
120
|
+
tags: ["autodocs"],
|
|
121
|
+
} satisfies Meta<typeof Button>;
|
|
122
|
+
|
|
123
|
+
// ✅ Good - hierarchy separator
|
|
124
|
+
const meta = {
|
|
125
|
+
title: "Design System/Components/Button", // Uses /
|
|
126
|
+
} satisfies Meta<typeof Button>;
|
|
127
|
+
|
|
128
|
+
// ❌ Bad - hierarchy with pipe
|
|
129
|
+
const meta = {
|
|
130
|
+
title: "Design System | Components | Button", // Don't use |
|
|
131
|
+
} satisfies Meta<typeof Button>;
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Rules:**
|
|
135
|
+
|
|
136
|
+
- `storybook/meta-inline-properties` - Inline meta properties (no spread)
|
|
137
|
+
- `storybook/hierarchy-separator` - Use `/` for hierarchy, not `|`
|
|
138
|
+
- `storybook/story-exports` - All exports must be stories (except default)
|
|
139
|
+
|
|
140
|
+
### Testing Integration
|
|
141
|
+
|
|
142
|
+
When writing play functions for interaction tests:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// ✅ Good - proper test setup
|
|
146
|
+
export const Login: Story = {
|
|
147
|
+
play: async ({ canvasElement }) => {
|
|
148
|
+
const canvas = within(canvasElement);
|
|
149
|
+
|
|
150
|
+
await userEvent.type(canvas.getByRole("textbox"), "user@example.com");
|
|
151
|
+
await userEvent.click(canvas.getByRole("button"));
|
|
152
|
+
|
|
153
|
+
await expect(canvas.getByText("Welcome")).toBeInTheDocument();
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// ❌ Bad - missing context parameter
|
|
158
|
+
export const Login: Story = {
|
|
159
|
+
play: async () => {
|
|
160
|
+
// Missing context - can't access canvas
|
|
161
|
+
await userEvent.click(/* ??? */);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ❌ Bad - not awaiting interactions
|
|
166
|
+
export const Login: Story = {
|
|
167
|
+
play: async ({ canvasElement }) => {
|
|
168
|
+
const canvas = within(canvasElement);
|
|
169
|
+
userEvent.click(canvas.getByRole("button")); // Missing await
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// ❌ Bad - using regular testing-library
|
|
174
|
+
import { screen } from "@testing-library/react"; // Don't import from here
|
|
175
|
+
|
|
176
|
+
export const Login: Story = {
|
|
177
|
+
play: async () => {
|
|
178
|
+
screen.getByRole("button"); // Use canvas from context instead
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Rules:**
|
|
184
|
+
|
|
185
|
+
- `storybook/context-in-play-function` - Requires context parameter in play functions
|
|
186
|
+
- `storybook/await-interactions` - Ensures interactions are awaited
|
|
187
|
+
- `storybook/use-storybook-expect` - Use `@storybook/test` expect
|
|
188
|
+
- `storybook/use-storybook-testing-library` - Use `@storybook/test` for queries
|
|
189
|
+
|
|
190
|
+
### Disabled Rules in Stories
|
|
191
|
+
|
|
192
|
+
Certain rules are automatically disabled in story files:
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
// ✅ OK in story files - anonymous default export
|
|
196
|
+
export default {
|
|
197
|
+
title: "Components/Button",
|
|
198
|
+
component: Button,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// ✅ OK in story files - rules of hooks don't apply
|
|
202
|
+
export const WithHook: Story = {
|
|
203
|
+
render: () => {
|
|
204
|
+
const [count, setCount] = useState(0); // OK in render function
|
|
205
|
+
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Auto-disabled:**
|
|
211
|
+
|
|
212
|
+
- `import/no-anonymous-default-export` - Story meta needs anonymous default
|
|
213
|
+
- `react-hooks/rules-of-hooks` - Hooks in render functions are OK
|
|
214
|
+
|
|
215
|
+
### Storybook Configuration
|
|
216
|
+
|
|
217
|
+
Additional rules for `.storybook/main.js` configuration:
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
// .storybook/main.ts
|
|
221
|
+
const config = {
|
|
222
|
+
stories: ["../src/**/*.stories.@(ts|tsx)"],
|
|
223
|
+
addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export default config;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Rules:**
|
|
230
|
+
|
|
231
|
+
- `storybook/no-uninstalled-addons` - Ensures addons are installed in `package.json`
|
|
232
|
+
|
|
233
|
+
## Examples
|
|
234
|
+
|
|
235
|
+
### Basic Component Story
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// Button.stories.tsx
|
|
239
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
240
|
+
import { Button } from "./Button";
|
|
241
|
+
|
|
242
|
+
const meta = {
|
|
243
|
+
title: "UI/Button",
|
|
244
|
+
component: Button,
|
|
245
|
+
tags: ["autodocs"],
|
|
246
|
+
argTypes: {
|
|
247
|
+
variant: {
|
|
248
|
+
control: "select",
|
|
249
|
+
options: ["primary", "secondary", "outline"],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
} satisfies Meta<typeof Button>;
|
|
253
|
+
|
|
254
|
+
export default meta;
|
|
255
|
+
type Story = StoryObj<typeof meta>;
|
|
256
|
+
|
|
257
|
+
export const Primary: Story = {
|
|
258
|
+
args: {
|
|
259
|
+
children: "Button",
|
|
260
|
+
variant: "primary",
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export const Secondary: Story = {
|
|
265
|
+
args: {
|
|
266
|
+
children: "Button",
|
|
267
|
+
variant: "secondary",
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const Disabled: Story = {
|
|
272
|
+
args: {
|
|
273
|
+
children: "Button",
|
|
274
|
+
disabled: true,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Story with Interactions
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// LoginForm.stories.tsx
|
|
283
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
284
|
+
import { expect, userEvent, within } from "@storybook/test";
|
|
285
|
+
import { LoginForm } from "./LoginForm";
|
|
286
|
+
|
|
287
|
+
const meta = {
|
|
288
|
+
title: "Forms/LoginForm",
|
|
289
|
+
component: LoginForm,
|
|
290
|
+
} satisfies Meta<typeof LoginForm>;
|
|
291
|
+
|
|
292
|
+
export default meta;
|
|
293
|
+
type Story = StoryObj<typeof meta>;
|
|
294
|
+
|
|
295
|
+
export const Default: Story = {};
|
|
296
|
+
|
|
297
|
+
export const FilledForm: Story = {
|
|
298
|
+
play: async ({ canvasElement }) => {
|
|
299
|
+
const canvas = within(canvasElement);
|
|
300
|
+
|
|
301
|
+
await userEvent.type(canvas.getByLabelText("Email"), "user@example.com");
|
|
302
|
+
await userEvent.type(canvas.getByLabelText("Password"), "password123");
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export const SubmitSuccess: Story = {
|
|
307
|
+
play: async ({ canvasElement }) => {
|
|
308
|
+
const canvas = within(canvasElement);
|
|
309
|
+
|
|
310
|
+
await userEvent.type(canvas.getByLabelText("Email"), "user@example.com");
|
|
311
|
+
await userEvent.type(canvas.getByLabelText("Password"), "password123");
|
|
312
|
+
await userEvent.click(canvas.getByRole("button", { name: /submit/i }));
|
|
313
|
+
|
|
314
|
+
await expect(canvas.getByText("Success!")).toBeInTheDocument();
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Multiple Components
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
// Card.stories.tsx
|
|
323
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
324
|
+
import { Card } from "./Card";
|
|
325
|
+
import { CardHeader } from "./CardHeader";
|
|
326
|
+
import { CardBody } from "./CardBody";
|
|
327
|
+
|
|
328
|
+
const meta = {
|
|
329
|
+
title: "Components/Card",
|
|
330
|
+
component: Card,
|
|
331
|
+
} satisfies Meta<typeof Card>;
|
|
332
|
+
|
|
333
|
+
export default meta;
|
|
334
|
+
type Story = StoryObj<typeof meta>;
|
|
335
|
+
|
|
336
|
+
export const Basic: Story = {
|
|
337
|
+
render: () => (
|
|
338
|
+
<Card>
|
|
339
|
+
<CardHeader>Title</CardHeader>
|
|
340
|
+
<CardBody>Content goes here</CardBody>
|
|
341
|
+
</Card>
|
|
342
|
+
),
|
|
343
|
+
};
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Customization
|
|
347
|
+
|
|
348
|
+
### Disable Specific Rules
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
export default eslintConfig({
|
|
352
|
+
storybook: true,
|
|
353
|
+
rules: {
|
|
354
|
+
// Allow non-PascalCase story names
|
|
355
|
+
"storybook/prefer-pascal-case": "off",
|
|
356
|
+
|
|
357
|
+
// Allow redundant story names
|
|
358
|
+
"storybook/no-redundant-story-name": "off",
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Custom Story Patterns
|
|
364
|
+
|
|
365
|
+
```js
|
|
366
|
+
export default eslintConfig({
|
|
367
|
+
storybook: true,
|
|
368
|
+
overrides: [
|
|
369
|
+
{
|
|
370
|
+
// Apply to custom story file pattern
|
|
371
|
+
files: ["**/*.stories.mdx"],
|
|
372
|
+
rules: {
|
|
373
|
+
// Custom rules for MDX stories
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Common Patterns
|
|
381
|
+
|
|
382
|
+
### Storybook + React + TypeScript
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
export default eslintConfig({
|
|
386
|
+
typescript: true,
|
|
387
|
+
react: true,
|
|
388
|
+
storybook: true,
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Storybook with Testing
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
396
|
+
import { expect, userEvent, within } from "@storybook/test";
|
|
397
|
+
|
|
398
|
+
// Story with comprehensive testing
|
|
399
|
+
export const InteractiveTest: Story = {
|
|
400
|
+
play: async ({ canvasElement }) => {
|
|
401
|
+
const canvas = within(canvasElement);
|
|
402
|
+
|
|
403
|
+
// Test user interactions
|
|
404
|
+
await userEvent.click(canvas.getByRole("button"));
|
|
405
|
+
|
|
406
|
+
// Verify results
|
|
407
|
+
await expect(canvas.getByText("Clicked")).toBeInTheDocument();
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Related Configurations
|
|
413
|
+
|
|
414
|
+
- [React](../react/README.md) - React configuration
|
|
415
|
+
- [TypeScript](../typescript/README.md) - TypeScript configuration
|
|
416
|
+
- [Testing](../testing/README.md) - Test configuration
|
|
417
|
+
|
|
418
|
+
## Learn More
|
|
419
|
+
|
|
420
|
+
- [eslint-plugin-storybook](https://github.com/storybookjs/eslint-plugin-storybook)
|
|
421
|
+
- [Storybook](https://storybook.js.org/)
|
|
422
|
+
- [Component Story Format (CSF)](https://storybook.js.org/docs/react/api/csf)
|
|
423
|
+
- [Storybook Interactions](https://storybook.js.org/docs/react/writing-tests/interaction-testing)
|
|
424
|
+
- [Main README](../../../README.md)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ESLint } from "eslint";
|
|
2
|
+
|
|
3
|
+
import storybookPlugin from "eslint-plugin-storybook";
|
|
4
|
+
|
|
5
|
+
import type { EslintConfigObject, EslintRuleConfig } from "../types.js";
|
|
6
|
+
|
|
7
|
+
import { configNames } from "../constants.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ESLint configuration for Storybook.
|
|
11
|
+
* Contains rules for best practices when working with Storybook.
|
|
12
|
+
*
|
|
13
|
+
* @param customRules - Optional custom rules to merge into the Storybook config.
|
|
14
|
+
* @returns Storybook ESLint config array.
|
|
15
|
+
*/
|
|
16
|
+
export const storybookConfig = (
|
|
17
|
+
customRules?: Record<string, EslintRuleConfig>,
|
|
18
|
+
): EslintConfigObject[] => [
|
|
19
|
+
{
|
|
20
|
+
files: [
|
|
21
|
+
"**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)",
|
|
22
|
+
"**/*.story.@(ts|tsx|js|jsx|mjs|cjs)",
|
|
23
|
+
],
|
|
24
|
+
name: configNames.storybook,
|
|
25
|
+
plugins: {
|
|
26
|
+
storybook: storybookPlugin as unknown as ESLint.Plugin,
|
|
27
|
+
},
|
|
28
|
+
rules: {
|
|
29
|
+
// Default Storybook rules
|
|
30
|
+
"import-x/no-anonymous-default-export": "off",
|
|
31
|
+
"react-hooks/rules-of-hooks": "off",
|
|
32
|
+
"storybook/await-interactions": "warn",
|
|
33
|
+
"storybook/context-in-play-function": "warn",
|
|
34
|
+
"storybook/csf-component": "warn",
|
|
35
|
+
"storybook/default-exports": "warn",
|
|
36
|
+
"storybook/hierarchy-separator": "warn",
|
|
37
|
+
"storybook/meta-inline-properties": "warn",
|
|
38
|
+
"storybook/no-redundant-story-name": "warn",
|
|
39
|
+
"storybook/prefer-pascal-case": "warn",
|
|
40
|
+
"storybook/story-exports": "warn",
|
|
41
|
+
"storybook/use-storybook-expect": "warn",
|
|
42
|
+
"storybook/use-storybook-testing-library": "warn",
|
|
43
|
+
// Merge custom rules
|
|
44
|
+
...(customRules ?? {}),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
files: [".storybook/main.@(js|cjs|mjs|ts)"],
|
|
49
|
+
name: configNames.storybookConfig,
|
|
50
|
+
plugins: {
|
|
51
|
+
storybook: storybookPlugin as unknown as ESLint.Plugin,
|
|
52
|
+
},
|
|
53
|
+
rules: {
|
|
54
|
+
"storybook/no-uninstalled-addons": "warn",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
];
|