From aefc977f286050246b99a2d0ab4d2a21f75c69d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Sch=C3=B6lander?= Date: Wed, 23 Oct 2024 09:52:32 +0200 Subject: [PATCH] Added no-exported-types-in-tsx-files --- .prettierrc | 6 +++ README.md | 33 ++++++++++++ package.json | 4 ++ src/rules/index.ts | 2 + src/rules/noExportedTypesInTsxFiles.ts | 40 +++++++++++++++ src/tests/noExportedTypesInTsxFiles.ts | 70 ++++++++++++++++++++++++++ yarn.lock | 17 +++++++ 7 files changed, 172 insertions(+) create mode 100644 .prettierrc create mode 100644 src/rules/noExportedTypesInTsxFiles.ts create mode 100644 src/tests/noExportedTypesInTsxFiles.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4632906 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "tabWidth": 4, + "useTabs": false, + "trailingComma": "es5", + "arrowParens": "always" +} diff --git a/README.md b/README.md index 0bf2109..ecae151 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Custom ESLint rules used internally at Meitner - [no-use-prefix-for-non-hook](#no-use-prefix-for-non-hook) - [no-react-namespace](#no-react-namespace) - [no-literal-jsx-style-prop-values](#no-literal-jsx-style-prop-values) +- [no-exported-types-outside-types-file](#no-exported-types-outside-types-file) ### no-inline-function-parameter-type-annotation @@ -178,3 +179,35 @@ Examples of invalid code ```ts ``` + +### no-exported-types-in-tsx-files + +Exporting your types from your component's tsx file can lead to dependency loops and make your code harder to maintain. + +This rule forbids exporting types from tsx files. + +Examples of valid code + +```ts +// MyComponent.tsx +import { Props } from "./MyComponent.types"; + +export default function MyComponent(props: Props) { + return
{props.children}
; +} +``` + +Examples of invalid code + +```ts +// MyComponent.tsx +import { Props } from "./MyComponent.types"; + +export default function MyComponent(props: Props) { // error + return
{props.children}
; +} + +export type Props = { + children: ReactNode; +}; +``` diff --git a/package.json b/package.json index 2a070c4..d27285b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@types/eslint": "^8.56.7", + "@types/node": "^22.7.9", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "@typescript-eslint/rule-tester": "7.5.0", @@ -30,5 +31,8 @@ }, "peerDependencies": { "eslint": "8.56.0" + }, + "dependencies": { + "prettier": "^3.3.3" } } diff --git a/src/rules/index.ts b/src/rules/index.ts index 23d4a0d..e382c34 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -1,4 +1,5 @@ import { alwaysSpreadJSXPropsFirst } from "./alwaysSpreadJSXPropsFirst"; +import { noExportedTypesInTsxFiles } from "./noExportedTypesInTsxFiles"; import { noInlineFunctionParameterTypeAnnotation } from "./noInlineFunctionParameterTypeAnnotation"; import { noLiteralJSXStylePropValues } from "./noLiteralJSXStylePropValues"; import { noMixedExports } from "./noMixedExports"; @@ -13,6 +14,7 @@ const rules = { "no-react-namespace": noReactNamespace, "no-literal-jsx-style-prop-values": noLiteralJSXStylePropValues, "always-spread-props-first": alwaysSpreadJSXPropsFirst, + "no-exported-types-in-tsx-files": noExportedTypesInTsxFiles, }; export { rules }; diff --git a/src/rules/noExportedTypesInTsxFiles.ts b/src/rules/noExportedTypesInTsxFiles.ts new file mode 100644 index 0000000..e26d527 --- /dev/null +++ b/src/rules/noExportedTypesInTsxFiles.ts @@ -0,0 +1,40 @@ +import { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; + +export const noExportedTypesInTsxFiles = ESLintUtils.RuleCreator.withoutDocs({ + create(context) { + const filename = context.filename; + const isTsxFile = filename.endsWith(".tsx"); + + return { + TSInterfaceDeclaration(node: TSESTree.TSInterfaceDeclaration) { + checkExportedType(node); + }, + TSTypeAliasDeclaration(node: TSESTree.TSTypeAliasDeclaration) { + checkExportedType(node); + }, + TSEnumDeclaration(node: TSESTree.TSEnumDeclaration) { + checkExportedType(node); + }, + }; + + function checkExportedType(node: TSESTree.Node) { + const isExported = node.parent?.type === "ExportNamedDeclaration"; + + if (isExported && isTsxFile) { + context.report({ + node, + messageId: "noExportedTypesInTsxFiles", + }); + } + } + }, + meta: { + type: "problem", + messages: { + noExportedTypesInTsxFiles: + "Exported types are not allowed in '.tsx' files.", + }, + schema: [], + }, + defaultOptions: [], +}); diff --git a/src/tests/noExportedTypesInTsxFiles.ts b/src/tests/noExportedTypesInTsxFiles.ts new file mode 100644 index 0000000..d46da7d --- /dev/null +++ b/src/tests/noExportedTypesInTsxFiles.ts @@ -0,0 +1,70 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; +import * as vitest from "vitest"; +import { noExportedTypesInTsxFiles } from "../rules/noExportedTypesInTsxFiles"; + +RuleTester.afterAll = vitest.afterAll; +RuleTester.it = vitest.it; +RuleTester.itOnly = vitest.it.only; +RuleTester.describe = vitest.describe; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", +}); + +ruleTester.run("noExportedTypesInTsxFiles", noExportedTypesInTsxFiles, { + valid: [ + { + code: ` + export interface MyInterface { + prop: string; + } + `, + filename: "types.ts", + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + }, + { + code: ` + interface MyInterface { + prop: string; + } + `, + filename: "component.tsx", + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + }, + { + code: ` + export type MyType = string; + `, + filename: "someFile.ts", + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + }, + ], + invalid: [ + { + code: ` + export interface MyInterface { + prop: string; + } + `, + filename: "component.tsx", + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + errors: [ + { + messageId: "noExportedTypesInTsxFiles", + }, + ], + }, + { + code: ` + export type MyType = string; + `, + filename: "component.tsx", + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + errors: [ + { + messageId: "noExportedTypesInTsxFiles", + }, + ], + }, + ], +}); diff --git a/yarn.lock b/yarn.lock index 713923f..e04d2df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -304,6 +304,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/node@^22.7.9": + version "22.7.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.9.tgz#2bf2797b5e84702d8262ea2cf843c3c3c880d0e9" + integrity sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg== + dependencies: + undici-types "~6.19.2" + "@types/semver@^7.5.0", "@types/semver@^7.5.8": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" @@ -1333,6 +1340,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== + pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" @@ -1544,6 +1556,11 @@ ufo@^1.3.2: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"