diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1 @@ +{} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cbd066c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,126 @@ +# `@remix-run/eslint-config` + +## 1.16.1 + +### Patch Changes + +- Don't require display name in root module ([#5450](https://github.com/remix-run/remix/pull/5450)) +- Update minimum version of Babel dependencies to avoid errors parsing decorators ([#6390](https://github.com/remix-run/remix/pull/6390)) + +## 1.16.0 + +### Minor Changes + +- add deprecation warning to `@remix-run/eslint-config/jest` ESLint config ([#5697](https://github.com/remix-run/remix/pull/5697)) + +## 1.15.0 + +### Patch Changes + +- Updated TypeScript `peerDependency` to allow for `^5.0.0` ([`6b6ee66a2`](https://github.com/remix-run/remix/commit/6b6ee66a2fef1a7b00ce4c95abed144ab80017e3)) + +## 1.14.3 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.14.2) for an overview of all changes in v1.14.3. + +## 1.14.2 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.14.2) for an overview of all changes in v1.14.2. + +## 1.14.1 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.14.1) for an overview of all changes in v1.14.1. + +## 1.14.0 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.14.0) for an overview of all changes in v1.14.0. + +## 1.13.0 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.13.0) for an overview of all changes in v1.13.0. + +## 1.12.0 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.12.0) for an overview of all changes in v1.12.0. + +## 1.11.1 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.11.1) for an overview of all changes in v1.11.1. + +## 1.11.0 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.11.0) for an overview of all changes in v1.11.0. + +## 1.10.1 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.10.1) for an overview of all changes in v1.10.1. + +## 1.10.0 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.10.0) for an overview of all changes in v1.10.0. + +## 1.9.0 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.9.0) for an overview of all changes in v1.9.0. + +## 1.8.2 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.8.2) for an overview of all changes in v1.8.2. + +## 1.8.1 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.8.1) for an overview of all changes in v1.8.1. + +## 1.8.0 + +### Patch Changes + +- Replace references to the old `migrate` command with the new `codemod` command ([#4646](https://github.com/remix-run/remix/pull/4646)) + +## 1.7.6 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.7.6) for an overview of all changes in v1.7.6. + +## 1.7.5 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.7.5) for an overview of all changes in v1.7.5. + +## 1.7.4 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.7.4) for an overview of all changes in v1.7.4. + +## 1.7.3 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.7.3) for an overview of all changes in v1.7.3. + +## 1.7.2 + +### Patch Changes + +- Update ESLint and plugin dependencies ([`e4ec81c77`](https://github.com/remix-run/remix/commit/e4ec81c77ef9f534450a45c9474ffba6dfd9bd24)) + +## 1.7.1 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.7.1) for an overview of all changes in v1.7.1. + +## 1.7.0 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.7.0) for an overview of all changes in v1.7.0. + +## 1.6.8 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.6.8) for an overview of all changes in v1.6.8. + +## 1.6.7 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.6.7) for an overview of all changes in v1.6.7. + +## 1.6.6 + +No significant changes to this package were made in this release. [See the releases page on GitHub](https://github.com/remix-run/remix/releases/tag/remix%401.6.6) for an overview of all changes in v1.6.6. + +## 1.6.5 + +### Patch Changes + +- Use `require.resolve` when importing `@babel/preset-react` ([#3716](https://github.com/remix-run/remix/pull/3716)) diff --git a/README.md b/README.md new file mode 100644 index 0000000..44ef8a2 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# `@remix-run/eslint-config` + +This package includes a shareable ESLint config for Remix projects. + +If you create your app with `create-remix` no additional configuration is necessary. + +## Installation + +First, install this package along with ESLint in your project. **This package requires at least version 8.1 of ESLint** + +```sh +npm install -D eslint @remix-run/eslint-config +``` + +Then create a file named `.eslintrc.js` in the root of your project: + +```js filename=.eslintrc.js +module.exports = { + extends: "@remix-run/eslint-config", +}; +``` + +### Jest + Testing Library + +This packages also ships with optional configuration options for projects that use [Jest](https://jestjs.io/) with [Testing Library](https://testing-library.com). To enable these rules, add the following to your `.eslintrc`: + +```js filename=.eslintrc.js +module.exports = { + extends: [ + "@remix-run/eslint-config", + "@remix-run/eslint-config/jest-testing-library", + ], +}; +``` + +Please note that because this ruleset is optional, we do not include the core libraries as peer dependencies for this package. If you use these rules, be sure that you have the following dependencies installed in your project: + +```json filename=package.json +{ + "dependencies": { + "@testing-library/jest-dom": ">=5.16.0", + "@testing-library/react": ">=12.0.0", + "jest": ">=26.0.0" + } +} +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..660bb16 --- /dev/null +++ b/index.js @@ -0,0 +1,97 @@ +// Splitting rules into separate modules allow for a lower-level API if our +// default rules become difficult to extend without lots of duplication. +const coreRules = require("./rules/core"); +const importRules = require("./rules/import"); +const reactRules = require("./rules/react"); +const jsxA11yRules = require("./rules/jsx-a11y"); +const typescriptRules = require("./rules/typescript"); +const importSettings = require("./settings/import"); +const reactSettings = require("./settings/react"); + +/** + * @see https://github.com/eslint/eslint/issues/3458 + * @see https://www.npmjs.com/package/@rushstack/eslint-patch + */ +require("@rushstack/eslint-patch/modern-module-resolution"); + +const OFF = 0; +// const WARN = 1; +// const ERROR = 2; + +/** @type {import('eslint').Linter.Config} */ +const config = { + parser: "@babel/eslint-parser", + parserOptions: { + sourceType: "module", + requireConfigFile: false, + ecmaVersion: "latest", + babelOptions: { + presets: [require.resolve("@babel/preset-react")], + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + plugins: ["import", "react", "react-hooks", "jsx-a11y"], + settings: { + ...reactSettings, + ...importSettings, + }, + + // NOTE: In general - we want to use prettier for the majority of stylistic + // concerns. However there are some "stylistic" eslint rules we use that should + // not fail a PR since we can auto-fix them after merging to dev. These rules + // should be set to WARN. + // + // ERROR should be used for "functional" rules that indicate a problem in the + // code, and these will cause a PR failure + + // IMPORTANT: Ensure that rules used here are compatible with + // typescript-eslint. If they are not, we need to turn the rule off in our + // overrides for ts/tsx. + + // To read the details for any rule, see https://eslint.org/docs/rules/[RULE-KEY] + rules: { + ...coreRules, + ...importRules, + ...reactRules, + ...jsxA11yRules, + }, + overrides: [ + { + files: ["**/*.ts?(x)"], + extends: ["plugin:import/typescript"], + parser: "@typescript-eslint/parser", + parserOptions: { + sourceType: "module", + ecmaVersion: 2019, + ecmaFeatures: { + jsx: true, + }, + warnOnUnsupportedTypeScriptVersion: true, + }, + plugins: ["@typescript-eslint"], + rules: { + ...typescriptRules, + }, + }, + { + files: [ + "**/routes/**/*.js?(x)", + "**/routes/**/*.tsx", + "app/root.js?(x)", + "app/root.tsx", + ], + rules: { + // Routes may use default exports without a name. At the route level + // identifying components for debugging purposes is less of an issue, as + // the route boundary is more easily identifiable. + "react/display-name": OFF, + }, + }, + ], +}; + +module.exports = config; diff --git a/internal.js b/internal.js new file mode 100644 index 0000000..aade8ad --- /dev/null +++ b/internal.js @@ -0,0 +1,89 @@ +/** + * This config is intended for internal Remix projects. It should not be + * documented nor considered public API in regard to semver considerations. + */ + +/** + * @see https://github.com/eslint/eslint/issues/3458 + * @see https://www.npmjs.com/package/@rushstack/eslint-patch + */ +require("@rushstack/eslint-patch/modern-module-resolution"); + +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + extends: [ + require.resolve("./index.js"), + require.resolve("./jest-testing-library.js"), + ], + env: { + node: true, + }, + plugins: [ + // Plugins used in the internal config should be installed in our + // repositories. We don't want to ship these as dependencies to consumers + // who may not use them. + "node", + "prefer-let", + ], + rules: { + "@typescript-eslint/consistent-type-imports": ERROR, + "import/order": [ + ERROR, + { + "newlines-between": "always", + groups: [ + ["builtin", "external"], + "internal", + ["parent", "sibling", "index"], + ], + }, + ], + "jest/no-disabled-tests": OFF, + "prefer-let/prefer-let": WARN, + }, + overrides: [ + { + // all code blocks in .md files + files: ["**/*.md/*.js?(x)", "**/*.md/*.ts?(x)"], + rules: { + "no-unreachable": OFF, + "no-unused-expressions": OFF, + "no-unused-labels": OFF, + "no-unused-vars": OFF, + "prefer-const": WARN, + "jsx-a11y/alt-text": OFF, + "jsx-a11y/anchor-has-content": OFF, + "prefer-let/prefer-let": OFF, + "react/jsx-no-comment-textnodes": OFF, + "react/jsx-no-undef": OFF, + }, + }, + { + // all ```ts & ```tsx code blocks in .md files + files: ["**/*.md/*.ts?(x)"], + rules: { + "@typescript-eslint/no-unused-expressions": OFF, + "@typescript-eslint/no-unused-vars": OFF, + }, + }, + { + files: ["packages/**/*.*"], + excludedFiles: "**/__tests__/**/*.*", + rules: { + // Validate dependencies are listed in workspace package.json files + "import/no-extraneous-dependencies": ERROR, + }, + }, + { + files: ["integration/**/*.*"], + env: { + "jest/globals": false, + }, + }, + ], +}; diff --git a/jest-testing-library.js b/jest-testing-library.js new file mode 100644 index 0000000..db2e297 --- /dev/null +++ b/jest-testing-library.js @@ -0,0 +1,29 @@ +const jestRules = require("./rules/jest"); +const jestDomRules = require("./rules/jest-dom"); +const testingLibraryRules = require("./rules/testing-library"); + +/** + * @see https://github.com/eslint/eslint/issues/3458 + * @see https://www.npmjs.com/package/@rushstack/eslint-patch + */ +require("@rushstack/eslint-patch/modern-module-resolution"); + +module.exports = { + plugins: ["jest", "jest-dom", "testing-library"], + env: { + node: true, + }, + overrides: [ + { + files: ["**/__tests__/**/*", "**/*.{spec,test}.*"], + env: { + "jest/globals": true, + }, + rules: { + ...jestRules, + ...jestDomRules, + ...testingLibraryRules, + }, + }, + ], +}; diff --git a/jest.js b/jest.js new file mode 100644 index 0000000..dac2ee8 --- /dev/null +++ b/jest.js @@ -0,0 +1,47 @@ +const jestRules = require("./rules/jest"); + +/** + * @see https://github.com/eslint/eslint/issues/3458 + * @see https://www.npmjs.com/package/@rushstack/eslint-patch + */ +require("@rushstack/eslint-patch/modern-module-resolution"); + +const alreadyWarned = {}; +const warnOnce = (condition, message) => { + if (!condition && !alreadyWarned[message]) { + alreadyWarned[message] = true; + console.warn(message); + } +}; + +warnOnce( + false, + "⚠️ DEPRECATED: The `@remix-run/eslint-config/jest` ESLint config " + + "has been deprecated in favor of " + + "`@remix-run/eslint-config/jest-testing-library` and will be removed in " + + "Remix v2. Please update your code to use " + + "`@remix-run/eslint-config/jest-testing-library` instead." +); + +/** + * @deprecated Use `@remix-run/eslint-config/jest-testing-library` instead. + */ +const jestConfig = { + plugins: ["jest"], + env: { + node: true, + }, + overrides: [ + { + files: ["**/__tests__/**/*", "**/*.{spec,test}.*"], + env: { + "jest/globals": true, + }, + rules: { + ...jestRules, + }, + }, + ], +}; + +module.exports = jestConfig; diff --git a/node.js b/node.js new file mode 100644 index 0000000..e515800 --- /dev/null +++ b/node.js @@ -0,0 +1,12 @@ +/** + * @see https://github.com/eslint/eslint/issues/3458 + * @see https://www.npmjs.com/package/@rushstack/eslint-patch + */ +require("@rushstack/eslint-patch/modern-module-resolution"); + +module.exports = { + plugins: ["node"], + env: { + node: true, + }, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..85c8831 --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "@remix-run/eslint-config", + "version": "1.16.1", + "description": "ESLint configuration for Remix projects", + "bugs": { + "url": "https://github.com/remix-run/remix/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/remix-run/remix", + "directory": "packages/remix-eslint-config" + }, + "license": "MIT", + "main": "index.js", + "files": [ + "README.md", + "index.js", + "internal.js", + "jest.js", + "jest-testing-library.js", + "node.js", + "rules", + "settings" + ], + "dependencies": { + "@babel/core": "^7.21.8", + "@babel/eslint-parser": "^7.21.8", + "@babel/preset-react": "^7.18.6", + "@rushstack/eslint-patch": "^1.2.0", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "eslint-import-resolver-node": "0.3.7", + "eslint-import-resolver-typescript": "^3.5.4", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^26.9.0", + "eslint-plugin-jest-dom": "^4.0.3", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^5.10.2" + }, + "devDependencies": { + "@types/eslint": "^8.37.0", + "eslint": "^8.37.0", + "jest": "^27.5.1", + "react": "^18.2.0", + "typescript": "^5.0.4" + }, + "peerDependencies": { + "eslint": "^8.0.0", + "react": "^17.0.0 || ^18.0.0", + "typescript": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } +} diff --git a/rules/core.js b/rules/core.js new file mode 100644 index 0000000..aff9275 --- /dev/null +++ b/rules/core.js @@ -0,0 +1,138 @@ +const { + defaultAdapterExports, + defaultRuntimeExports, + architectSpecificExports, + cloudflareSpecificExports, + cloudflarePagesSpecificExports, + cloudflareWorkersSpecificExports, + nodeSpecificExports, + reactSpecificExports, +} = require("./packageExports"); + +// const OFF = 0; +const WARN = 1; +const ERROR = 2; + +const getReplaceRemixImportsMessage = (packageName) => + `All \`remix\` exports are considered deprecated as of v1.3.3. Please use \`@remix-run/${packageName}\` instead. Run \`npx @remix-run/dev@latest codemod replace-remix-magic-imports\` to automatically migrate your code.`; + +const replaceRemixImportsOptions = [ + { + packageExports: defaultAdapterExports, + packageName: + "{architect|cloudflare-pages|cloudflare-workers|express|netlify|vercel}", + }, + { packageExports: defaultRuntimeExports, packageName: "{cloudflare|node}" }, + { packageExports: architectSpecificExports, packageName: "architect" }, + { packageExports: cloudflareSpecificExports, packageName: "cloudflare" }, + { + packageExports: cloudflarePagesSpecificExports, + packageName: "cloudflare-pages", + }, + { + packageExports: cloudflareWorkersSpecificExports, + packageName: "cloudflare-workers", + }, + { packageExports: nodeSpecificExports, packageName: "node" }, + { packageExports: reactSpecificExports, packageName: "react" }, +].map(({ packageExports, packageName }) => ({ + importNames: [...packageExports.value, ...packageExports.type], + message: getReplaceRemixImportsMessage(packageName), + name: "remix", +})); + +module.exports = { + "array-callback-return": WARN, + "getter-return": WARN, + "new-parens": WARN, + "no-array-constructor": WARN, + "no-caller": ERROR, + "no-cond-assign": [WARN, "except-parens"], + "no-const-assign": ERROR, + "no-control-regex": WARN, + "no-dupe-args": WARN, + "no-dupe-class-members": WARN, + "no-dupe-keys": WARN, + "no-duplicate-case": WARN, + "no-empty-character-class": WARN, + "no-empty-pattern": WARN, + "no-duplicate-imports": WARN, + "no-empty": [WARN, { allowEmptyCatch: true }], + "no-eval": ERROR, + "no-ex-assign": WARN, + "no-extend-native": WARN, + "no-extra-bind": WARN, + "no-extra-label": WARN, + "no-extra-boolean-cast": WARN, + "no-func-assign": ERROR, + "no-global-assign": ERROR, + "no-implied-eval": WARN, + "no-invalid-regexp": WARN, + "no-label-var": WARN, + "no-labels": [WARN, { allowLoop: true, allowSwitch: false }], + "no-lone-blocks": WARN, + "no-loop-func": WARN, + "no-mixed-operators": [ + WARN, + { + groups: [ + ["&", "|", "^", "~", "<<", ">>", ">>>"], + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||"], + ["in", "instanceof"], + ], + allowSamePrecedence: false, + }, + ], + "no-unsafe-negation": WARN, + "no-new-func": WARN, + "no-new-object": WARN, + "no-octal": WARN, + "no-redeclare": ERROR, + "no-restricted-imports": [WARN, ...replaceRemixImportsOptions], + "no-script-url": WARN, + "no-self-assign": WARN, + "no-self-compare": WARN, + "no-sequences": WARN, + "no-shadow-restricted-names": WARN, + "no-sparse-arrays": WARN, + "no-template-curly-in-string": WARN, + "no-this-before-super": WARN, + "no-undef": ERROR, + "no-unreachable": WARN, + "no-unused-expressions": [ + WARN, + { + allowShortCircuit: true, + allowTernary: true, + allowTaggedTemplates: true, + }, + ], + "no-unused-labels": WARN, + "no-unused-vars": [ + WARN, + { + args: "none", + ignoreRestSiblings: true, + }, + ], + "no-use-before-define": [ + WARN, + { classes: false, functions: false, variables: false }, + ], + "no-useless-computed-key": WARN, + "no-useless-concat": WARN, + "no-useless-constructor": WARN, + "no-useless-escape": WARN, + "no-useless-rename": [ + WARN, + { + ignoreDestructuring: false, + ignoreImport: false, + ignoreExport: false, + }, + ], + "require-yield": WARN, + "use-isnan": WARN, + "valid-typeof": WARN, +}; diff --git a/rules/import.js b/rules/import.js new file mode 100644 index 0000000..b98e759 --- /dev/null +++ b/rules/import.js @@ -0,0 +1,9 @@ +// const OFF = 0; +// const WARN = 1; +const ERROR = 2; + +module.exports = { + "import/first": ERROR, + "import/no-amd": ERROR, + "import/no-webpack-loader-syntax": ERROR, +}; diff --git a/rules/jest-dom.js b/rules/jest-dom.js new file mode 100644 index 0000000..aad70a2 --- /dev/null +++ b/rules/jest-dom.js @@ -0,0 +1,17 @@ +// const OFF = 0; +const WARN = 1; +// const ERROR = 2; + +module.exports = { + "jest-dom/prefer-checked": WARN, + "jest-dom/prefer-empty": WARN, + "jest-dom/prefer-enabled-disabled": WARN, + "jest-dom/prefer-focus": WARN, + "jest-dom/prefer-in-document": WARN, + "jest-dom/prefer-required": WARN, + "jest-dom/prefer-to-have-attribute": WARN, + "jest-dom/prefer-to-have-class": WARN, + "jest-dom/prefer-to-have-style": WARN, + "jest-dom/prefer-to-have-text-content": WARN, + "jest-dom/prefer-to-have-value": WARN, +}; diff --git a/rules/jest.js b/rules/jest.js new file mode 100644 index 0000000..89f235d --- /dev/null +++ b/rules/jest.js @@ -0,0 +1,19 @@ +// const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + "jest/no-conditional-expect": WARN, + "jest/no-deprecated-functions": WARN, + "jest/no-disabled-tests": WARN, + "jest/no-export": ERROR, + "jest/no-focused-tests": WARN, + "jest/no-identical-title": WARN, + "jest/no-interpolation-in-snapshots": WARN, + "jest/no-jasmine-globals": ERROR, + "jest/no-jest-import": WARN, + "jest/no-mocks-import": WARN, + "jest/valid-describe-callback": ERROR, + "jest/valid-expect": ERROR, + "jest/valid-expect-in-promise": ERROR, +}; diff --git a/rules/jsx-a11y.js b/rules/jsx-a11y.js new file mode 100644 index 0000000..3f19447 --- /dev/null +++ b/rules/jsx-a11y.js @@ -0,0 +1,21 @@ +// const OFF = 0; +const WARN = 1; +// const ERROR = 2; + +module.exports = { + "jsx-a11y/alt-text": WARN, + "jsx-a11y/anchor-has-content": [WARN, { components: ["Link", "NavLink"] }], + "jsx-a11y/anchor-is-valid": [WARN, { aspects: ["noHref", "invalidHref"] }], + "jsx-a11y/aria-activedescendant-has-tabindex": WARN, + "jsx-a11y/aria-props": WARN, + "jsx-a11y/aria-proptypes": WARN, + "jsx-a11y/aria-role": [WARN, { ignoreNonDOM: true }], + "jsx-a11y/aria-unsupported-elements": WARN, + "jsx-a11y/iframe-has-title": WARN, + "jsx-a11y/img-redundant-alt": WARN, + "jsx-a11y/lang": WARN, + "jsx-a11y/no-access-key": WARN, + "jsx-a11y/no-redundant-roles": WARN, + "jsx-a11y/role-has-required-aria-props": WARN, + "jsx-a11y/role-supports-aria-props": WARN, +}; diff --git a/rules/packageExports.js b/rules/packageExports.js new file mode 100644 index 0000000..a3c02ef --- /dev/null +++ b/rules/packageExports.js @@ -0,0 +1,162 @@ +const defaultAdapterExports = { + value: ["createRequestHandler"], + type: ["GetLoadContextFunction", "RequestHandler"], +}; + +const defaultRuntimeExports = { + value: [ + "createCookie", + "createCookieSessionStorage", + "createMemorySessionStorage", + "createRequestHandler", + "createSession", + "createSessionStorage", + "isCookie", + "isSession", + "json", + "MaxPartSizeExceededError", + "redirect", + "unstable_composeUploadHandlers", + "unstable_createMemoryUploadHandler", + "unstable_parseMultipartFormData", + ], + type: [ + "ActionFunction", + "AppData", + "AppLoadContext", + "Cookie", + "CookieOptions", + "CookieParseOptions", + "CookieSerializeOptions", + "CookieSignatureOptions", + "CreateRequestHandlerFunction", + "DataFunctionArgs", + "EntryContext", + "ErrorBoundaryComponent", + "HandleDataRequestFunction", + "HandleDocumentRequestFunction", + "HeadersFunction", + "HtmlLinkDescriptor", + "HtmlMetaDescriptor", + "LinkDescriptor", + "LinksFunction", + "LoaderFunction", + "MemoryUploadHandlerFilterArgs", + "MemoryUploadHandlerOptions", + "MetaDescriptor", + "MetaFunction", + "PageLinkDescriptor", + "RequestHandler", + "RouteComponent", + "RouteHandle", + "ServerBuild", + "ServerEntryModule", + "Session", + "SessionData", + "SessionIdStorageStrategy", + "SessionStorage", + "UploadHandler", + "UploadHandlerPart", + ], +}; + +const architectSpecificExports = { + value: ["createArcTableSessionStorage"], + type: [], +}; + +const cloudflareSpecificExports = { + value: ["createCloudflareKVSessionStorage", "createWorkersKVSessionStorage"], + type: [], +}; + +const cloudflarePagesSpecificExports = { + value: ["createPagesFunctionHandler"], + type: ["createPagesFunctionHandlerParams"], +}; + +const cloudflareWorkersSpecificExports = { + value: ["createEventHandler", "handleAsset"], + type: [], +}; + +const nodeSpecificExports = { + value: [ + "AbortController", + "createFileSessionStorage", + "createReadableStreamFromReadable", + "fetch", + "FormData", + "Headers", + "installGlobals", + "NodeOnDiskFile", + "readableStreamToString", + "Request", + "Response", + "unstable_createFileUploadHandler", + "writeAsyncIterableToWritable", + "writeReadableStreamToWritable", + ], + type: ["HeadersInit", "RequestInfo", "RequestInit", "ResponseInit"], +}; + +const reactSpecificExports = { + value: [ + "Form", + "Link", + "Links", + "LiveReload", + "Meta", + "NavLink", + "Outlet", + "PrefetchPageLinks", + "RemixBrowser", + "RemixServer", + "Scripts", + "ScrollRestoration", + "useActionData", + "useBeforeUnload", + "useCatch", + "useFetcher", + "useFetchers", + "useFormAction", + "useHref", + "useLoaderData", + "useLocation", + "useMatches", + "useNavigate", + "useNavigationType", + "useOutlet", + "useOutletContext", + "useParams", + "useResolvedPath", + "useSearchParams", + "useSubmit", + "useTransition", + ], + type: [ + "FormEncType", + "FormMethod", + "FormProps", + "HtmlLinkDescriptor", + "HtmlMetaDescriptor", + "LinkProps", + "NavLinkProps", + "RemixBrowserProps", + "RemixServerProps", + "SubmitFunction", + "SubmitOptions", + "ThrownResponse", + ], +}; + +module.exports = { + defaultAdapterExports, + defaultRuntimeExports, + architectSpecificExports, + cloudflareSpecificExports, + cloudflarePagesSpecificExports, + cloudflareWorkersSpecificExports, + nodeSpecificExports, + reactSpecificExports, +}; diff --git a/rules/react.js b/rules/react.js new file mode 100644 index 0000000..98b1392 --- /dev/null +++ b/rules/react.js @@ -0,0 +1,30 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + "react/display-name": WARN, + "react/forbid-foreign-prop-types": [WARN, { allowInPropTypes: true }], + "react/jsx-key": WARN, + "react/jsx-no-comment-textnodes": WARN, + "react/jsx-no-target-blank": WARN, + "react/jsx-no-undef": ERROR, + "react/jsx-pascal-case": [WARN, { allowAllCaps: true, ignore: [] }], + "react/jsx-uses-vars": WARN, + "react/jsx-uses-react": WARN, + "react/no-danger-with-children": WARN, + "react/no-direct-mutation-state": WARN, + "react/no-find-dom-node": WARN, + "react/no-is-mounted": WARN, + "react/no-render-return-value": ERROR, + "react/no-string-refs": WARN, + "react/no-typos": WARN, + "react/react-in-jsx-scope": OFF, + "react/require-render-return": ERROR, + "react/style-prop-object": WARN, + + // react-hooks + // https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks + "react-hooks/exhaustive-deps": WARN, + "react-hooks/rules-of-hooks": ERROR, +}; diff --git a/rules/testing-library.js b/rules/testing-library.js new file mode 100644 index 0000000..96b3408 --- /dev/null +++ b/rules/testing-library.js @@ -0,0 +1,25 @@ +// const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + "testing-library/await-async-query": ERROR, + "testing-library/await-async-utils": ERROR, + "testing-library/no-await-sync-events": ERROR, + "testing-library/no-await-sync-query": ERROR, + "testing-library/no-debugging-utils": WARN, + "testing-library/no-promise-in-fire-event": ERROR, + "testing-library/no-render-in-setup": ERROR, + "testing-library/no-unnecessary-act": ERROR, + "testing-library/no-wait-for-empty-callback": ERROR, + "testing-library/no-wait-for-multiple-assertions": ERROR, + "testing-library/no-wait-for-side-effects": ERROR, + "testing-library/no-wait-for-snapshot": ERROR, + "testing-library/prefer-find-by": WARN, + "testing-library/prefer-presence-queries": WARN, + "testing-library/prefer-query-by-disappearance": WARN, + "testing-library/prefer-screen-queries": WARN, + "testing-library/prefer-user-event": WARN, + "testing-library/prefer-wait-for": WARN, + "testing-library/render-result-naming-convention": WARN, +}; diff --git a/rules/typescript.js b/rules/typescript.js new file mode 100644 index 0000000..f81d2f1 --- /dev/null +++ b/rules/typescript.js @@ -0,0 +1,54 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + "no-dupe-class-members": OFF, + "no-undef": OFF, + + // Add TypeScript specific rules (and turn off ESLint equivalents) + "@typescript-eslint/consistent-type-assertions": WARN, + "@typescript-eslint/consistent-type-imports": WARN, + + "no-array-constructor": OFF, + "@typescript-eslint/no-array-constructor": WARN, + + // There is a bug w/ @typescript-eslint/no-duplicate-imports triggered + // by multiple imports inside of module declarations. We should reenable + // this rule when the bug is fixed. + // https://github.com/typescript-eslint/typescript-eslint/issues/3071 + "no-duplicate-imports": OFF, + // "@typescript-eslint/no-duplicate-imports": WARN, + + "no-redeclare": OFF, + "@typescript-eslint/no-redeclare": ERROR, + "no-use-before-define": OFF, + "@typescript-eslint/no-use-before-define": [ + WARN, + { + functions: false, + classes: false, + variables: false, + typedefs: false, + }, + ], + "no-unused-expressions": OFF, + "@typescript-eslint/no-unused-expressions": [ + WARN, + { + allowShortCircuit: true, + allowTernary: true, + allowTaggedTemplates: true, + }, + ], + "no-unused-vars": OFF, + "@typescript-eslint/no-unused-vars": [ + WARN, + { + args: "none", + ignoreRestSiblings: true, + }, + ], + "no-useless-constructor": OFF, + "@typescript-eslint/no-useless-constructor": WARN, +}; diff --git a/settings/import.js b/settings/import.js new file mode 100644 index 0000000..63309b3 --- /dev/null +++ b/settings/import.js @@ -0,0 +1,14 @@ +module.exports = { + "import/ignore": ["node_modules", "\\.(css|md|svg|json)$"], + "import/parsers": { + [require.resolve("@typescript-eslint/parser")]: [".ts", ".tsx", ".d.ts"], + }, + "import/resolver": { + [require.resolve("eslint-import-resolver-node")]: { + extensions: [".js", ".jsx", ".ts", ".tsx"], + }, + [require.resolve("eslint-import-resolver-typescript")]: { + alwaysTryTypes: true, + }, + }, +}; diff --git a/settings/react.js b/settings/react.js new file mode 100644 index 0000000..1d91102 --- /dev/null +++ b/settings/react.js @@ -0,0 +1,16 @@ +module.exports = { + react: { + version: "detect", + formComponents: ["Form"], + linkComponents: [ + { + name: "Link", + linkAttribute: "to", + }, + { + name: "NavLink", + linkAttribute: "to", + }, + ], + }, +};