From c556bda0ecbae89388821185ab86d340c553bc1e Mon Sep 17 00:00:00 2001 From: Valera Melnikov Date: Wed, 18 Dec 2024 18:58:43 +0300 Subject: [PATCH] chore: add custom consistent-storybook-title rule --- .../src/NumberInput/NumberInput.stories.tsx | 2 +- .../src/SearchInput/SearchInput.stories.tsx | 2 +- .../Templates/IDEHeader/IDEHeader.stories.tsx | 2 +- .../ComboBox/stories/ComboBox.stories.tsx | 2 +- .../Datepicker/stories/Datepicker.stories.tsx | 2 +- .../IconButton/stories/IconButton.stories.tsx | 2 +- .../stories/InlineButtons.stories.tsx | 2 +- .../RadioGroup.chromatic.stories.tsx | 2 +- .../RadioGroup/stories/RadioGroup.stories.tsx | 2 +- .../TagGroup/stories/TagGroup.stories.tsx | 2 +- .../TextArea/stories/TextArea.stories.tsx | 2 +- .../TextField/stories/TextField.stories.tsx | 2 +- .../TimeField/stories/TimeField.stories.tsx | 2 +- .../stories/ToggleGroup.stories.tsx | 2 +- .../stories/ToolbarButtons.stories.tsx | 2 +- .../widgets/src/testing/ColorGrid.stories.tsx | 2 +- .../consistent-storybook-title/rule.test.ts | 103 +++++++++++++++ .../src/consistent-storybook-title/rule.ts | 117 ++++++++++++++++++ .../packages/eslint-plugin/src/index.ts | 3 + 19 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts create mode 100644 app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts diff --git a/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx b/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx index 543d7531c0c3..350fb658ed7c 100644 --- a/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx +++ b/app/client/packages/design-system/ads/src/NumberInput/NumberInput.stories.tsx @@ -4,7 +4,7 @@ import type { NumberInputProps } from "./NumberInput.types"; import type { StoryObj } from "@storybook/react"; export default { - title: "ADS/Components/Input/NumberInput", + title: "ADS/Components/Input/Number Input", component: NumberInput, decorators: [ (Story: () => React.ReactNode) => ( diff --git a/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx b/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx index b6399f4ceb84..306e0e64a007 100644 --- a/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx +++ b/app/client/packages/design-system/ads/src/SearchInput/SearchInput.stories.tsx @@ -4,7 +4,7 @@ import type { SearchInputProps } from "./SearchInput.types"; import type { StoryObj } from "@storybook/react"; export default { - title: "ADS/Components/Input/SearchInput", + title: "ADS/Components/Input/Search Input", component: SearchInput, decorators: [ (Story: () => React.ReactNode) => ( diff --git a/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx b/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx index 6c709080dd52..c24b5e8eafda 100644 --- a/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx +++ b/app/client/packages/design-system/ads/src/Templates/IDEHeader/IDEHeader.stories.tsx @@ -12,7 +12,7 @@ import { Text } from "../../Text"; import { ListHeaderContainer } from "../EntityExplorer/styles"; const meta: Meta = { - title: "ADS/Templates/IDEHeader", + title: "ADS/Templates/IDE Header", component: IDEHeader, parameters: { layout: "fullscreen", diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx index 5f01ae804ca7..36a64aa1c2d8 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx @@ -6,7 +6,7 @@ import { ComboBox, ListBoxItem, Flex, Button } from "@appsmith/wds"; import { items } from "./items"; const meta: Meta = { - title: "WDS/Widgets/ComboBox", + title: "WDS/Widgets/Combo Box", component: ComboBox, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx b/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx index bfb042895983..37137ed02297 100644 --- a/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/Datepicker/stories/Datepicker.stories.tsx @@ -10,7 +10,7 @@ import { DatePicker } from "../src"; */ const meta: Meta = { component: DatePicker, - title: "WDS/Widgets/DatePicker", + title: "WDS/Widgets/Date Picker", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx b/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx index 96fdcf0343dc..a5241ff2ca7f 100644 --- a/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/IconButton/stories/IconButton.stories.tsx @@ -14,7 +14,7 @@ import { objectKeys } from "@appsmith/utils"; */ const meta: Meta = { component: IconButton, - title: "WDS/Widgets/IconButton", + title: "WDS/Widgets/Icon Button", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx index 44a274739d78..c30b43e51a2a 100644 --- a/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx @@ -21,7 +21,7 @@ import { */ const meta: Meta = { component: InlineButtons, - title: "WDS/Widgets/InlineButtons", + title: "WDS/Widgets/Inline Buttons", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx index 2225cc3f1e01..86e63c2f8c87 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx @@ -6,7 +6,7 @@ import { StoryGrid, DataAttrWrapper } from "@design-system/storybook"; const meta: Meta = { component: RadioGroup, - title: "Design System/Widgets/RadioGroup", + title: "Design System/Widgets/Radio Group", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx index e16fdfbc1874..9ed7e7417f02 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx @@ -8,7 +8,7 @@ const items = [ ]; const meta: Meta = { - title: "WDS/Widgets/RadioGroup", + title: "WDS/Widgets/Radio Group", component: RadioGroup, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx index 2325e2b81e27..0fa6d3d77803 100644 --- a/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TagGroup/stories/TagGroup.stories.tsx @@ -8,7 +8,7 @@ import type { TagGroupProps } from "../src/TagGroup"; */ const meta: Meta = { component: TagGroup, - title: "WDS/Widgets/TagGroup", + title: "WDS/Widgets/Tag Group", subcomponents: { Tag, }, diff --git a/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx b/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx index b2b66dc45ae1..7cbd9126b494 100644 --- a/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TextArea/stories/TextArea.stories.tsx @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Flex, TextArea, Button } from "@appsmith/wds"; const meta: Meta = { - title: "WDS/Widgets/TextArea", + title: "WDS/Widgets/Text Area", component: TextArea, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx b/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx index 8e44d7ce549c..ed1a6f6c5683 100644 --- a/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TextField/stories/TextField.stories.tsx @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Flex, Icon, TextField, Button } from "@appsmith/wds"; const meta: Meta = { - title: "WDS/Widgets/TextField", + title: "WDS/Widgets/Text Field", component: TextField, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx b/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx index a498cd5826fd..3c1b13b70b16 100644 --- a/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/TimeField/stories/TimeField.stories.tsx @@ -4,7 +4,7 @@ import { TimeField } from "../src"; import { Time } from "@internationalized/date"; const meta: Meta = { - title: "WDS/Widgets/TimeField", + title: "WDS/Widgets/Time Field", component: TimeField, parameters: { docs: { diff --git a/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx index 5db40b210272..995ed35bd5a4 100644 --- a/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ToggleGroup/stories/ToggleGroup.stories.tsx @@ -8,7 +8,7 @@ const items = [ ]; const meta: Meta = { - title: "WDS/Widgets/ToggleGroup", + title: "WDS/Widgets/Toggle Group", component: ToggleGroup, tags: ["autodocs"], args: { diff --git a/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx b/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx index 917338e6b3cb..2fdeaa192ba7 100644 --- a/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ToolbarButtons/stories/ToolbarButtons.stories.tsx @@ -21,7 +21,7 @@ import { */ const meta: Meta = { component: ToolbarButtons, - title: "WDS/Widgets/ToolbarButtons", + title: "WDS/Widgets/Toolbar Buttons", }; export default meta; diff --git a/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx b/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx index d67a774feb21..977f95427f84 100644 --- a/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx +++ b/app/client/packages/design-system/widgets/src/testing/ColorGrid.stories.tsx @@ -4,7 +4,7 @@ import { ColorGrid } from "./ColorGrid"; const meta: Meta = { component: ColorGrid, - title: "WDS/Testing/ColorGrid", + title: "WDS/Testing/Color Grid", args: { source: "oklch", size: "small", diff --git a/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts new file mode 100644 index 000000000000..b55b7948a244 --- /dev/null +++ b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.test.ts @@ -0,0 +1,103 @@ +import { TSESLint } from "@typescript-eslint/utils"; +import { consistentStorybookTitle } from "./rule"; + +const ruleTester = new TSESLint.RuleTester({ + parser: require.resolve("@typescript-eslint/parser"), + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}); + +ruleTester.run("storybook-title-case", consistentStorybookTitle, { + valid: [ + { + code: `export default { title: "ADS/Templates/IDE Header" };`, + filename: "example.stories.tsx", + }, + { + code: `export default { title: "ADS/Components/Input/IDE Search Input" };`, + filename: "example.stories.tsx", + }, + { + code: `export default { title: "ADS/Components/Input/AAA Number Input" };`, + filename: "example.stories.tsx", + }, + { + code: `const meta = { title: "WDS/Widgets/Button" };`, + filename: "example.stories.tsx", + }, + ], + invalid: [ + { + code: `export default { title: "ADS/Templates/IDEHeader" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "ADS/Templates/IDEHeader", + suggestedTitle: "ADS/Templates/IDE Header", + }, + }, + ], + output: `export default { title: "ADS/Templates/IDE Header" };`, + }, + { + code: `export default { title: "ADS/Components/Input/IDESearch Input" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "ADS/Components/Input/IDESearch Input", + suggestedTitle: "ADS/Components/Input/IDE Search Input", + }, + }, + ], + output: `export default { title: "ADS/Components/Input/IDE Search Input" };`, + }, + { + code: `export default { title: "ADS/Components/Input/IDESearchInput" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "ADS/Components/Input/IDESearchInput", + suggestedTitle: "ADS/Components/Input/IDE Search Input", + }, + }, + ], + output: `export default { title: "ADS/Components/Input/IDE Search Input" };`, + }, + { + code: `export default { title: "ADS/Components/Input/AAANumber Input" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "ADS/Components/Input/AAANumber Input", + suggestedTitle: "ADS/Components/Input/AAA Number Input", + }, + }, + ], + output: `export default { title: "ADS/Components/Input/AAA Number Input" };`, + }, + { + code: `export default { title: "WDS/Widgets/button" };`, + filename: "example.stories.tsx", + errors: [ + { + messageId: "invalidTitle", + data: { + title: "WDS/Widgets/button", + suggestedTitle: "WDS/Widgets/Button", + }, + }, + ], + output: `export default { title: "WDS/Widgets/Button" };`, + }, + ], +}); diff --git a/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts new file mode 100644 index 000000000000..aed34b0eb2f2 --- /dev/null +++ b/app/client/packages/eslint-plugin/src/consistent-storybook-title/rule.ts @@ -0,0 +1,117 @@ +import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; + +const titleCase = (str: string): string => { + return str + .split("/") + .map( + (segment) => + segment + .replace(/([A-Z]+)([A-Z][a-z0-9])/g, "$1 $2") // Acronyms followed by words + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") // Lowercase followed by uppercase + .replace(/\b\w/g, (char) => char.toUpperCase()), // Capitalize first letter + ) + .join("/"); +}; + +export const consistentStorybookTitle: TSESLint.RuleModule<"invalidTitle", []> = + { + defaultOptions: [], + meta: { + type: "problem", + docs: { + description: + "Ensure Storybook titles in `export default` or meta objects are in Title Case", + recommended: "error", + }, + messages: { + invalidTitle: + 'The Storybook title "{{ title }}" is not in Title Case. Suggested: "{{ suggestedTitle }}".', + }, + schema: [], // No options + fixable: "code", // Allows auto-fixing + }, + + create(context) { + const filename = context.getFilename(); + const isStoryFile = filename.endsWith(".stories.tsx"); // Apply only to *.stories.tsx files + + if (!isStoryFile) { + return {}; // No-op for non-story files + } + + const validateTitle = (title: string, node: TSESTree.Node) => { + const expectedTitle = titleCase(title); + + if (title !== expectedTitle) { + context.report({ + node, + messageId: "invalidTitle", + data: { + title, + suggestedTitle: expectedTitle, + }, + fix: (fixer) => fixer.replaceText(node, `"${expectedTitle}"`), // Use double quotes + }); + } + }; + + return { + ExportDefaultDeclaration(node: TSESTree.ExportDefaultDeclaration) { + if ( + node.declaration.type === "ObjectExpression" && + node.declaration.properties.some( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "title" && + prop.value.type === "Literal" && + typeof prop.value.value === "string", + ) + ) { + const titleProperty = node.declaration.properties.find( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "title", + ) as TSESTree.Property; + + const titleValue = titleProperty.value as TSESTree.Literal; + + if (typeof titleValue.value === "string") { + validateTitle(titleValue.value, titleValue); + } + } + }, + + VariableDeclaration(node: TSESTree.VariableDeclaration) { + node.declarations.forEach((declaration) => { + if ( + declaration.init && + declaration.init.type === "ObjectExpression" && + declaration.init.properties.some( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "title" && + prop.value.type === "Literal" && + typeof prop.value.value === "string", + ) + ) { + const titleProperty = declaration.init.properties.find( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "title", + ) as TSESTree.Property; + + const titleValue = titleProperty.value as TSESTree.Literal; + + if (typeof titleValue.value === "string") { + validateTitle(titleValue.value, titleValue); + } + } + }); + }, + }; + }, + }; diff --git a/app/client/packages/eslint-plugin/src/index.ts b/app/client/packages/eslint-plugin/src/index.ts index eda6deb6d349..49fb2417f454 100644 --- a/app/client/packages/eslint-plugin/src/index.ts +++ b/app/client/packages/eslint-plugin/src/index.ts @@ -1,16 +1,19 @@ import { objectKeysRule } from "./object-keys/rule"; import { namedUseEffectRule } from "./named-use-effect/rule"; +import { consistentStorybookTitle } from "./consistent-storybook-title/rule"; const plugin = { rules: { "object-keys": objectKeysRule, "named-use-effect": namedUseEffectRule, + "consistent-storybook-title": consistentStorybookTitle, }, configs: { recommended: { rules: { "@appsmith/object-keys": "warn", "@appsmith/named-use-effect": "warn", + "@appsmith/consistent-storybook-title": "error", }, }, },