eslint-plugin-storybook 0.5.13 → 0.6.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/CHANGELOG.md +13 -0
- package/README.md +26 -14
- package/dist/configs/addon-interactions.js +6 -0
- package/dist/configs/csf.js +6 -0
- package/dist/configs/recommended.js +6 -0
- package/dist/rules/no-uninstalled-addons.js +149 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# v0.6.0 (Sun Jul 10 2022)
|
|
2
|
+
|
|
3
|
+
#### 🚀 Enhancement
|
|
4
|
+
|
|
5
|
+
- feat(no-uninstalled-addons): add uninstalled plugin rule [#96](https://github.com/storybookjs/eslint-plugin-storybook/pull/96) ([@andrelas1](https://github.com/andrelas1) [@yannbf](https://github.com/yannbf))
|
|
6
|
+
|
|
7
|
+
#### Authors: 2
|
|
8
|
+
|
|
9
|
+
- Andre Luis Araujo Santos ([@andrelas1](https://github.com/andrelas1))
|
|
10
|
+
- Yann Braga ([@yannbf](https://github.com/yannbf))
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
1
14
|
# v0.5.13 (Fri Jun 24 2022)
|
|
2
15
|
|
|
3
16
|
#### ⚠️ Pushed to `main`
|
package/README.md
CHANGED
|
@@ -48,6 +48,17 @@ npm install eslint-plugin-storybook --save-dev
|
|
|
48
48
|
yarn add eslint-plugin-storybook --dev
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
And finally, add this to your `.eslintignore` file:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
// Inside your .eslintignore file
|
|
55
|
+
!.storybook
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This allows for this plugin to also lint your configuration files inside the .storybook folder, so that you always have a correct configuration and don't face any issues regarding mistyped addon names, for instance.
|
|
59
|
+
|
|
60
|
+
> For more info on why this line is required in the .eslintignore file, check this [ESLint documentation](https://eslint.org/docs/latest/user-guide/configuring/ignoring-code#:~:text=In%20addition%20to,contents%2C%20are%20ignored).
|
|
61
|
+
|
|
51
62
|
## Usage
|
|
52
63
|
|
|
53
64
|
Use `.eslintrc.*` file to configure rules. See also: https://eslint.org/docs/user-guide/configuring
|
|
@@ -96,20 +107,21 @@ This plugin does not support MDX files.
|
|
|
96
107
|
|
|
97
108
|
**Configurations**: csf, csf-strict, addon-interactions, recommended
|
|
98
109
|
|
|
99
|
-
| Name | Description
|
|
100
|
-
| ------------------------------------------------------------------------------------------ |
|
|
101
|
-
| [`storybook/await-interactions`](./docs/rules/await-interactions.md) | Interactions should be awaited
|
|
102
|
-
| [`storybook/context-in-play-function`](./docs/rules/context-in-play-function.md) | Pass a context when invoking play function of another story
|
|
103
|
-
| [`storybook/csf-component`](./docs/rules/csf-component.md) | The component property should be set
|
|
104
|
-
| [`storybook/default-exports`](./docs/rules/default-exports.md) | Story files should have a default export
|
|
105
|
-
| [`storybook/hierarchy-separator`](./docs/rules/hierarchy-separator.md) | Deprecated hierarchy separator in title property
|
|
106
|
-
| [`storybook/no-redundant-story-name`](./docs/rules/no-redundant-story-name.md) | A story should not have a redundant name property
|
|
107
|
-
| [`storybook/no-stories-of`](./docs/rules/no-stories-of.md) | storiesOf is deprecated and should not be used
|
|
108
|
-
| [`storybook/no-title-property-in-meta`](./docs/rules/no-title-property-in-meta.md) | Do not define a title in meta
|
|
109
|
-
| [`storybook/
|
|
110
|
-
| [`storybook/
|
|
111
|
-
| [`storybook/
|
|
112
|
-
| [`storybook/use-storybook-
|
|
110
|
+
| Name | Description | 🔧 | Included in configurations |
|
|
111
|
+
| ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | --- | -------------------------------------------------------- |
|
|
112
|
+
| [`storybook/await-interactions`](./docs/rules/await-interactions.md) | Interactions should be awaited | 🔧 | <ul><li>addon-interactions</li><li>recommended</li></ul> |
|
|
113
|
+
| [`storybook/context-in-play-function`](./docs/rules/context-in-play-function.md) | Pass a context when invoking play function of another story | | <ul><li>recommended</li><li>addon-interactions</li></ul> |
|
|
114
|
+
| [`storybook/csf-component`](./docs/rules/csf-component.md) | The component property should be set | | <ul><li>csf</li></ul> |
|
|
115
|
+
| [`storybook/default-exports`](./docs/rules/default-exports.md) | Story files should have a default export | 🔧 | <ul><li>csf</li><li>recommended</li></ul> |
|
|
116
|
+
| [`storybook/hierarchy-separator`](./docs/rules/hierarchy-separator.md) | Deprecated hierarchy separator in title property | 🔧 | <ul><li>csf</li><li>recommended</li></ul> |
|
|
117
|
+
| [`storybook/no-redundant-story-name`](./docs/rules/no-redundant-story-name.md) | A story should not have a redundant name property | 🔧 | <ul><li>csf</li><li>recommended</li></ul> |
|
|
118
|
+
| [`storybook/no-stories-of`](./docs/rules/no-stories-of.md) | storiesOf is deprecated and should not be used | | <ul><li>csf-strict</li></ul> |
|
|
119
|
+
| [`storybook/no-title-property-in-meta`](./docs/rules/no-title-property-in-meta.md) | Do not define a title in meta | 🔧 | <ul><li>csf-strict</li></ul> |
|
|
120
|
+
| [`storybook/no-uninstalled-addons`](./docs/rules/no-uninstalled-addons.md) | This rule identifies storybook addons that are invalid because they are either not installed or contain a typo in their name. | | <ul><li>recommended</li></ul> |
|
|
121
|
+
| [`storybook/prefer-pascal-case`](./docs/rules/prefer-pascal-case.md) | Stories should use PascalCase | 🔧 | <ul><li>recommended</li></ul> |
|
|
122
|
+
| [`storybook/story-exports`](./docs/rules/story-exports.md) | A story file must contain at least one story export | | <ul><li>recommended</li><li>csf</li></ul> |
|
|
123
|
+
| [`storybook/use-storybook-expect`](./docs/rules/use-storybook-expect.md) | Use expect from `@storybook/jest` | 🔧 | <ul><li>addon-interactions</li><li>recommended</li></ul> |
|
|
124
|
+
| [`storybook/use-storybook-testing-library`](./docs/rules/use-storybook-testing-library.md) | Do not use testing-library directly on stories | 🔧 | <ul><li>addon-interactions</li><li>recommended</li></ul> |
|
|
113
125
|
|
|
114
126
|
<!-- RULES-LIST:END -->
|
|
115
127
|
|
package/dist/configs/csf.js
CHANGED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview This rule identifies storybook addons that are invalid because they are either not installed or contain a typo in their name.
|
|
4
|
+
* @author Andre "andrelas1" Santos
|
|
5
|
+
*/
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const create_storybook_rule_1 = require("../utils/create-storybook-rule");
|
|
9
|
+
const constants_1 = require("../utils/constants");
|
|
10
|
+
const ast_1 = require("../utils/ast");
|
|
11
|
+
module.exports = (0, create_storybook_rule_1.createStorybookRule)({
|
|
12
|
+
name: 'no-uninstalled-addons',
|
|
13
|
+
defaultOptions: [],
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'This rule identifies storybook addons that are invalid because they are either not installed or contain a typo in their name.',
|
|
18
|
+
categories: [constants_1.CategoryId.RECOMMENDED],
|
|
19
|
+
recommended: 'error', // or 'error'
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
addonIsNotInstalled: `The {{ addonName }} is not installed. Did you forget to install it?`,
|
|
23
|
+
},
|
|
24
|
+
schema: [], // Add a schema if the rule has options. Otherwise remove this
|
|
25
|
+
},
|
|
26
|
+
create(context) {
|
|
27
|
+
// variables should be defined here
|
|
28
|
+
//----------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
//----------------------------------------------------------------------
|
|
31
|
+
// this will not only exclude the nullables but it will also exclude the type undefined from them, so that TS does not complain
|
|
32
|
+
function excludeNullable(item) {
|
|
33
|
+
return !!item;
|
|
34
|
+
}
|
|
35
|
+
const mergeDepsWithDevDeps = (packageJson) => {
|
|
36
|
+
const deps = Object.keys(packageJson.dependencies || {});
|
|
37
|
+
const devDeps = Object.keys(packageJson.devDependencies || {});
|
|
38
|
+
return [...deps, ...devDeps];
|
|
39
|
+
};
|
|
40
|
+
const isAddonInstalled = (addon, installedAddons) => {
|
|
41
|
+
// cleanup /register or /preset from registered addon
|
|
42
|
+
const addonName = addon.replace(/\/register$/, '').replace(/\/preset$/, '');
|
|
43
|
+
return installedAddons.includes(addonName);
|
|
44
|
+
};
|
|
45
|
+
const areThereAddonsNotInstalled = (addons, installedSbAddons) => {
|
|
46
|
+
const result = addons
|
|
47
|
+
.filter((addon) => !isAddonInstalled(addon, installedSbAddons))
|
|
48
|
+
.map((addon) => ({ name: addon }));
|
|
49
|
+
return result.length ? result : false;
|
|
50
|
+
};
|
|
51
|
+
const getPackageJson = (path) => {
|
|
52
|
+
const packageJson = {
|
|
53
|
+
devDependencies: {},
|
|
54
|
+
dependencies: {},
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
const file = (0, fs_1.readFileSync)(path, 'utf8');
|
|
58
|
+
const parsedFile = JSON.parse(file);
|
|
59
|
+
packageJson.dependencies = parsedFile.dependencies || {};
|
|
60
|
+
packageJson.devDependencies = parsedFile.devDependencies || {};
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
throw new Error('Could not fetch package.json - it is probably not in the same directory as the .storybook folder');
|
|
64
|
+
}
|
|
65
|
+
return packageJson;
|
|
66
|
+
};
|
|
67
|
+
const extractAllAddonsFromTheStorybookConfig = (addonsExpression) => {
|
|
68
|
+
if (addonsExpression === null || addonsExpression === void 0 ? void 0 : addonsExpression.elements) {
|
|
69
|
+
// extract all nodes taht are a string inside the addons array
|
|
70
|
+
const nodesWithAddons = addonsExpression.elements
|
|
71
|
+
.map((elem) => ((0, ast_1.isLiteral)(elem) ? { value: elem.value, node: elem } : undefined))
|
|
72
|
+
.filter(excludeNullable);
|
|
73
|
+
const listOfAddonsInString = nodesWithAddons.map((elem) => elem.value);
|
|
74
|
+
// extract all nodes that are an object inside the addons array
|
|
75
|
+
const nodesWithAddonsInObj = addonsExpression.elements
|
|
76
|
+
.map((elem) => ((0, ast_1.isObjectExpression)(elem) ? elem : { properties: [] }))
|
|
77
|
+
.map((elem) => {
|
|
78
|
+
const property = elem.properties.find((prop) => (0, ast_1.isProperty)(prop) && (0, ast_1.isIdentifier)(prop.key) && prop.key.name === 'name');
|
|
79
|
+
return (0, ast_1.isLiteral)(property === null || property === void 0 ? void 0 : property.value)
|
|
80
|
+
? { value: property.value.value, node: property.value }
|
|
81
|
+
: undefined;
|
|
82
|
+
})
|
|
83
|
+
.filter(excludeNullable);
|
|
84
|
+
const listOfAddonsInObj = nodesWithAddonsInObj.map((elem) => elem.value);
|
|
85
|
+
const listOfAddons = [...listOfAddonsInString, ...listOfAddonsInObj];
|
|
86
|
+
const listOfAddonElements = [...nodesWithAddons, ...nodesWithAddonsInObj];
|
|
87
|
+
return { listOfAddons, listOfAddonElements };
|
|
88
|
+
}
|
|
89
|
+
return { listOfAddons: [], listOfAddonElements: [] };
|
|
90
|
+
};
|
|
91
|
+
function reportUninstalledAddons(addonsProp) {
|
|
92
|
+
// when this is running for .storybook/main.js, we get the path to the folder which contains the package.json of the
|
|
93
|
+
// project. This will be handy for monorepos that may be running ESLint in a node process in another folder.
|
|
94
|
+
const projectRoot = context.getPhysicalFilename
|
|
95
|
+
? (0, path_1.resolve)(context.getPhysicalFilename(), '../../')
|
|
96
|
+
: './';
|
|
97
|
+
let packageJsonObject;
|
|
98
|
+
try {
|
|
99
|
+
packageJsonObject = getPackageJson(`${projectRoot}/package.json`);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
// if we cannot find the package.json, we cannot check if the addons are installed
|
|
103
|
+
console.error(e);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const depsAndDevDeps = mergeDepsWithDevDeps(packageJsonObject);
|
|
107
|
+
const { listOfAddons, listOfAddonElements } = extractAllAddonsFromTheStorybookConfig(addonsProp);
|
|
108
|
+
const result = areThereAddonsNotInstalled(listOfAddons, depsAndDevDeps);
|
|
109
|
+
if (result) {
|
|
110
|
+
const elemsWithErrors = listOfAddonElements.filter((elem) => !!result.find((addon) => addon.name === elem.value));
|
|
111
|
+
elemsWithErrors.forEach((elem) => {
|
|
112
|
+
context.report({
|
|
113
|
+
node: elem.node,
|
|
114
|
+
messageId: 'addonIsNotInstalled',
|
|
115
|
+
data: { addonName: elem.value },
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//----------------------------------------------------------------------
|
|
121
|
+
// Public
|
|
122
|
+
//----------------------------------------------------------------------
|
|
123
|
+
return {
|
|
124
|
+
AssignmentExpression: function (node) {
|
|
125
|
+
if ((0, ast_1.isObjectExpression)(node.right)) {
|
|
126
|
+
const addonsProp = node.right.properties.find((prop) => (0, ast_1.isProperty)(prop) && (0, ast_1.isIdentifier)(prop.key) && prop.key.name === 'addons');
|
|
127
|
+
if (addonsProp && addonsProp.value && (0, ast_1.isArrayExpression)(addonsProp.value)) {
|
|
128
|
+
reportUninstalledAddons(addonsProp.value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
ExportDefaultDeclaration: function (node) {
|
|
133
|
+
if ((0, ast_1.isObjectExpression)(node.declaration)) {
|
|
134
|
+
const addonsProp = node.declaration.properties.find((prop) => (0, ast_1.isProperty)(prop) && (0, ast_1.isIdentifier)(prop.key) && prop.key.name === 'addons');
|
|
135
|
+
if (addonsProp && addonsProp.value && (0, ast_1.isArrayExpression)(addonsProp.value)) {
|
|
136
|
+
reportUninstalledAddons(addonsProp.value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
ExportNamedDeclaration: function (node) {
|
|
141
|
+
const addonsProp = (0, ast_1.isVariableDeclaration)(node.declaration) &&
|
|
142
|
+
node.declaration.declarations.find((decl) => (0, ast_1.isVariableDeclarator)(decl) && (0, ast_1.isIdentifier)(decl.id) && decl.id.name === 'addons');
|
|
143
|
+
if (addonsProp && (0, ast_1.isArrayExpression)(addonsProp.init)) {
|
|
144
|
+
reportUninstalledAddons(addonsProp.init);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
});
|