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 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 | 🔧 | Included in configurations |
100
- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | --- | -------------------------------------------------------- |
101
- | [`storybook/await-interactions`](./docs/rules/await-interactions.md) | Interactions should be awaited | 🔧 | <ul><li>addon-interactions</li><li>recommended</li></ul> |
102
- | [`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> |
103
- | [`storybook/csf-component`](./docs/rules/csf-component.md) | The component property should be set | | <ul><li>csf</li></ul> |
104
- | [`storybook/default-exports`](./docs/rules/default-exports.md) | Story files should have a default export | 🔧 | <ul><li>csf</li><li>recommended</li></ul> |
105
- | [`storybook/hierarchy-separator`](./docs/rules/hierarchy-separator.md) | Deprecated hierarchy separator in title property | 🔧 | <ul><li>csf</li><li>recommended</li></ul> |
106
- | [`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> |
107
- | [`storybook/no-stories-of`](./docs/rules/no-stories-of.md) | storiesOf is deprecated and should not be used | | <ul><li>csf-strict</li></ul> |
108
- | [`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> |
109
- | [`storybook/prefer-pascal-case`](./docs/rules/prefer-pascal-case.md) | Stories should use PascalCase | 🔧 | <ul><li>recommended</li></ul> |
110
- | [`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> |
111
- | [`storybook/use-storybook-expect`](./docs/rules/use-storybook-expect.md) | Use expect from `@storybook/jest` | 🔧 | <ul><li>addon-interactions</li><li>recommended</li></ul> |
112
- | [`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> |
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
 
@@ -12,5 +12,11 @@ module.exports = {
12
12
  'storybook/use-storybook-testing-library': 'error',
13
13
  },
14
14
  },
15
+ {
16
+ files: ['main.@(js|cjs|mjs|ts)'],
17
+ rules: {
18
+ 'storybook/no-uninstalled-addons': 'error',
19
+ },
20
+ },
15
21
  ],
16
22
  };
@@ -13,5 +13,11 @@ module.exports = {
13
13
  'storybook/story-exports': 'error',
14
14
  },
15
15
  },
16
+ {
17
+ files: ['main.@(js|cjs|mjs|ts)'],
18
+ rules: {
19
+ 'storybook/no-uninstalled-addons': 'error',
20
+ },
21
+ },
16
22
  ],
17
23
  };
@@ -17,5 +17,11 @@ module.exports = {
17
17
  'storybook/use-storybook-testing-library': 'error',
18
18
  },
19
19
  },
20
+ {
21
+ files: ['main.@(js|cjs|mjs|ts)'],
22
+ rules: {
23
+ 'storybook/no-uninstalled-addons': 'error',
24
+ },
25
+ },
20
26
  ],
21
27
  };
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-storybook",
3
- "version": "0.5.13",
3
+ "version": "0.6.0",
4
4
  "description": "Best practice rules for Storybook",
5
5
  "keywords": [
6
6
  "eslint",