js-style-kit 0.6.1 → 0.8.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.
Files changed (67) hide show
  1. package/README.md +5 -0
  2. package/dist/bin/index.cjs +2 -1
  3. package/dist/bin/index.cjs.map +1 -1
  4. package/dist/index.d.ts +23 -7
  5. package/dist/index.js +135 -36
  6. package/dist/index.js.map +1 -1
  7. package/package.json +10 -7
  8. package/src/eslint/base/README.md +186 -0
  9. package/src/eslint/base/config.ts +37 -0
  10. package/src/eslint/base/rules.ts +444 -0
  11. package/src/eslint/base/types.ts +20 -0
  12. package/src/eslint/constants.ts +52 -0
  13. package/src/eslint/convex/README.md +30 -0
  14. package/src/eslint/convex/config.ts +34 -0
  15. package/src/eslint/convex/rules.ts +8 -0
  16. package/src/eslint/convex/types.ts +8 -0
  17. package/src/eslint/ignores.ts +34 -0
  18. package/src/eslint/import/README.md +397 -0
  19. package/src/eslint/import/config.ts +48 -0
  20. package/src/eslint/import/rules.ts +81 -0
  21. package/src/eslint/index.ts +273 -0
  22. package/src/eslint/jsdoc/README.md +399 -0
  23. package/src/eslint/jsdoc/config.ts +29 -0
  24. package/src/eslint/jsdoc/rules.ts +81 -0
  25. package/src/eslint/jsdoc/types.ts +56 -0
  26. package/src/eslint/nextjs/config.ts +25 -0
  27. package/src/eslint/nextjs/rules.ts +25 -0
  28. package/src/eslint/nextjs/types.ts +27 -0
  29. package/src/eslint/perfectionist/README.md +454 -0
  30. package/src/eslint/perfectionist/config.ts +25 -0
  31. package/src/eslint/perfectionist/rules.ts +39 -0
  32. package/src/eslint/prefer-arrow-function/config.ts +33 -0
  33. package/src/eslint/prefer-arrow-function/types.ts +13 -0
  34. package/src/eslint/process-custom-rules.ts +72 -0
  35. package/src/eslint/query/README.md +254 -0
  36. package/src/eslint/query/config.ts +27 -0
  37. package/src/eslint/query/rules.ts +11 -0
  38. package/src/eslint/query/types.ts +11 -0
  39. package/src/eslint/react/README.md +416 -0
  40. package/src/eslint/react/config.ts +65 -0
  41. package/src/eslint/react/rules.ts +188 -0
  42. package/src/eslint/react/types.ts +26 -0
  43. package/src/eslint/react-refresh/config.ts +28 -0
  44. package/src/eslint/react-refresh/rules.ts +48 -0
  45. package/src/eslint/storybook/README.md +424 -0
  46. package/src/eslint/storybook/config.ts +57 -0
  47. package/src/eslint/testing/README.md +436 -0
  48. package/src/eslint/testing/config.ts +99 -0
  49. package/src/eslint/testing/get-import-restrictions.ts +70 -0
  50. package/src/eslint/testing/jest-rules.ts +47 -0
  51. package/src/eslint/testing/vitest-rules.ts +42 -0
  52. package/src/eslint/turbo/README.md +380 -0
  53. package/src/eslint/turbo/config.ts +26 -0
  54. package/src/eslint/turbo/types.ts +7 -0
  55. package/src/eslint/types.ts +36 -0
  56. package/src/eslint/typescript/README.md +229 -0
  57. package/src/eslint/typescript/config.ts +48 -0
  58. package/src/eslint/typescript/rules.ts +137 -0
  59. package/src/eslint/typescript/types.ts +35 -0
  60. package/src/eslint/unicorn/README.md +497 -0
  61. package/src/eslint/unicorn/config.ts +36 -0
  62. package/src/eslint/unicorn/rules.ts +86 -0
  63. package/src/index.ts +3 -0
  64. package/src/modules.d.ts +5 -0
  65. package/src/prettier/README.md +413 -0
  66. package/src/prettier/index.ts +110 -0
  67. 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
+ ];