Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: storybookjs/eslint-plugin-storybook
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.11.6
Choose a base ref
...
head repository: storybookjs/eslint-plugin-storybook
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.12.0
Choose a head ref
  • 12 commits
  • 8 files changed
  • 4 contributors

Commits on Mar 20, 2025

  1. Generate files

    NotWoods authored and Sidnioulz committed Mar 20, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    Sidnioulz Steve Dodier-Lazaro
    Copy the full SHA
    a2fb2ac View commit details
  2. Add meta-satisfies-type rule

    NotWoods authored and Sidnioulz committed Mar 20, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    Sidnioulz Steve Dodier-Lazaro
    Copy the full SHA
    070028f View commit details
  3. Allow 0 categories

    NotWoods authored and Sidnioulz committed Mar 20, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    Sidnioulz Steve Dodier-Lazaro
    Copy the full SHA
    91a356c View commit details
  4. Remove typo test file

    NotWoods authored and Sidnioulz committed Mar 20, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    Sidnioulz Steve Dodier-Lazaro
    Copy the full SHA
    cd5b6d9 View commit details
  5. docs: Improve formatting of rule examples

    Sidnioulz committed Mar 20, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    Sidnioulz Steve Dodier-Lazaro
    Copy the full SHA
    ee8fdad View commit details
  6. Verified

    This commit was signed with the committer’s verified signature.
    Sidnioulz Steve Dodier-Lazaro
    Copy the full SHA
    dc47940 View commit details
  7. Run update-all

    Sidnioulz committed Mar 20, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    Sidnioulz Steve Dodier-Lazaro
    Copy the full SHA
    844b02c View commit details

Commits on Mar 21, 2025

  1. Preserve excludeFromConfig check

    Sidnioulz committed Mar 21, 2025

    Verified

    This commit was signed with the committer’s verified signature.
    Sidnioulz Steve Dodier-Lazaro
    Copy the full SHA
    6c891fc View commit details

Commits on Mar 25, 2025

  1. Run update-all

    kasperpeulen committed Mar 25, 2025
    Copy the full SHA
    0748c68 View commit details
  2. Merge pull request #200 from storybookjs/feat/meta-satisfies-type

    Add meta-satisfies-type rule (rebased)
    kasperpeulen authored Mar 25, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    29e5062 View commit details
  3. Copy the full SHA
    64b02ce View commit details
  4. Copy the full SHA
    3f75087 View commit details
Showing with 265 additions and 6 deletions.
  1. +14 −0 CHANGELOG.md
  2. +1 −0 README.md
  3. +59 −0 docs/rules/meta-satisfies-type.md
  4. +2 −0 lib/index.ts
  5. +115 −0 lib/rules/meta-satisfies-type.ts
  6. +1 −1 package.json
  7. +72 −0 tests/lib/rules/meta-satisfies-type.test.ts
  8. +1 −5 tools/utils/categories.ts
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# v0.12.0 (Tue Mar 25 2025)

#### 🚀 Enhancement

- Add meta-satisfies-type rule (rebased) [#200](https://github.com/storybookjs/eslint-plugin-storybook/pull/200) ([@NotWoods](https://github.com/NotWoods) [@Sidnioulz](https://github.com/Sidnioulz) [@kasperpeulen](https://github.com/kasperpeulen))

#### Authors: 3

- Kasper Peulen ([@kasperpeulen](https://github.com/kasperpeulen))
- Steve Dodier-Lazaro ([@Sidnioulz](https://github.com/Sidnioulz))
- Tiger Oakes ([@NotWoods](https://github.com/NotWoods))

---

# v0.11.6 (Mon Mar 17 2025)

#### 🐛 Bug Fix
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -180,6 +180,7 @@ This plugin does not support MDX files.
| [`storybook/default-exports`](./docs/rules/default-exports.md) | Story files should have a default export | 🔧 | <ul><li>csf</li><li>flat/csf</li><li>recommended</li><li>flat/recommended</li><li>csf-strict</li><li>flat/csf-strict</li></ul> |
| [`storybook/hierarchy-separator`](./docs/rules/hierarchy-separator.md) | Deprecated hierarchy separator in title property | 🔧 | <ul><li>csf</li><li>flat/csf</li><li>recommended</li><li>flat/recommended</li><li>csf-strict</li><li>flat/csf-strict</li></ul> |
| [`storybook/meta-inline-properties`](./docs/rules/meta-inline-properties.md) | Meta should only have inline properties | | N/A |
| [`storybook/meta-satisfies-type`](./docs/rules/meta-satisfies-type.md) | Meta should use `satisfies Meta` | 🔧 | N/A |
| [`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>flat/csf</li><li>recommended</li><li>flat/recommended</li><li>csf-strict</li><li>flat/csf-strict</li></ul> |
| [`storybook/no-stories-of`](./docs/rules/no-stories-of.md) | storiesOf is deprecated and should not be used | | <ul><li>csf-strict</li><li>flat/csf-strict</li></ul> |
| [`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><li>flat/csf-strict</li></ul> |
59 changes: 59 additions & 0 deletions docs/rules/meta-satisfies-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Meta should be followed by `satisfies Meta` (meta-satisfies-type)

<!-- RULE-CATEGORIES:START -->

**Included in these configurations**: N/A

<!-- RULE-CATEGORIES:END -->

## Rule Details

This rule enforces writing `satisfies Meta` after the meta object definition. This is useful to ensure that stories use the correct properties in the metadata.

Additionally, `satisfies` is preferred over type annotations (`const meta: Meta = {...}`) and type assertions (`const meta = {...} as Meta`). This is because other types like `StoryObj` will check to see which properties are defined in meta and use it for increased type safety. Using type annotations or assertions hides this information from the type-checker, so satisfies should be used instead.

Examples of **incorrect** code for this rule:

```ts
export default {
title: 'Button',
args: { primary: true },
component: Button,
}
```

```ts
const meta: Meta<typeof Button> = {
title: 'Button',
args: { primary: true },
component: Button,
}
export default meta
```

Examples of **correct** code for this rule:

```ts
export default {
title: 'Button',
args: { primary: true },
component: Button,
} satisfies Meta<typeof Button>
```

```ts
const meta = {
title: 'Button',
args: { primary: true },
component: Button,
} satisfies Meta<typeof Button>
export default meta
```

## When Not To Use It

If you aren't using TypeScript or you're using a version older than TypeScript 4.9, `satisfies` is not supported and you can avoid this rule.

## Further Reading

- [Improved type safety in Storybook 7](https://storybook.js.org/blog/improved-type-safety-in-storybook-7/?ref=storybookblog.ghost.io)
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import csfComponent from './rules/csf-component'
import defaultExports from './rules/default-exports'
import hierarchySeparator from './rules/hierarchy-separator'
import metaInlineProperties from './rules/meta-inline-properties'
import metaSatisfiesType from './rules/meta-satisfies-type'
import noRedundantStoryName from './rules/no-redundant-story-name'
import noStoriesOf from './rules/no-stories-of'
import noTitlePropertyInMeta from './rules/no-title-property-in-meta'
@@ -51,6 +52,7 @@ export = {
'default-exports': defaultExports,
'hierarchy-separator': hierarchySeparator,
'meta-inline-properties': metaInlineProperties,
'meta-satisfies-type': metaSatisfiesType,
'no-redundant-story-name': noRedundantStoryName,
'no-stories-of': noStoriesOf,
'no-title-property-in-meta': noTitlePropertyInMeta,
115 changes: 115 additions & 0 deletions lib/rules/meta-satisfies-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @fileoverview Meta should be followed by `satisfies Meta`
* @author Tiger Oakes
*/

import { AST_NODE_TYPES, ASTUtils, TSESTree, TSESLint } from '@typescript-eslint/utils'
import { getMetaObjectExpression } from '../utils'
import { createStorybookRule } from '../utils/create-storybook-rule'
import { isTSSatisfiesExpression } from '../utils/ast'

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

export = createStorybookRule({
name: 'meta-satisfies-type',
defaultOptions: [],
meta: {
type: 'problem',
fixable: 'code',
severity: 'error',
docs: {
description: 'Meta should use `satisfies Meta`',
categories: [],
excludeFromConfig: true,
},
messages: {
metaShouldSatisfyType: 'CSF Meta should use `satisfies` for type safety',
},
schema: [],
},

create(context) {
// variables should be defined here
const sourceCode = context.getSourceCode()

//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
const getTextWithParentheses = (node: TSESTree.Node): string => {
// Capture parentheses before and after the node
let beforeCount = 0
let afterCount = 0

if (ASTUtils.isParenthesized(node, sourceCode)) {
const bodyOpeningParen = sourceCode.getTokenBefore(node, ASTUtils.isOpeningParenToken)
const bodyClosingParen = sourceCode.getTokenAfter(node, ASTUtils.isClosingParenToken)

if (bodyOpeningParen && bodyClosingParen) {
beforeCount = node.range[0] - bodyOpeningParen.range[0]
afterCount = bodyClosingParen.range[1] - node.range[1]
}
}

return sourceCode.getText(node, beforeCount, afterCount)
}

const getFixer = (meta: TSESTree.ObjectExpression): TSESLint.ReportFixFunction | undefined => {
const { parent } = meta
if (!parent) {
return undefined
}

switch (parent.type) {
// {} as Meta
case AST_NODE_TYPES.TSAsExpression:
return (fixer) => [
fixer.replaceText(parent, getTextWithParentheses(meta)),
fixer.insertTextAfter(
parent,
` satisfies ${getTextWithParentheses(parent.typeAnnotation)}`
),
]
// const meta: Meta = {}
case AST_NODE_TYPES.VariableDeclarator: {
const { typeAnnotation } = parent.id
if (typeAnnotation) {
return (fixer) => [
fixer.remove(typeAnnotation),
fixer.insertTextAfter(
meta,
` satisfies ${getTextWithParentheses(typeAnnotation.typeAnnotation)}`
),
]
}
return undefined
}
default:
return undefined
}
}
// any helper functions should go here or else delete this section

//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------

return {
ExportDefaultDeclaration(node) {
const meta = getMetaObjectExpression(node, context)
if (!meta) {
return null
}

if (!meta.parent || !isTSSatisfiesExpression(meta.parent)) {
context.report({
node: meta,
messageId: 'metaShouldSatisfyType',
fix: getFixer(meta),
})
}
},
}
},
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-storybook",
"version": "0.11.6",
"version": "0.12.0",
"description": "Best practice rules for Storybook",
"keywords": [
"eslint",
72 changes: 72 additions & 0 deletions tests/lib/rules/meta-satisfies-type.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @fileoverview Meta should use `satisfies Meta`
* @author Tiger Oakes
*/

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

import rule from '../../../lib/rules/meta-satisfies-type'
import ruleTester from '../../utils/rule-tester'

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

ruleTester.run('meta-satisfies-type', rule, {
valid: [
"export default { title: 'Button', args: { primary: true } } satisfies Meta<typeof Button>",
`const meta = {
component: AccountForm,
} satisfies Meta<typeof AccountForm>;
export default meta;`,
],

invalid: [
{
code: `export default { title: 'Button', args: { primary: true } }`,
errors: [{ messageId: 'metaShouldSatisfyType' }],
},
{
code: `
const meta = {
component: AccountForm,
}
export default meta;
`,
errors: [{ messageId: 'metaShouldSatisfyType' }],
},
{
code: `
const meta: Meta<typeof AccountForm> = {
component: AccountForm,
}
export default meta;`,
output: `
const meta = {
component: AccountForm,
} satisfies Meta<typeof AccountForm>
export default meta;`,
errors: [{ messageId: 'metaShouldSatisfyType' }],
},
{
code: `export default { title: 'Button', args: { primary: true } } as Meta<typeof Button>`,
output: `export default { title: 'Button', args: { primary: true } } satisfies Meta<typeof Button>`,
errors: [{ messageId: 'metaShouldSatisfyType' }],
},
{
code: `
const meta = ( {
component: AccountForm,
}) as (Meta<typeof AccountForm> )
export default ( meta );`,
output: `
const meta = ( {
component: AccountForm,
}) satisfies (Meta<typeof AccountForm> )
export default ( meta );`,
errors: [{ messageId: 'metaShouldSatisfyType' }],
},
],
})
6 changes: 1 addition & 5 deletions tools/utils/categories.ts
Original file line number Diff line number Diff line change
@@ -25,12 +25,8 @@ for (const categoryId of categoryIds) {

for (const rule of rules) {
const ruleCategories = rule.meta.docs?.categories
// Throw if rule does not have a category
if (!ruleCategories?.length) {
throw new Error(`Rule "${rule.ruleId}" does not have any category.`)
}

if (ruleCategories.includes(categoryId) && rule.meta.docs?.excludeFromConfig !== true) {
if (ruleCategories?.includes(categoryId) && rule.meta.docs?.excludeFromConfig !== true) {
categoriesConfig[categoryId].rules?.push(rule)
}
}