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 +91 -17
- package/dist/configs/addon-interactions.js +10 -0
- package/dist/configs/csf-strict.js +9 -0
- package/dist/configs/csf.js +11 -0
- package/dist/configs/recommended.js +14 -0
- package/dist/index.js +20 -0
- package/dist/rules/await-interactions.js +106 -0
- package/dist/rules/csf-component.js +49 -0
- package/dist/rules/default-exports.js +52 -0
- package/dist/rules/hierarchy-separator.js +67 -0
- package/dist/rules/meta-inline-properties.js +83 -0
- package/dist/rules/no-redundant-story-name.js +107 -0
- package/dist/rules/no-stories-of.js +43 -0
- package/dist/rules/no-title-property-in-meta.js +60 -0
- package/dist/rules/prefer-pascal-case.js +121 -0
- package/dist/rules/use-storybook-expect.js +102 -0
- package/dist/rules/use-storybook-testing-library.js +103 -0
- package/dist/types/index.js +15 -0
- package/dist/utils/ast.js +35 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/create-storybook-rule.js +21 -0
- package/dist/utils/index.js +49 -0
- package/package.json +81 -11
- package/.editorconfig +0 -16
- package/docs/rules/prefer-csf.md +0 -28
- package/lib/index.js +0 -3
- package/lib/rules/prefer-csf.js +0 -27
- package/tests/lib/rules/prefer-csf.js +0 -32
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
|
-
|
|
31
|
+
Best practice rules for Storybook
|
|
4
32
|
|
|
5
33
|
## Installation
|
|
6
34
|
|
|
7
|
-
You'll first need to install [ESLint](
|
|
35
|
+
You'll first need to install [ESLint](https://eslint.org/):
|
|
8
36
|
|
|
9
|
-
```
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
### Run the plugin only against story files
|
|
34
60
|
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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,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
|
+
});
|