diff --git a/docs/docs/developer/frontend/style-guide.mdx b/docs/docs/developer/frontend/style-guide.mdx index 6e85aefb9..dfd47a65d 100644 --- a/docs/docs/developer/frontend/style-guide.mdx +++ b/docs/docs/developer/frontend/style-guide.mdx @@ -85,6 +85,32 @@ const EmailField = ({ value }: OwnProps) => ( ); ``` +#### No Single Variable Prop Spreading in JSX Elements + +We discourage the use of single variable prop spreading in JSX elements, e.g., `{...props}`. This practice often leads to less readable and maintainable code as it's unclear what props are being passed down to the component. + +```tsx +/* ❌ - Bad, spreads a single variable prop into the underlying component + */ +const MyComponent = (props: OwnProps) => { + return ; +} +``` + +```tsx +/* ✅ - Good, Explicitly lists all props + * - Enhances readability and maintainability + */ +const MyComponent = ({ prop1, prop2, prop3 }: OwnProps) => { + return ; +}; +``` + +Rationale: +- It's clearer to see at a glance which props are being passed down, making the code easier to understand and maintain. +- It helps to prevent tight coupling between components via their props. +- It's easier to catch misspelled or unused props with linting tools when props are listed explicitly + ## JavaScript ### Use nullish-coalescing operator `??` diff --git a/front/.eslintrc.js b/front/.eslintrc.js index dc688765d..c0c06a4d5 100644 --- a/front/.eslintrc.js +++ b/front/.eslintrc.js @@ -62,6 +62,7 @@ module.exports = { 'simple-import-sort/exports': 'error', 'twenty/effect-components': 'error', 'twenty/no-hardcoded-colors': 'error', + 'twenty/no-spread-props': 'error', 'twenty/matching-state-variable': 'error', 'twenty/sort-css-properties-alphabetically': 'error', 'twenty/styled-components-prefixed-with-styled': 'error', diff --git a/front/src/modules/activities/table/components/CellCommentChip.tsx b/front/src/modules/activities/table/components/CellCommentChip.tsx index 17d2aba11..4027975c4 100644 --- a/front/src/modules/activities/table/components/CellCommentChip.tsx +++ b/front/src/modules/activities/table/components/CellCommentChip.tsx @@ -10,6 +10,7 @@ export const CellCommentChip = (props: CommentChipProps) => { return ( + {/* eslint-disable-next-line twenty/no-spread-props */} ); diff --git a/front/src/modules/auth/components/Logo.tsx b/front/src/modules/auth/components/Logo.tsx index 0c561e90c..003559a9d 100644 --- a/front/src/modules/auth/components/Logo.tsx +++ b/front/src/modules/auth/components/Logo.tsx @@ -50,6 +50,7 @@ const StyledMainLogo = styled.div` export const Logo = ({ workspaceLogo, ...props }: Props) => { if (!workspaceLogo) { return ( + // eslint-disable-next-line twenty/no-spread-props @@ -57,6 +58,7 @@ export const Logo = ({ workspaceLogo, ...props }: Props) => { } return ( + // eslint-disable-next-line twenty/no-spread-props diff --git a/front/src/modules/auth/components/Modal.tsx b/front/src/modules/auth/components/Modal.tsx index 3c7aad04e..765b04aa7 100644 --- a/front/src/modules/auth/components/Modal.tsx +++ b/front/src/modules/auth/components/Modal.tsx @@ -11,6 +11,7 @@ const StyledContent = styled(UIModal.Content)` type Props = React.ComponentProps<'div'>; export const AuthModal = ({ children, ...restProps }: Props) => ( + // eslint-disable-next-line twenty/no-spread-props {children} diff --git a/front/src/modules/auth/sign-in-up/components/FooterNote.tsx b/front/src/modules/auth/sign-in-up/components/FooterNote.tsx index 8d6e6f052..cd41568fb 100644 --- a/front/src/modules/auth/sign-in-up/components/FooterNote.tsx +++ b/front/src/modules/auth/sign-in-up/components/FooterNote.tsx @@ -11,4 +11,5 @@ const StyledContainer = styled.div` text-align: center; `; +// eslint-disable-next-line twenty/no-spread-props export const FooterNote = (props: Props) => ; diff --git a/front/src/modules/spreadsheet-import/components/Heading.tsx b/front/src/modules/spreadsheet-import/components/Heading.tsx index 04c6873da..d35a7e620 100644 --- a/front/src/modules/spreadsheet-import/components/Heading.tsx +++ b/front/src/modules/spreadsheet-import/components/Heading.tsx @@ -28,6 +28,7 @@ const StyledDescription = styled.span` `; export const Heading = ({ title, description, ...props }: Props) => ( + // eslint-disable-next-line twenty/no-spread-props {title} {description && {description}} diff --git a/front/src/modules/spreadsheet-import/components/Table.tsx b/front/src/modules/spreadsheet-import/components/Table.tsx index 9f840f507..8971191df 100644 --- a/front/src/modules/spreadsheet-import/components/Table.tsx +++ b/front/src/modules/spreadsheet-import/components/Table.tsx @@ -115,6 +115,7 @@ export const Table = (props: Props) => { const { rtl } = useSpreadsheetImportInternal(); return ( + // eslint-disable-next-line twenty/no-spread-props ); }; diff --git a/front/src/modules/ui/button/components/MainButton.tsx b/front/src/modules/ui/button/components/MainButton.tsx index 81740185b..34726ede6 100644 --- a/front/src/modules/ui/button/components/MainButton.tsx +++ b/front/src/modules/ui/button/components/MainButton.tsx @@ -107,6 +107,7 @@ export const MainButton = ({ }: MainButtonProps) => { const theme = useTheme(); return ( + // eslint-disable-next-line twenty/no-spread-props {Icon && } {title} diff --git a/front/src/modules/ui/button/components/RoundedIconButton.tsx b/front/src/modules/ui/button/components/RoundedIconButton.tsx index bf8092581..3c7ccccba 100644 --- a/front/src/modules/ui/button/components/RoundedIconButton.tsx +++ b/front/src/modules/ui/button/components/RoundedIconButton.tsx @@ -40,6 +40,7 @@ export const RoundedIconButton = ({ const theme = useTheme(); return ( + // eslint-disable-next-line twenty/no-spread-props {} diff --git a/front/src/modules/ui/checkmark/components/AnimatedCheckmark.tsx b/front/src/modules/ui/checkmark/components/AnimatedCheckmark.tsx index 6732fce5b..8261c9e10 100644 --- a/front/src/modules/ui/checkmark/components/AnimatedCheckmark.tsx +++ b/front/src/modules/ui/checkmark/components/AnimatedCheckmark.tsx @@ -24,6 +24,7 @@ export const AnimatedCheckmark = ({ height={size} > { const theme = useTheme(); return ( + // eslint-disable-next-line twenty/no-spread-props diff --git a/front/src/modules/ui/color-scheme/components/ColorSchemeCard.tsx b/front/src/modules/ui/color-scheme/components/ColorSchemeCard.tsx index f9f265b9a..9166a78d1 100644 --- a/front/src/modules/ui/color-scheme/components/ColorSchemeCard.tsx +++ b/front/src/modules/ui/color-scheme/components/ColorSchemeCard.tsx @@ -105,6 +105,7 @@ const ColorSchemeSegment = ({ controls, ...rest }: ColorSchemeSegmentProps) => ( + // eslint-disable-next-line twenty/no-spread-props Aa @@ -173,6 +174,7 @@ export const ColorSchemeCard = ({ diff --git a/front/src/modules/ui/dialog/components/Dialog.tsx b/front/src/modules/ui/dialog/components/Dialog.tsx index 2c5dc0c7b..2986917fd 100644 --- a/front/src/modules/ui/dialog/components/Dialog.tsx +++ b/front/src/modules/ui/dialog/components/Dialog.tsx @@ -137,6 +137,7 @@ export const Dialog = ({ {title && {title}} @@ -151,6 +152,7 @@ export const Dialog = ({ }} fullWidth={true} variant={button.variant ?? 'secondary'} + // eslint-disable-next-line twenty/no-spread-props {...button} /> ))} diff --git a/front/src/modules/ui/dialog/components/DialogProvider.tsx b/front/src/modules/ui/dialog/components/DialogProvider.tsx index 7f0c5e7cf..fa90897ce 100644 --- a/front/src/modules/ui/dialog/components/DialogProvider.tsx +++ b/front/src/modules/ui/dialog/components/DialogProvider.tsx @@ -40,6 +40,7 @@ export const DialogProvider = ({ children }: React.PropsWithChildren) => { {dialogInternal.queue.map((dialog) => ( handleDialogClose(dialog.id)} /> diff --git a/front/src/modules/ui/dropdown/components/DropdownMenuContainer.tsx b/front/src/modules/ui/dropdown/components/DropdownMenuContainer.tsx index ea4b90efa..9acf24f1e 100644 --- a/front/src/modules/ui/dropdown/components/DropdownMenuContainer.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownMenuContainer.tsx @@ -39,6 +39,7 @@ export const DropdownMenuContainer = ({ }); return ( + // eslint-disable-next-line twenty/no-spread-props {children} diff --git a/front/src/modules/ui/dropdown/components/DropdownMenuHeader.tsx b/front/src/modules/ui/dropdown/components/DropdownMenuHeader.tsx index e43e426c4..9889a6982 100644 --- a/front/src/modules/ui/dropdown/components/DropdownMenuHeader.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownMenuHeader.tsx @@ -55,6 +55,7 @@ export const DropdownMenuHeader = ({ const theme = useTheme(); return ( + // eslint-disable-next-line twenty/no-spread-props {StartIcon && ( diff --git a/front/src/modules/ui/dropdown/components/DropdownMenuSearchInput.tsx b/front/src/modules/ui/dropdown/components/DropdownMenuSearchInput.tsx index 6e6b9abb3..8b0bee084 100644 --- a/front/src/modules/ui/dropdown/components/DropdownMenuSearchInput.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownMenuSearchInput.tsx @@ -38,6 +38,7 @@ export const DropdownMenuSearchInput = forwardRef< InputHTMLAttributes >((props, ref) => ( + {/* eslint-disable-next-line twenty/no-spread-props */} )); diff --git a/front/src/modules/ui/input/components/Radio.tsx b/front/src/modules/ui/input/components/Radio.tsx index a7c5b8afc..794ae739c 100644 --- a/front/src/modules/ui/input/components/Radio.tsx +++ b/front/src/modules/ui/input/components/Radio.tsx @@ -126,6 +126,7 @@ export const Radio = ({ }; return ( + // eslint-disable-next-line twenty/no-spread-props = { return ( diff --git a/front/src/modules/ui/input/text/components/TextInputSettings.tsx b/front/src/modules/ui/input/text/components/TextInputSettings.tsx index 37441a08e..35d2ad031 100644 --- a/front/src/modules/ui/input/text/components/TextInputSettings.tsx +++ b/front/src/modules/ui/input/text/components/TextInputSettings.tsx @@ -170,6 +170,7 @@ const TextInputComponent = ( onChange(event.target.value); } }} + // eslint-disable-next-line twenty/no-spread-props {...props} /> diff --git a/front/src/modules/ui/input/text/components/__stories__/TextInput.stories.tsx b/front/src/modules/ui/input/text/components/__stories__/TextInput.stories.tsx index a5eef269b..94ed10920 100644 --- a/front/src/modules/ui/input/text/components/__stories__/TextInput.stories.tsx +++ b/front/src/modules/ui/input/text/components/__stories__/TextInput.stories.tsx @@ -28,6 +28,7 @@ const FakeTextInput = ({ const [value, setValue] = useState(initialValue); return ( { @@ -41,6 +42,7 @@ const FakeTextInput = ({ export const Default: Story = { argTypes: { value: { control: false } }, args: { value: 'A good value ' }, + // eslint-disable-next-line twenty/no-spread-props render: (args) => , play: async ({ canvasElement }) => { const canvas = within(canvasElement); diff --git a/front/src/modules/ui/layout/components/PageHeader.tsx b/front/src/modules/ui/layout/components/PageHeader.tsx index 3bdac95aa..d7d768df0 100644 --- a/front/src/modules/ui/layout/components/PageHeader.tsx +++ b/front/src/modules/ui/layout/components/PageHeader.tsx @@ -92,6 +92,7 @@ export const PageHeader = ({ const theme = useTheme(); return ( + // eslint-disable-next-line twenty/no-spread-props {!isNavbarOpened && ( diff --git a/front/src/modules/ui/modal/components/Modal.tsx b/front/src/modules/ui/modal/components/Modal.tsx index cf3c1cb82..cc3ec3096 100644 --- a/front/src/modules/ui/modal/components/Modal.tsx +++ b/front/src/modules/ui/modal/components/Modal.tsx @@ -99,18 +99,21 @@ const StyledBackDrop = styled(motion.div)` type ModalHeaderProps = React.PropsWithChildren & React.ComponentProps<'div'>; const ModalHeader = ({ children, ...restProps }: ModalHeaderProps) => ( + // eslint-disable-next-line twenty/no-spread-props {children} ); type ModalContentProps = React.PropsWithChildren & React.ComponentProps<'div'>; const ModalContent = ({ children, ...restProps }: ModalContentProps) => ( + // eslint-disable-next-line twenty/no-spread-props {children} ); type ModalFooterProps = React.PropsWithChildren & React.ComponentProps<'div'>; const ModalFooter = ({ children, ...restProps }: ModalFooterProps) => ( + // eslint-disable-next-line twenty/no-spread-props {children} ); @@ -203,6 +206,7 @@ export const Modal = ({ exit="exit" layout variants={modalVariants} + // eslint-disable-next-line twenty/no-spread-props {...restProps} > {children} diff --git a/front/src/modules/ui/snack-bar/components/SnackBar.tsx b/front/src/modules/ui/snack-bar/components/SnackBar.tsx index fd7c07b54..9ad8c8246 100644 --- a/front/src/modules/ui/snack-bar/components/SnackBar.tsx +++ b/front/src/modules/ui/snack-bar/components/SnackBar.tsx @@ -160,6 +160,7 @@ export const SnackBar = ({ onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} variant={variant} + // eslint-disable-next-line twenty/no-spread-props {...rootProps} > diff --git a/front/src/modules/ui/snack-bar/components/SnackBarProvider.tsx b/front/src/modules/ui/snack-bar/components/SnackBarProvider.tsx index 68a6f4338..a73558ce4 100644 --- a/front/src/modules/ui/snack-bar/components/SnackBarProvider.tsx +++ b/front/src/modules/ui/snack-bar/components/SnackBarProvider.tsx @@ -80,6 +80,7 @@ export const SnackBarProvider = ({ children }: React.PropsWithChildren) => { layout > handleSnackBarClose(snackBar.id)} /> diff --git a/front/src/modules/ui/step-bar/components/StepBar.tsx b/front/src/modules/ui/step-bar/components/StepBar.tsx index f4b8f0048..6807982ce 100644 --- a/front/src/modules/ui/step-bar/components/StepBar.tsx +++ b/front/src/modules/ui/step-bar/components/StepBar.tsx @@ -25,6 +25,7 @@ export const StepBar = ({ children, activeStep, ...restProps }: StepsProps) => { const isMobile = useIsMobile(); return ( + // eslint-disable-next-line twenty/no-spread-props {React.Children.map(children, (child, index) => { if (!React.isValidElement(child)) { diff --git a/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx b/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx index 9d0a8fef0..b6c74ca64 100644 --- a/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx +++ b/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx @@ -51,6 +51,7 @@ export const EntityTableColumnMenu = ({ ); return ( + // eslint-disable-next-line twenty/no-spread-props {hiddenTableColumns.map((column) => ( diff --git a/front/src/modules/ui/tooltip/AppTooltip.tsx b/front/src/modules/ui/tooltip/AppTooltip.tsx index 0b4f35d8b..0e4fa5b6d 100644 --- a/front/src/modules/ui/tooltip/AppTooltip.tsx +++ b/front/src/modules/ui/tooltip/AppTooltip.tsx @@ -44,5 +44,6 @@ export type AppToolipProps = { }; export const AppTooltip = (props: AppToolipProps) => ( + // eslint-disable-next-line twenty/no-spread-props ); diff --git a/front/src/modules/ui/tooltip/__stories__/Tooltip.stories.tsx b/front/src/modules/ui/tooltip/__stories__/Tooltip.stories.tsx index 054d7d0e4..19a92b210 100644 --- a/front/src/modules/ui/tooltip/__stories__/Tooltip.stories.tsx +++ b/front/src/modules/ui/tooltip/__stories__/Tooltip.stories.tsx @@ -26,6 +26,7 @@ export const Default: Story = {

Hover me!

+ {/* eslint-disable-next-line twenty/no-spread-props */} ), diff --git a/front/src/modules/ui/utilities/animation/components/AnimatedEaseIn.tsx b/front/src/modules/ui/utilities/animation/components/AnimatedEaseIn.tsx index ced648b9f..a2ec18e17 100644 --- a/front/src/modules/ui/utilities/animation/components/AnimatedEaseIn.tsx +++ b/front/src/modules/ui/utilities/animation/components/AnimatedEaseIn.tsx @@ -21,6 +21,7 @@ export const AnimatedEaseIn = ({ initial={initial} animate={animate} transition={transition} + // eslint-disable-next-line twenty/no-spread-props {...restProps} > {children} diff --git a/front/src/modules/ui/utilities/animation/components/AnimatedTextWord.tsx b/front/src/modules/ui/utilities/animation/components/AnimatedTextWord.tsx index 39f364127..d67957ffd 100644 --- a/front/src/modules/ui/utilities/animation/components/AnimatedTextWord.tsx +++ b/front/src/modules/ui/utilities/animation/components/AnimatedTextWord.tsx @@ -58,6 +58,7 @@ export const AnimatedTextWord = ({ text = '', ...restProps }: Props) => { variants={containerAnimation} initial="hidden" animate="visible" + // eslint-disable-next-line twenty/no-spread-props {...restProps} > {words.map((word, index) => ( diff --git a/packages/eslint-plugin-twenty-ts/dist/index.js b/packages/eslint-plugin-twenty-ts/dist/index.js index 65510895f..ceb667f37 100644 --- a/packages/eslint-plugin-twenty-ts/dist/index.js +++ b/packages/eslint-plugin-twenty-ts/dist/index.js @@ -4,6 +4,7 @@ module.exports = { rules: { "effect-components": require("./src/rules/effect-components"), "no-hardcoded-colors": require("./src/rules/no-hardcoded-colors"), + "no-spread-props": require('./src/rules/no-spread-props'), "matching-state-variable": require("./src/rules/matching-state-variable"), "sort-css-properties-alphabetically": require("./src/rules/sort-css-properties-alphabetically"), "styled-components-prefixed-with-styled": require("./src/rules/styled-components-prefixed-with-styled"), diff --git a/packages/eslint-plugin-twenty-ts/dist/src/rules/no-spread-props.js b/packages/eslint-plugin-twenty-ts/dist/src/rules/no-spread-props.js new file mode 100644 index 000000000..facd53271 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/rules/no-spread-props.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("@typescript-eslint/utils"); +const createRule = utils_1.ESLintUtils.RuleCreator(() => "https://docs.twenty.com/developer/frontend/style-guide#no-single-variable-prop-spreading-in-jsx-elements"); +const noSpreadPropsRule = createRule({ + create: (context) => ({ + JSXSpreadAttribute: (node) => { + if (node.argument.type === "Identifier") { + context.report({ + node, + messageId: "noSpreadProps", + }); + } + }, + }), + name: "no-spread-props", + meta: { + docs: { + description: "Disallow passing props as {...props} in React components.", + }, + messages: { + noSpreadProps: `Single variable prop spreading is disallowed in JSX elements.\nPrefer explicitly listing out all props or using an object expression like so: \`{...{ prop1, prop2 }}\`.\nSee https://docs.twenty.com/developer/frontend/style-guide#no-single-variable-prop-spreading-in-jsx-elements for more information.`, + }, + type: "suggestion", + schema: [], + fixable: "code", + }, + defaultOptions: [], +}); +module.exports = noSpreadPropsRule; +exports.default = noSpreadPropsRule; diff --git a/packages/eslint-plugin-twenty-ts/dist/src/tests/no-spread-props.spec.js b/packages/eslint-plugin-twenty-ts/dist/src/tests/no-spread-props.spec.js new file mode 100644 index 000000000..a07a5014c --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/dist/src/tests/no-spread-props.spec.js @@ -0,0 +1,34 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const rule_tester_1 = require("@typescript-eslint/rule-tester"); +const no_spread_props_1 = __importDefault(require("../rules/no-spread-props")); +const ruleTester = new rule_tester_1.RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, + }); +ruleTester.run("no-spread-props", no_spread_props_1.default, { + valid: [ + { + code: "", + }, + ], + invalid: [ + { + code: "", + errors: [ + { + messageId: "noSpreadProps", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-twenty/index.ts b/packages/eslint-plugin-twenty/index.ts index 68b50740d..9e2f0b9c0 100644 --- a/packages/eslint-plugin-twenty/index.ts +++ b/packages/eslint-plugin-twenty/index.ts @@ -2,6 +2,7 @@ module.exports = { rules: { "effect-components": require("./src/rules/effect-components"), "no-hardcoded-colors": require("./src/rules/no-hardcoded-colors"), + "no-spread-props": require("./src/rules/no-spread-props"), "matching-state-variable": require("./src/rules/matching-state-variable"), "sort-css-properties-alphabetically": require("./src/rules/sort-css-properties-alphabetically"), "styled-components-prefixed-with-styled": require("./src/rules/styled-components-prefixed-with-styled"), diff --git a/packages/eslint-plugin-twenty/src/rules/no-spread-props.ts b/packages/eslint-plugin-twenty/src/rules/no-spread-props.ts new file mode 100644 index 000000000..5dae31b3b --- /dev/null +++ b/packages/eslint-plugin-twenty/src/rules/no-spread-props.ts @@ -0,0 +1,36 @@ +import { ESLintUtils } from "@typescript-eslint/utils"; + +const createRule = ESLintUtils.RuleCreator( + () => + "https://docs.twenty.com/developer/frontend/style-guide#no-single-variable-prop-spreading-in-jsx-elements", +); + +const noSpreadPropsRule = createRule({ + create: (context) => ({ + JSXSpreadAttribute: (node) => { + if (node.argument.type === "Identifier") { + context.report({ + node, + messageId: "noSpreadProps", + }); + } + }, + }), + name: "no-spread-props", + meta: { + docs: { + description: "Disallow passing props as {...props} in React components.", + }, + messages: { + noSpreadProps: `Single variable prop spreading is disallowed in JSX elements.\nPrefer explicitly listing out all props or using an object expression like so: \`{...{ prop1, prop2 }}\`.\nSee https://docs.twenty.com/developer/frontend/style-guide#no-single-variable-prop-spreading-in-jsx-elements for more information.`, + }, + type: "suggestion", + schema: [], + fixable: "code", + }, + defaultOptions: [], +}); + +module.exports = noSpreadPropsRule; + +export default noSpreadPropsRule; diff --git a/packages/eslint-plugin-twenty/src/tests/no-spread-props.spec.ts b/packages/eslint-plugin-twenty/src/tests/no-spread-props.spec.ts new file mode 100644 index 000000000..f20ff8f1e --- /dev/null +++ b/packages/eslint-plugin-twenty/src/tests/no-spread-props.spec.ts @@ -0,0 +1,35 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; + +import noSpreadPropsRule from "../rules/no-spread-props"; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run("no-spread-props", noSpreadPropsRule, { + valid: [ + { + code: "", + }, + { + code: "", + }, + ], + invalid: [ + { + code: "", + errors: [ + { + messageId: "noSpreadProps", + }, + ], + }, + ], +});