eslint-plugin-storybook 0.1.0 → 0.2.2

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 CHANGED
@@ -1,21 +1,48 @@
1
+ <p align="center">
2
+ <a href="https://storybook.js.org/">
3
+ <img src="https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png" alt="Storybook" width="400" />
4
+ </a>
5
+ </p>
6
+
7
+ <p align="center">Build bulletproof UI components faster</p>
8
+
9
+ <br/>
10
+
11
+ <p align="center">
12
+ <a href="https://discord.gg/storybook">
13
+ <img src="https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat" />
14
+ </a>
15
+ <a href="https://storybook.js.org/community/">
16
+ <img src="https://img.shields.io/badge/community-join-4BC424.svg" alt="Storybook Community" />
17
+ </a>
18
+ <a href="#backers">
19
+ <img src="https://opencollective.com/storybook/backers/badge.svg" alt="Backers on Open Collective" />
20
+ </a>
21
+ <a href="#sponsors">
22
+ <img src="https://opencollective.com/storybook/sponsors/badge.svg" alt="Sponsors on Open Collective" />
23
+ </a>
24
+ <a href="https://twitter.com/intent/follow?screen_name=storybookjs">
25
+ <img src="https://badgen.net/twitter/follow/storybookjs?icon=twitter&label=%40storybookjs" alt="Official Twitter Handle" />
26
+ </a>
27
+ </p>
28
+
1
29
  # eslint-plugin-storybook
2
30
 
3
- Eslint rules for Storybook Best Practices
31
+ Best practice rules for Storybook
4
32
 
5
33
  ## Installation
6
34
 
7
- You'll first need to install [ESLint](http://eslint.org):
35
+ You'll first need to install [ESLint](https://eslint.org/):
8
36
 
9
- ```
10
- $ npm i eslint --save-dev
37
+ ```sh
38
+ npm i eslint --save-dev
11
39
  ```
12
40
 
13
41
  Next, install `eslint-plugin-storybook`:
14
42
 
43
+ ```sh
44
+ npm install eslint-plugin-storybook --save-dev
15
45
  ```
16
- $ npm install eslint-plugin-storybook --save-dev
17
- ```
18
-
19
46
 
20
47
  ## Usage
21
48
 
@@ -23,28 +50,75 @@ Add `storybook` to the plugins section of your `.eslintrc` configuration file. Y
23
50
 
24
51
  ```json
25
52
  {
26
- "plugins": [
27
- "storybook"
28
- ]
53
+ "plugins": ["storybook"]
29
54
  }
30
55
  ```
31
56
 
57
+ Then, define which rule configurations to extend in your eslint file. Before that, it's important to understand that **Storybook linting rules should only be applied in your stories files**. You don't want rules to affect your other files such as production or test code as the rules might conflict with rules from other ESLint plugins.
32
58
 
33
- Then configure the rules you want to use under the rules section.
59
+ ### Run the plugin only against story files
34
60
 
35
- ```json
61
+ We don't want `eslint-plugin-storybook` to run against your whole codebase. To run this plugin only against your stories files, you have the following options:
62
+
63
+ #### ESLint `overrides`
64
+
65
+ One way of restricting ESLint config by file patterns is by using [ESLint `overrides`](https://eslint.org/docs/user-guide/configuring/configuration-files#configuration-based-on-glob-patterns).
66
+
67
+ Assuming you are using the recommended `.stories` extension in your files, the following config would run `eslint-plugin-storybook` only against your stories files:
68
+
69
+ ```javascript
70
+ // .eslintrc
36
71
  {
37
- "rules": {
38
- "storybook/prefer-csf": "error"
39
- }
40
- }
72
+ // 1) Here we have our usual config which applies to the whole project, so we don't put storybook preset here.
73
+ "extends": ["airbnb", "plugin:prettier/recommended"],
74
+
75
+ // 2) We load eslint-plugin-storybook globally with other ESLint plugins.
76
+ "plugins": ["react-hooks", "storybook"],
77
+
78
+ "overrides": [
79
+ {
80
+ // 3) Now we enable eslint-plugin-storybook rules or preset only for matching files!
81
+ // you can use the one defined in your main.js
82
+ "files": ['src/**/*.stories.@(js|jsx|ts|tsx)'],
83
+ "extends": ["plugin:storybook/recommended"],
84
+
85
+ // 4) Optional: you can override or disable specific rules here if you want. Else delete this
86
+ "rules": {
87
+ 'storybook/no-redundant-story-name': 'error'
88
+ }
89
+ },
90
+ ],
91
+ };
41
92
  ```
42
93
 
94
+ #### ESLint Cascading and Hierarchy
95
+
96
+ Another approach for customizing ESLint config by paths is through [ESLint Cascading and Hierarchy](https://eslint.org/docs/user-guide/configuring/configuration-files#cascading-and-hierarchy). This is useful if all your stories are placed under the same folder, so you can place there another `.eslintrc` where you enable `eslint-plugin-storybook` for applying it only to the files under such folder, rather than enabling it on your global `.eslintrc` which would apply to your whole project.
97
+
43
98
  ## Supported Rules
44
99
 
45
- * prefer-csf: Display lint message when storiesOf API is used instead of the Component Story Format
100
+ <!-- RULES-LIST:START -->
101
+
102
+ **Key**: 🔧 = fixable
46
103
 
104
+ **Configurations**: csf, csf-strict, addon-interactions, recommended
47
105
 
106
+ | Name | Description | 🔧 | Included in configurations |
107
+ | ------------------------------------------------------------------------------------------ | ------------------------------------------------- | --- | ------------------------------- |
108
+ | [`storybook/await-interactions`](./docs/rules/await-interactions.md) | Interactions should be awaited | 🔧 | addon-interactions, recommended |
109
+ | [`storybook/csf-component`](./docs/rules/csf-component.md) | The component property should be set | | csf |
110
+ | [`storybook/default-exports`](./docs/rules/default-exports.md) | Story files should have a default export | | csf, recommended |
111
+ | [`storybook/hierarchy-separator`](./docs/rules/hierarchy-separator.md) | Deprecated hierachy separator in title property | 🔧 | csf, recommended |
112
+ | [`storybook/no-redundant-story-name`](./docs/rules/no-redundant-story-name.md) | A story should not have a redundant name property | 🔧 | csf, recommended |
113
+ | [`storybook/no-stories-of`](./docs/rules/no-stories-of.md) | storiesOf is deprecated and should not be used | | csf-strict |
114
+ | [`storybook/no-title-property-in-meta`](./docs/rules/no-title-property-in-meta.md) | Do not define a title in meta | 🔧 | csf-strict |
115
+ | [`storybook/prefer-pascal-case`](./docs/rules/prefer-pascal-case.md) | Stories should use PascalCase | 🔧 | recommended |
116
+ | [`storybook/use-storybook-expect`](./docs/rules/use-storybook-expect.md) | Use expect from `@storybook/jest` | 🔧 | addon-interactions, recommended |
117
+ | [`storybook/use-storybook-testing-library`](./docs/rules/use-storybook-testing-library.md) | Do not use testing-library directly on stories | 🔧 | addon-interactions, recommended |
48
118
 
119
+ <!-- RULES-LIST:END -->
49
120
 
121
+ ## Contributors
50
122
 
123
+ Looking into improving this plugin? That would be awesome!
124
+ Please refer to [the contributing guidelines](./CONTRIBUTING.md) for steps to contributing.
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ module.exports = {
3
+ plugins: ['storybook'],
4
+ rules: {
5
+ 'import/no-anonymous-default-export': 'off',
6
+ 'storybook/await-interactions': 'error',
7
+ 'storybook/use-storybook-expect': 'error',
8
+ 'storybook/use-storybook-testing-library': 'error',
9
+ },
10
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ module.exports = {
3
+ extends: require.resolve('./csf'),
4
+ rules: {
5
+ 'import/no-anonymous-default-export': 'off',
6
+ 'storybook/no-stories-of': 'error',
7
+ 'storybook/no-title-property-in-meta': 'error',
8
+ },
9
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ module.exports = {
3
+ plugins: ['storybook'],
4
+ rules: {
5
+ 'import/no-anonymous-default-export': 'off',
6
+ 'storybook/csf-component': 'warn',
7
+ 'storybook/default-exports': 'error',
8
+ 'storybook/hierarchy-separator': 'warn',
9
+ 'storybook/no-redundant-story-name': 'warn',
10
+ },
11
+ };
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ module.exports = {
3
+ plugins: ['storybook'],
4
+ rules: {
5
+ 'import/no-anonymous-default-export': 'off',
6
+ 'storybook/await-interactions': 'error',
7
+ 'storybook/default-exports': 'error',
8
+ 'storybook/hierarchy-separator': 'warn',
9
+ 'storybook/no-redundant-story-name': 'warn',
10
+ 'storybook/prefer-pascal-case': 'warn',
11
+ 'storybook/use-storybook-expect': 'error',
12
+ 'storybook/use-storybook-testing-library': 'error',
13
+ },
14
+ };
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Best practice rules for Storybook
4
+ * @author Yann Braga
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.configs = exports.rules = void 0;
11
+ //------------------------------------------------------------------------------
12
+ // Requirements
13
+ //------------------------------------------------------------------------------
14
+ const requireindex_1 = __importDefault(require("requireindex"));
15
+ //------------------------------------------------------------------------------
16
+ // Plugin Definition
17
+ //------------------------------------------------------------------------------
18
+ // import all rules in lib/rules
19
+ exports.rules = (0, requireindex_1.default)(__dirname + '/rules');
20
+ exports.configs = (0, requireindex_1.default)(__dirname + '/configs');
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Interactions should be awaited
4
+ * @author Yann Braga
5
+ */
6
+ const create_storybook_rule_1 = require("../utils/create-storybook-rule");
7
+ const constants_1 = require("../utils/constants");
8
+ const ast_1 = require("../utils/ast");
9
+ module.exports = (0, create_storybook_rule_1.createStorybookRule)({
10
+ name: 'await-interactions',
11
+ defaultOptions: [],
12
+ meta: {
13
+ docs: {
14
+ description: 'Interactions should be awaited',
15
+ categories: [constants_1.CategoryId.ADDON_INTERACTIONS, constants_1.CategoryId.RECOMMENDED],
16
+ recommended: 'error', // or 'warn'
17
+ },
18
+ messages: {
19
+ interactionShouldBeAwaited: 'Interaction should be awaited: {{method}}',
20
+ fixSuggestion: 'Add `await` to method',
21
+ },
22
+ type: 'problem',
23
+ fixable: 'code',
24
+ schema: [],
25
+ },
26
+ create(context) {
27
+ // variables should be defined here
28
+ //----------------------------------------------------------------------
29
+ // Helpers
30
+ //----------------------------------------------------------------------
31
+ // any helper functions should go here or else delete this section
32
+ const FUNCTIONS_TO_BE_AWAITED = [
33
+ 'waitFor',
34
+ 'waitForElementToBeRemoved',
35
+ 'wait',
36
+ 'waitForElement',
37
+ 'waitForDomChange',
38
+ 'userEvent',
39
+ ];
40
+ const getMethodThatShouldBeAwaited = (expr) => {
41
+ const shouldAwait = (name) => {
42
+ return FUNCTIONS_TO_BE_AWAITED.includes(name) || name.startsWith('findBy');
43
+ };
44
+ if ((0, ast_1.isMemberExpression)(expr.callee) &&
45
+ (0, ast_1.isIdentifier)(expr.callee.object) &&
46
+ shouldAwait(expr.callee.object.name)) {
47
+ return expr.callee.object;
48
+ }
49
+ if ((0, ast_1.isMemberExpression)(expr.callee) &&
50
+ (0, ast_1.isIdentifier)(expr.callee.property) &&
51
+ shouldAwait(expr.callee.property.name)) {
52
+ return expr.callee.property;
53
+ }
54
+ if ((0, ast_1.isMemberExpression)(expr.callee) &&
55
+ (0, ast_1.isCallExpression)(expr.callee.object) &&
56
+ (0, ast_1.isIdentifier)(expr.callee.object.callee) &&
57
+ (0, ast_1.isIdentifier)(expr.callee.property) &&
58
+ expr.callee.object.callee.name === 'expect') {
59
+ return expr.callee.property;
60
+ }
61
+ if ((0, ast_1.isIdentifier)(expr.callee) && shouldAwait(expr.callee.name)) {
62
+ return expr.callee;
63
+ }
64
+ return null;
65
+ };
66
+ //----------------------------------------------------------------------
67
+ // Public
68
+ //----------------------------------------------------------------------
69
+ /**
70
+ * @param {import('eslint').Rule.Node} node
71
+ */
72
+ let invocationsThatShouldBeAwaited = [];
73
+ return {
74
+ CallExpression(node) {
75
+ const method = getMethodThatShouldBeAwaited(node);
76
+ if (method && !(0, ast_1.isAwaitExpression)(node.parent)) {
77
+ invocationsThatShouldBeAwaited.push({ node, method });
78
+ }
79
+ },
80
+ 'Program:exit': function () {
81
+ if (invocationsThatShouldBeAwaited.length) {
82
+ invocationsThatShouldBeAwaited.forEach(({ node, method }) => {
83
+ context.report({
84
+ node,
85
+ messageId: 'interactionShouldBeAwaited',
86
+ data: {
87
+ method: method.name,
88
+ },
89
+ fix: function (fixer) {
90
+ return fixer.insertTextBefore(node, 'await ');
91
+ },
92
+ suggest: [
93
+ {
94
+ messageId: 'fixSuggestion',
95
+ fix: function (fixer) {
96
+ return fixer.insertTextBefore(node, 'await ');
97
+ },
98
+ },
99
+ ],
100
+ });
101
+ });
102
+ }
103
+ },
104
+ };
105
+ },
106
+ });
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Component property should be set
4
+ * @author Yann Braga
5
+ */
6
+ const utils_1 = require("../utils");
7
+ const constants_1 = require("../utils/constants");
8
+ const create_storybook_rule_1 = require("../utils/create-storybook-rule");
9
+ module.exports = (0, create_storybook_rule_1.createStorybookRule)({
10
+ name: 'csf-component',
11
+ defaultOptions: [],
12
+ meta: {
13
+ type: 'suggestion',
14
+ docs: {
15
+ description: 'The component property should be set',
16
+ categories: [constants_1.CategoryId.CSF],
17
+ recommended: 'warn',
18
+ },
19
+ messages: {
20
+ missingComponentProperty: 'Missing component property.',
21
+ },
22
+ schema: [],
23
+ },
24
+ create(context) {
25
+ // variables should be defined here
26
+ //----------------------------------------------------------------------
27
+ // Helpers
28
+ //----------------------------------------------------------------------
29
+ // any helper functions should go here or else delete this section
30
+ //----------------------------------------------------------------------
31
+ // Public
32
+ //----------------------------------------------------------------------
33
+ return {
34
+ ExportDefaultDeclaration(node) {
35
+ const meta = (0, utils_1.getMetaObjectExpression)(node, context);
36
+ if (!meta) {
37
+ return null;
38
+ }
39
+ const componentProperty = meta.properties.find((property) => property.key.name === 'component');
40
+ if (!componentProperty) {
41
+ context.report({
42
+ node,
43
+ messageId: 'missingComponentProperty',
44
+ });
45
+ }
46
+ },
47
+ };
48
+ },
49
+ });
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Story files should have a default export
4
+ * @author Yann Braga
5
+ */
6
+ const constants_1 = require("../utils/constants");
7
+ const ast_1 = require("../utils/ast");
8
+ const create_storybook_rule_1 = require("../utils/create-storybook-rule");
9
+ module.exports = (0, create_storybook_rule_1.createStorybookRule)({
10
+ name: 'default-export',
11
+ defaultOptions: [],
12
+ meta: {
13
+ type: 'problem',
14
+ docs: {
15
+ description: 'Story files should have a default export',
16
+ categories: [constants_1.CategoryId.CSF, constants_1.CategoryId.RECOMMENDED],
17
+ recommended: 'error',
18
+ },
19
+ messages: {
20
+ shouldHaveDefaultExport: 'The file should have a default export.',
21
+ },
22
+ schema: [],
23
+ },
24
+ create(context) {
25
+ // variables should be defined here
26
+ //----------------------------------------------------------------------
27
+ // Helpers
28
+ //----------------------------------------------------------------------
29
+ // any helper functions should go here or else delete this section
30
+ //----------------------------------------------------------------------
31
+ // Public
32
+ //----------------------------------------------------------------------
33
+ let hasDefaultExport = false;
34
+ return {
35
+ ExportDefaultSpecifier: function () {
36
+ hasDefaultExport = true;
37
+ },
38
+ ExportDefaultDeclaration: function () {
39
+ hasDefaultExport = true;
40
+ },
41
+ 'Program:exit': function (node) {
42
+ if (!hasDefaultExport) {
43
+ const firstNonImportStatement = node.body.find((n) => !(0, ast_1.isImportDeclaration)(n));
44
+ context.report({
45
+ node: firstNonImportStatement || node.body[0] || node,
46
+ messageId: 'shouldHaveDefaultExport',
47
+ });
48
+ }
49
+ },
50
+ };
51
+ },
52
+ });
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Deprecated hierarchy separator
4
+ * @author Yann Braga
5
+ */
6
+ const utils_1 = require("../utils");
7
+ const ast_1 = require("../utils/ast");
8
+ const constants_1 = require("../utils/constants");
9
+ const create_storybook_rule_1 = require("../utils/create-storybook-rule");
10
+ module.exports = (0, create_storybook_rule_1.createStorybookRule)({
11
+ name: 'hierarchy-separator',
12
+ defaultOptions: [],
13
+ meta: {
14
+ type: 'problem',
15
+ fixable: 'code',
16
+ docs: {
17
+ description: 'Deprecated hierachy separator in title property',
18
+ categories: [constants_1.CategoryId.CSF, constants_1.CategoryId.RECOMMENDED],
19
+ recommended: 'warn',
20
+ },
21
+ messages: {
22
+ useCorrectSeparators: 'Use correct separators',
23
+ deprecatedHierarchySeparator: 'Deprecated hierachy separator in title property: {{metaTitle}}.',
24
+ },
25
+ schema: [],
26
+ },
27
+ create: function (context) {
28
+ return {
29
+ ExportDefaultDeclaration: function (node) {
30
+ const meta = (0, utils_1.getMetaObjectExpression)(node, context);
31
+ if (!meta) {
32
+ return null;
33
+ }
34
+ const titleNode = meta.properties.find((prop) => prop.key.name === 'title');
35
+ //@ts-ignore
36
+ if (!titleNode || !(0, ast_1.isLiteral)(titleNode.value)) {
37
+ return;
38
+ }
39
+ //@ts-ignore
40
+ const metaTitle = titleNode.value.raw || '';
41
+ if (metaTitle.includes('|') || metaTitle.includes('.')) {
42
+ context.report({
43
+ node,
44
+ messageId: 'deprecatedHierarchySeparator',
45
+ data: { metaTitle },
46
+ // In case we want this to be auto fixed by --fix
47
+ fix: function (fixer) {
48
+ return fixer.replaceTextRange(
49
+ //@ts-ignore
50
+ titleNode.value.range, metaTitle.replace(/\||\./g, '/'));
51
+ },
52
+ suggest: [
53
+ {
54
+ messageId: 'useCorrectSeparators',
55
+ fix: function (fixer) {
56
+ return fixer.replaceTextRange(
57
+ //@ts-ignore
58
+ titleNode.value.range, metaTitle.replace(/\||\./g, '/'));
59
+ },
60
+ },
61
+ ],
62
+ });
63
+ }
64
+ },
65
+ };
66
+ },
67
+ });
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Meta should have inline properties
4
+ * @author Yann Braga
5
+ */
6
+ const utils_1 = require("../utils");
7
+ const constants_1 = require("../utils/constants");
8
+ const create_storybook_rule_1 = require("../utils/create-storybook-rule");
9
+ module.exports = (0, create_storybook_rule_1.createStorybookRule)({
10
+ name: 'meta-inline-properties',
11
+ defaultOptions: [{ csfVersion: 3 }],
12
+ meta: {
13
+ type: 'problem',
14
+ docs: {
15
+ description: 'Meta should only have inline properties',
16
+ categories: [constants_1.CategoryId.CSF, constants_1.CategoryId.RECOMMENDED],
17
+ excludeFromConfig: true,
18
+ recommended: 'error',
19
+ },
20
+ messages: {
21
+ metaShouldHaveInlineProperties: 'Meta should only have inline properties: {{property}}',
22
+ },
23
+ schema: [
24
+ {
25
+ type: 'object',
26
+ properties: {
27
+ csfVersion: {
28
+ type: 'number',
29
+ },
30
+ },
31
+ additionalProperties: false,
32
+ },
33
+ ],
34
+ },
35
+ create(context) {
36
+ // variables should be defined here
37
+ // In case we need to get options defined in the rule schema
38
+ // const options = context.options[0] || {}
39
+ // const csfVersion = options.csfVersion
40
+ //----------------------------------------------------------------------
41
+ // Helpers
42
+ //----------------------------------------------------------------------
43
+ const isInline = (node) => {
44
+ return (node.value.type === 'ObjectExpression' ||
45
+ node.value.type === 'Literal' ||
46
+ node.value.type === 'ArrayExpression');
47
+ };
48
+ // any helper functions should go here or else delete this section
49
+ //----------------------------------------------------------------------
50
+ // Public
51
+ //----------------------------------------------------------------------
52
+ return {
53
+ ExportDefaultDeclaration(node) {
54
+ const meta = (0, utils_1.getMetaObjectExpression)(node, context);
55
+ if (!meta) {
56
+ return null;
57
+ }
58
+ const ruleProperties = ['title', 'args'];
59
+ let dynamicProperties = [];
60
+ const metaNodes = meta.properties.filter((prop) =>
61
+ //@ts-ignore
62
+ ruleProperties.includes(prop.key.name));
63
+ metaNodes.forEach((metaNode) => {
64
+ if (!isInline(metaNode)) {
65
+ dynamicProperties.push(metaNode);
66
+ }
67
+ });
68
+ if (dynamicProperties.length > 0) {
69
+ //@ts-ignore
70
+ dynamicProperties.forEach((propertyNode) => {
71
+ context.report({
72
+ node: propertyNode,
73
+ messageId: 'metaShouldHaveInlineProperties',
74
+ data: {
75
+ property: propertyNode.key.name,
76
+ },
77
+ });
78
+ });
79
+ }
80
+ },
81
+ };
82
+ },
83
+ });