no-uninstalled-addons.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. var __importDefault = (this && this.__importDefault) || function (mod) {
  7. return (mod && mod.__esModule) ? mod : { "default": mod };
  8. };
  9. const fs_1 = require("fs");
  10. const ts_dedent_1 = __importDefault(require("ts-dedent"));
  11. const path_1 = require("path");
  12. const create_storybook_rule_1 = require("../utils/create-storybook-rule");
  13. const constants_1 = require("../utils/constants");
  14. const ast_1 = require("../utils/ast");
  15. const utils_1 = require("../utils");
  16. module.exports = (0, create_storybook_rule_1.createStorybookRule)({
  17. name: 'no-uninstalled-addons',
  18. defaultOptions: [
  19. {
  20. packageJsonLocation: '',
  21. ignore: [],
  22. },
  23. ],
  24. meta: {
  25. type: 'problem',
  26. docs: {
  27. description: 'This rule identifies storybook addons that are invalid because they are either not installed or contain a typo in their name.',
  28. categories: [constants_1.CategoryId.RECOMMENDED],
  29. recommended: 'error', // or 'error'
  30. },
  31. messages: {
  32. addonIsNotInstalled: `The {{ addonName }} is not installed in {{packageJsonPath}}. Did you forget to install it or is your package.json in a different location?`,
  33. },
  34. schema: [
  35. {
  36. type: 'object',
  37. properties: {
  38. packageJsonLocation: {
  39. type: 'string',
  40. },
  41. ignore: {
  42. type: 'array',
  43. items: {
  44. type: 'string',
  45. },
  46. },
  47. },
  48. },
  49. ],
  50. },
  51. create(context) {
  52. // variables should be defined here
  53. const { packageJsonLocation, ignore } = context.options.reduce((acc, val) => {
  54. return {
  55. packageJsonLocation: val['packageJsonLocation'] || acc.packageJsonLocation,
  56. ignore: val['ignore'] || acc.ignore,
  57. };
  58. }, { packageJsonLocation: '', ignore: [] });
  59. //----------------------------------------------------------------------
  60. // Helpers
  61. //----------------------------------------------------------------------
  62. // this will not only exclude the nullables but it will also exclude the type undefined from them, so that TS does not complain
  63. function excludeNullable(item) {
  64. return !!item;
  65. }
  66. const mergeDepsWithDevDeps = (packageJson) => {
  67. const deps = Object.keys(packageJson.dependencies || {});
  68. const devDeps = Object.keys(packageJson.devDependencies || {});
  69. return [...deps, ...devDeps];
  70. };
  71. const isAddonInstalled = (addon, installedAddons) => {
  72. // cleanup /register or /preset + file extension from registered addon
  73. const addonName = addon
  74. .replace(/\.[mc]?js$/, '')
  75. .replace(/\/register$/, '')
  76. .replace(/\/preset$/, '');
  77. return installedAddons.includes(addonName);
  78. };
  79. const filterLocalAddons = (addon) => {
  80. const isLocalAddon = (addon) => addon.startsWith('.') ||
  81. addon.startsWith('/') ||
  82. // for local Windows files e.g. (C: F: D:)
  83. /\w:.*/.test(addon) ||
  84. addon.startsWith('\\');
  85. return !isLocalAddon(addon);
  86. };
  87. const areThereAddonsNotInstalled = (addons, installedSbAddons) => {
  88. const result = addons
  89. // remove local addons (e.g. ./my-addon/register.js)
  90. .filter(filterLocalAddons)
  91. .filter((addon) => !isAddonInstalled(addon, installedSbAddons) && !ignore.includes(addon))
  92. .map((addon) => ({ name: addon }));
  93. return result.length ? result : false;
  94. };
  95. const getPackageJson = (path) => {
  96. const packageJson = {
  97. devDependencies: {},
  98. dependencies: {},
  99. };
  100. try {
  101. const file = (0, fs_1.readFileSync)(path, 'utf8');
  102. const parsedFile = JSON.parse(file);
  103. packageJson.dependencies = parsedFile.dependencies || {};
  104. packageJson.devDependencies = parsedFile.devDependencies || {};
  105. }
  106. catch (e) {
  107. throw new Error((0, ts_dedent_1.default) `The provided path in your eslintrc.json - ${path} is not a valid path to a package.json file or your package.json file is not in the same folder as ESLint is running from.
  108. Read more at: https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/no-uninstalled-addons.md
  109. `);
  110. }
  111. return packageJson;
  112. };
  113. const extractAllAddonsFromTheStorybookConfig = (addonsExpression) => {
  114. if (addonsExpression === null || addonsExpression === void 0 ? void 0 : addonsExpression.elements) {
  115. // extract all nodes taht are a string inside the addons array
  116. const nodesWithAddons = addonsExpression.elements
  117. .map((elem) => ((0, ast_1.isLiteral)(elem) ? { value: elem.value, node: elem } : undefined))
  118. .filter(excludeNullable);
  119. const listOfAddonsInString = nodesWithAddons.map((elem) => elem.value);
  120. // extract all nodes that are an object inside the addons array
  121. const nodesWithAddonsInObj = addonsExpression.elements
  122. .map((elem) => ((0, ast_1.isObjectExpression)(elem) ? elem : { properties: [] }))
  123. .map((elem) => {
  124. const property = elem.properties.find((prop) => (0, ast_1.isProperty)(prop) && (0, ast_1.isIdentifier)(prop.key) && prop.key.name === 'name');
  125. return (0, ast_1.isLiteral)(property === null || property === void 0 ? void 0 : property.value)
  126. ? { value: property.value.value, node: property.value }
  127. : undefined;
  128. })
  129. .filter(excludeNullable);
  130. const listOfAddonsInObj = nodesWithAddonsInObj.map((elem) => elem.value);
  131. const listOfAddons = [...listOfAddonsInString, ...listOfAddonsInObj];
  132. const listOfAddonElements = [...nodesWithAddons, ...nodesWithAddonsInObj];
  133. return { listOfAddons, listOfAddonElements };
  134. }
  135. return { listOfAddons: [], listOfAddonElements: [] };
  136. };
  137. function reportUninstalledAddons(addonsProp) {
  138. const packageJsonPath = (0, path_1.resolve)(packageJsonLocation || `./package.json`);
  139. let packageJsonObject;
  140. try {
  141. packageJsonObject = getPackageJson(packageJsonPath);
  142. }
  143. catch (e) {
  144. // if we cannot find the package.json, we cannot check if the addons are installed
  145. throw new Error(e);
  146. }
  147. const depsAndDevDeps = mergeDepsWithDevDeps(packageJsonObject);
  148. const { listOfAddons, listOfAddonElements } = extractAllAddonsFromTheStorybookConfig(addonsProp);
  149. const result = areThereAddonsNotInstalled(listOfAddons, depsAndDevDeps);
  150. if (result) {
  151. const elemsWithErrors = listOfAddonElements.filter((elem) => !!result.find((addon) => addon.name === elem.value));
  152. const rootDir = process.cwd().split(path_1.sep).pop();
  153. const packageJsonPath = `${rootDir}${path_1.sep}${(0, path_1.relative)(process.cwd(), packageJsonLocation)}`;
  154. elemsWithErrors.forEach((elem) => {
  155. context.report({
  156. node: elem.node,
  157. messageId: 'addonIsNotInstalled',
  158. data: {
  159. addonName: elem.value,
  160. packageJsonPath,
  161. },
  162. });
  163. });
  164. }
  165. }
  166. function findAddonsPropAndReport(node) {
  167. const addonsProp = node.properties.find((prop) => (0, ast_1.isProperty)(prop) && (0, ast_1.isIdentifier)(prop.key) && prop.key.name === 'addons');
  168. if ((addonsProp === null || addonsProp === void 0 ? void 0 : addonsProp.value) && (0, ast_1.isArrayExpression)(addonsProp.value)) {
  169. reportUninstalledAddons(addonsProp.value);
  170. }
  171. }
  172. //----------------------------------------------------------------------
  173. // Public
  174. //----------------------------------------------------------------------
  175. return {
  176. AssignmentExpression: function (node) {
  177. if ((0, ast_1.isObjectExpression)(node.right)) {
  178. findAddonsPropAndReport(node.right);
  179. }
  180. },
  181. ExportDefaultDeclaration: function (node) {
  182. const meta = (0, utils_1.getMetaObjectExpression)(node, context);
  183. if (!meta)
  184. return null;
  185. findAddonsPropAndReport(meta);
  186. },
  187. ExportNamedDeclaration: function (node) {
  188. const addonsProp = (0, ast_1.isVariableDeclaration)(node.declaration) &&
  189. node.declaration.declarations.find((decl) => (0, ast_1.isVariableDeclarator)(decl) && (0, ast_1.isIdentifier)(decl.id) && decl.id.name === 'addons');
  190. if (addonsProp && (0, ast_1.isArrayExpression)(addonsProp.init)) {
  191. reportUninstalledAddons(addonsProp.init);
  192. }
  193. },
  194. };
  195. },
  196. });