🚀 Big News:Socket Has Acquired Secure Annex.Learn More
Socket
Book a DemoSign in
Socket

eslint-plugin-storybook

Package Overview
Dependencies
Maintainers
2
Versions
1042
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

eslint-plugin-storybook - npm Package Compare versions

Comparing version
0.0.1-alpha.0
to
0.0.1-alpha.1
+175
lib/rules/await-interactions.js
/**
* @fileoverview Interactions should be awaited
* @author Yann Braga
*/
'use strict'
const { docsUrl, isPlayFunction } = require('../utils')
const { CATEGORY_ID } = require('../utils/constants')
const {
isCallExpression,
isMemberExpression,
isIdentifier,
isExpressionStatement,
isVariableDeclaration,
isVariableDeclarator,
isAwaitExpression,
} = require('../utils/ast')
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: {
type: null, // `problem`, `suggestion`, or `layout`
docs: {
description: 'Interactions should be awaited',
category: CATEGORY_ID.ADDON_INTERACTIONS,
recommended: true,
recommendedConfig: 'error', // or 'warn'
url: docsUrl('await-interactions'), // URL to the documentation page for this rule
},
messages: {
interactionShouldBeAwaited: 'Interaction should be awaited: {{method}}',
fixSuggestion: 'Add `await` to method',
},
// fixable: 'code', // Or `code` or `whitespace`
},
create(context) {
// variables should be defined here
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
// any helper functions should go here or else delete this section
const FUNCTIONS_TO_BE_AWAITED = [
'expect',
'waitFor',
'waitForElementToBeRemoved',
'wait',
'waitForElement',
'waitForDomChange',
'userEvent',
]
/**
* @param {import('eslint').Rule.Node[]} body
*/
const getNonAwaitedCallExpressions = (body = []) => {
return body
.filter((b) => {
return isExpressionStatement(b) && isCallExpression(b.expression)
})
.map((d) => d.expression)
}
/**
* @param {import('eslint').Rule.Node[]} body
*/
const getNonAwaitedInitializations = (body = []) => {
const initializations = body
.flatMap((b) => {
return (
isVariableDeclaration(b) &&
b.declarations
.filter((d) => isVariableDeclarator(d) && !isAwaitExpression(d.init))
.map((d) => d.init)
)
})
.filter(Boolean)
return initializations
}
const getMethodThatShouldBeAwaited = (expression = {}) => {
const shouldAwait = (name) => {
console.log('should I await..', name)
return FUNCTIONS_TO_BE_AWAITED.includes(name) || name.startsWith('findBy')
}
if (
isCallExpression(expression) &&
isMemberExpression(expression.callee) &&
isIdentifier(expression.callee.object) &&
shouldAwait(expression.callee.object.name)
) {
return expression.callee.object
}
if (
isCallExpression(expression) &&
isMemberExpression(expression.callee) &&
isIdentifier(expression.callee.property) &&
shouldAwait(expression.callee.property.name)
) {
return expression.callee.property
}
return null
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* @param {import('eslint').Rule.Node} node
*/
let invocationsThatShouldBeAwaited = []
return {
AssignmentExpression(node) {
if (!isExpressionStatement(node.parent)) {
return null
}
if (isPlayFunction(node)) {
const { right } = node
const expressionBody = (right.body && right.body.body) || []
const callExpressions = [
...getNonAwaitedCallExpressions(expressionBody),
...getNonAwaitedInitializations(expressionBody),
]
callExpressions.forEach((expression) => {
const method = getMethodThatShouldBeAwaited(expression)
if (method) {
invocationsThatShouldBeAwaited.push(method)
}
})
}
},
'Program:exit': function () {
if (invocationsThatShouldBeAwaited.length) {
invocationsThatShouldBeAwaited.forEach((node) => {
context.report({
node,
messageId: 'interactionShouldBeAwaited',
data: {
method: node.name,
},
// @TODO: make this auto-fixable. Currently it's pretty dumb so something like this can happen:
// canvas.findByText => canvas.await findByText
// instead of the correct: await canvas.findByText
// fix: function (fixer) {
// return fixer.insertTextBefore(node, 'await ')
// },
// suggest: [
// {
// messageId: 'fixSuggestion',
// fix: function (fixer) {
// return fixer.insertTextBefore(node, 'await ')
// },
// },
// ],
})
})
}
},
}
},
}
const { AST_NODE_TYPES, ASTUtils } = require('@typescript-eslint/experimental-utils')
const isNodeOfType = (nodeType) => (node) => node && node.type === nodeType
const isArrayExpression = isNodeOfType(AST_NODE_TYPES.ArrayExpression)
const isArrowFunctionExpression = isNodeOfType(AST_NODE_TYPES.ArrowFunctionExpression)
const isBlockStatement = isNodeOfType(AST_NODE_TYPES.BlockStatement)
const isCallExpression = isNodeOfType(AST_NODE_TYPES.CallExpression)
const isExpressionStatement = isNodeOfType(AST_NODE_TYPES.ExpressionStatement)
const isVariableDeclaration = isNodeOfType(AST_NODE_TYPES.VariableDeclaration)
const isAssignmentExpression = isNodeOfType(AST_NODE_TYPES.AssignmentExpression)
const isSequenceExpression = isNodeOfType(AST_NODE_TYPES.SequenceExpression)
const isImportDeclaration = isNodeOfType(AST_NODE_TYPES.ImportDeclaration)
const isImportDefaultSpecifier = isNodeOfType(AST_NODE_TYPES.ImportDefaultSpecifier)
const isImportNamespaceSpecifier = isNodeOfType(AST_NODE_TYPES.ImportNamespaceSpecifier)
const isImportSpecifier = isNodeOfType(AST_NODE_TYPES.ImportSpecifier)
const isJSXAttribute = isNodeOfType(AST_NODE_TYPES.JSXAttribute)
const isLiteral = isNodeOfType(AST_NODE_TYPES.Literal)
const isMemberExpression = isNodeOfType(AST_NODE_TYPES.MemberExpression)
const isNewExpression = isNodeOfType(AST_NODE_TYPES.NewExpression)
const isObjectExpression = isNodeOfType(AST_NODE_TYPES.ObjectExpression)
const isObjectPattern = isNodeOfType(AST_NODE_TYPES.ObjectPattern)
const isProperty = isNodeOfType(AST_NODE_TYPES.Property)
const isReturnStatement = isNodeOfType(AST_NODE_TYPES.ReturnStatement)
const isFunctionExpression = isNodeOfType(AST_NODE_TYPES.FunctionExpression)
module.exports = Object.assign(
{
isArrayExpression,
isArrowFunctionExpression,
isBlockStatement,
isCallExpression,
isExpressionStatement,
isVariableDeclaration,
isAssignmentExpression,
isSequenceExpression,
isImportDeclaration,
isImportDefaultSpecifier,
isImportNamespaceSpecifier,
isImportSpecifier,
isJSXAttribute,
isLiteral,
isMemberExpression,
isNewExpression,
isObjectExpression,
isObjectPattern,
isProperty,
isReturnStatement,
isFunctionExpression,
},
ASTUtils
)
+1
-0

@@ -10,2 +10,3 @@ /*

'import/no-anonymous-default-export': 'off',
'storybook/await-interactions': 'error',
'storybook/use-storybook-expect': 'error',

@@ -12,0 +13,0 @@ 'storybook/use-storybook-testing-library': 'error',

+0
-6

@@ -13,10 +13,4 @@ /*

'storybook/hierarchy-separator': 'warn',
'storybook/meta-inline-properties': [
'error',
{
csfVersion: 3,
},
],
'storybook/no-redundant-story-name': 'warn',
},
}

@@ -10,10 +10,5 @@ /*

'import/no-anonymous-default-export': 'off',
'storybook/await-interactions': 'error',
'storybook/default-exports': 'error',
'storybook/hierarchy-separator': 'warn',
'storybook/meta-inline-properties': [
'error',
{
csfVersion: 3,
},
],
'storybook/no-redundant-story-name': 'warn',

@@ -20,0 +15,0 @@ 'storybook/prefer-pascal-case': 'warn',

@@ -25,3 +25,3 @@ /**

messages: {
description: 'Missing component property.',
missingComponentProperty: 'Missing component property.',
},

@@ -61,3 +61,3 @@ fixable: null, // Or `code` or `whitespace`

node,
messageId: 'description',
messageId: 'missingComponentProperty',
})

@@ -64,0 +64,0 @@ }

@@ -25,3 +25,3 @@ /**

messages: {
description: 'The file should have a default export.',
shouldHaveDefaultExport: 'The file should have a default export.',
},

@@ -57,3 +57,3 @@ },

loc: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
messageId: 'description',
messageId: 'shouldHaveDefaultExport',
})

@@ -60,0 +60,0 @@ }

@@ -26,3 +26,3 @@ /**

messages: {
fixSuggestion: 'Use correct separators',
useCorrectSeparators: 'Use correct separators',
deprecatedHierarchySeparator:

@@ -46,7 +46,7 @@ 'Deprecated hierachy separator in title property: {{metaTitle}}.',

if (!titleNode) {
if (!titleNode || !titleNode.value.type === 'Literal') {
return
}
const metaTitle = titleNode.value.raw
const metaTitle = titleNode.value.raw || ''

@@ -64,3 +64,3 @@ if (metaTitle.includes('|') || metaTitle.includes('.')) {

{
messageId: 'fixSuggestion',
messageId: 'useCorrectSeparators',
fix: function (fixer) {

@@ -67,0 +67,0 @@ return fixer.replaceTextRange(

@@ -21,2 +21,3 @@ /**

recommended: true,
excludeFromConfig: true,
recommendedConfig: ['error', { csfVersion: 3 }],

@@ -26,3 +27,3 @@ url: docsUrl('meta-inline-properties'), // URL to the documentation page for this rule

messages: {
description: 'Meta should only have inline properties: {{properties}}',
metaShouldHaveInlineProperties: 'Meta should only have inline properties: {{property}}',
},

@@ -52,3 +53,9 @@ schema: [

//----------------------------------------------------------------------
const isInline = (node) => {
return (
node.value.type === 'ObjectExpression' ||
node.value.type === 'Literal' ||
node.value.type === 'ArrayExpression'
)
}
// any helper functions should go here or else delete this section

@@ -77,9 +84,4 @@

metaNodes.forEach((metaNode) => {
const isDynamic =
metaNode.shorthand ||
metaNode.value.type === 'BinaryExpression' ||
(metaNode.value.type === 'TemplateLiteral' && metaNode.value.expressions.length > 0)
if (isDynamic) {
dynamicProperties.push(metaNode.key.name)
if (!isInline(metaNode)) {
dynamicProperties.push(metaNode)
}

@@ -89,9 +91,10 @@ })

if (dynamicProperties.length > 0) {
context.report({
node,
loc: node.loc,
messageId: 'description',
data: {
properties: dynamicProperties.join(', '),
},
dynamicProperties.forEach((propertyNode) => {
context.report({
node: propertyNode,
messageId: 'metaShouldHaveInlineProperties',
data: {
property: propertyNode.key.name,
},
})
})

@@ -98,0 +101,0 @@ }

@@ -26,4 +26,4 @@ /**

messages: {
fixSuggestion: 'Remove redundant name',
description:
removeRedundantName: 'Remove redundant name',
storyNameIsRedundant:
'Named exports should not use the name annotation if it is redundant to the name that would be generated by the export name',

@@ -87,6 +87,6 @@ },

node: storyNameNode,
messageId: 'description',
messageId: 'storyNameIsRedundant',
suggest: [
{
messageId: 'fixSuggestion',
messageId: 'removeRedundantName',
fix: function (fixer) {

@@ -93,0 +93,0 @@ return fixer.remove(storyNameNode)

@@ -25,3 +25,3 @@ /**

messages: {
description: 'storiesOf is deprecated and should not be used',
doNotUseStoriesOf: 'storiesOf is deprecated and should not be used',
},

@@ -48,3 +48,3 @@ },

node,
messageId: 'description',
messageId: 'doNotUseStoriesOf',
})

@@ -51,0 +51,0 @@ }

@@ -27,4 +27,4 @@ /**

messages: {
fixSuggestion: 'Do not define a title in meta',
description: `CSF3 does not need a title in meta`,
removeTitleInMeta: 'Do not define a title in meta',
noTitleInMeta: `CSF3 does not need a title in meta`,
},

@@ -49,3 +49,3 @@ },

node,
messageId: 'description',
messageId: 'noTitleInMeta',
// In case we want this to be auto fixed by --fix

@@ -59,3 +59,3 @@ // fix: function (fixer) {

{
messageId: 'fixSuggestion',
messageId: 'removeTitleInMeta',
fix: function (fixer) {

@@ -62,0 +62,0 @@ // @TODO this suggestion keeps the comma and might result in error:

@@ -24,4 +24,4 @@ /**

messages: {
fixSuggestion: 'Use pascal case',
description: 'The story should use PascalCase notation: {{name}}',
convertToPascalCase: 'Use pascal case',
usePascalCase: 'The story should use PascalCase notation: {{name}}',
},

@@ -75,7 +75,7 @@ },

node: identifier,
messageId: 'description',
messageId: 'usePascalCase',
data: { name },
suggest: [
{
messageId: 'fixSuggestion',
messageId: 'convertToPascalCase',
fix: function (fixer) {

@@ -82,0 +82,0 @@ return fixer.replaceTextRange(identifier.range, toPascalCase(name))

@@ -9,2 +9,8 @@ /**

const { CATEGORY_ID } = require('../utils/constants')
const {
isExpressionStatement,
isCallExpression,
isMemberExpression,
isIdentifier,
} = require('../utils/ast')

@@ -28,4 +34,4 @@ //------------------------------------------------------------------------------

messages: {
fixSuggestion: 'Update imports',
description:
updateImports: 'Update imports',
useExpectFromStorybook:
'Do not use expect from jest directly in the story. You should use from `@storybook/jest` instead.',

@@ -43,7 +49,13 @@ },

const getExpressionStatements = (body = []) => {
return body.filter((b) => b.type === 'ExpressionStatement')
return body.filter((b) => isExpressionStatement(b))
}
const isExpect = (expression = {}) => {
return expression.callee.object.callee.name === 'expect'
return (
isCallExpression(expression) &&
isMemberExpression(expression.callee) &&
isCallExpression(expression.callee.object) &&
isIdentifier(expression.callee.object.callee) &&
expression.callee.object.callee.name === 'expect'
)
}

@@ -71,2 +83,6 @@

AssignmentExpression(node) {
if (!isExpressionStatement(node.parent)) {
return null
}
if (isPlayFunction(node)) {

@@ -88,3 +104,3 @@ const { right } = node

node,
messageId: 'description',
messageId: 'useExpectFromStorybook',
fix: function (fixer) {

@@ -98,3 +114,3 @@ return fixer.insertTextAfterRange(

{
messageId: 'fixSuggestion',
messageId: 'updateImports',
fix: function (fixer) {

@@ -101,0 +117,0 @@ return fixer.insertTextAfterRange(

@@ -27,4 +27,4 @@ /**

messages: {
fixSuggestion: 'Update imports',
description:
updateImports: 'Update imports',
dontUseTestingLibraryDirectly:
'Do not use `{{library}}` directly in the story. You should import the functions from `@storybook/testing-library` instead.',

@@ -57,3 +57,3 @@ },

node,
messageId: 'description',
messageId: 'dontUseTestingLibraryDirectly',
data: {

@@ -70,3 +70,3 @@ library: node.source.value,

{
messageId: 'fixSuggestion',
messageId: 'updateImports',
fix: function (fixer) {

@@ -73,0 +73,0 @@ return fixer.replaceTextRange(

{
"name": "eslint-plugin-storybook",
"version": "0.0.1-alpha.0",
"version": "0.0.1-alpha.1",
"description": "Best practice rules for Storybook",

@@ -12,2 +12,8 @@ "keywords": [

"author": "yannbf@gmail.com",
"contributors": [
{
"name": "Rafael Rozon",
"email": "rafaelrozon.developer@gmail.com"
}
],
"main": "lib/index.js",

@@ -38,3 +44,4 @@ "files": [

"dependencies": {
"requireindex": "^1.1.0"
"requireindex": "^1.1.0",
"@typescript-eslint/experimental-utils": "^5.3.0"
},

@@ -41,0 +48,0 @@ "devDependencies": {

@@ -108,6 +108,6 @@ <p align="center">

| ------------------------------------------------------------------------------------------ | ------------------------------------------------- | --- | ------------------------------- |
| [`storybook/await-interactions`](./docs/rules/await-interactions.md) | Interactions should be awaited | | addon-interactions, recommended |
| [`storybook/csf-component`](./docs/rules/csf-component.md) | The component property should be set | | csf |
| [`storybook/default-exports`](./docs/rules/default-exports.md) | Story files should have a default export | | csf, recommended |
| [`storybook/hierarchy-separator`](./docs/rules/hierarchy-separator.md) | Deprecated hierachy separator in title property | 🔧 | csf, recommended |
| [`storybook/meta-inline-properties`](./docs/rules/meta-inline-properties.md) | Meta should only have inline properties | | csf, recommended |
| [`storybook/no-redundant-story-name`](./docs/rules/no-redundant-story-name.md) | A story should not have a redundant name property | 🔧 | csf, recommended |

@@ -114,0 +114,0 @@ | [`storybook/no-stories-of`](./docs/rules/no-stories-of.md) | storiesOf is deprecated and should not be used | | csf-strict |