diff --git a/.eslintrc.js b/.eslintrc.js index 29c7311c..f2a286f5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,22 +1,135 @@ -/** @type {import('@types/eslint').Linter.BaseConfig} */ +/** + * This is intended to be a basic starting point for linting in the Blues Stack. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ module.exports = { root: true, - extends: [ - "@remix-run/eslint-config", - "@remix-run/eslint-config/node", - "@remix-run/eslint-config/jest-testing-library", - "prettier", - ], + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, env: { - "cypress/globals": true, + browser: true, + commonjs: true, + es6: true, }, - plugins: ["cypress"], - // We're using vitest which has a very similar API to jest - // (so the linting plugins work nicely), but we have to - // set the jest version explicitly. - settings: { - jest: { - version: 28, + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:jsx-a11y/recommended", + "prettier", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + }, + rules: { + "react/jsx-no-leaked-render": [ + "warn", + { validStrategies: ["ternary"] }, + ], + }, }, - }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/stylistic", + "plugin:import/recommended", + "plugin:import/typescript", + "prettier", + ], + rules: { + "import/order": [ + "error", + { + alphabetize: { caseInsensitive: true, order: "asc" }, + groups: ["builtin", "external", "internal", "parent", "sibling"], + "newlines-between": "always", + }, + ], + }, + }, + + // Markdown + { + files: ["**/*.md"], + plugins: ["markdown"], + extends: ["plugin:markdown/recommended", "prettier"], + }, + + // Jest/Vitest + { + files: ["**/*.test.{js,jsx,ts,tsx}"], + plugins: ["jest", "jest-dom", "testing-library"], + extends: [ + "plugin:jest/recommended", + "plugin:jest-dom/recommended", + "plugin:testing-library/react", + "prettier", + ], + env: { + "jest/globals": true, + }, + settings: { + jest: { + // we're using vitest which has a very similar API to jest + // (so the linting plugins work nicely), but it means we have to explicitly + // set the jest version. + version: 28, + }, + }, + }, + + // Cypress + { + files: ["cypress/**/*.ts"], + plugins: ["cypress"], + extends: ["plugin:cypress/recommended", "prettier"], + }, + + // Node + { + files: [".eslintrc.js", "mocks/**/*.js"], + env: { + node: true, + }, + }, + ], }; diff --git a/.eslintrc.repo.js b/.eslintrc.repo.js deleted file mode 100644 index 67f48962..00000000 --- a/.eslintrc.repo.js +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-env es6 */ -const OFF = 0; -const WARN = 1; -// const ERROR = 2; - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: [ - "./.eslintrc.js", - "@remix-run/eslint-config/internal", - "plugin:markdown/recommended", - ], - plugins: ["markdown"], - settings: { - "import/internal-regex": "^~/", - }, - rules: { - "prefer-let/prefer-let": OFF, - "prefer-const": WARN, - - "import/order": [ - WARN, - { - alphabetize: { caseInsensitive: true, order: "asc" }, - groups: ["builtin", "external", "internal", "parent", "sibling"], - "newlines-between": "always", - }, - ], - - "react/jsx-no-leaked-render": [WARN, { validStrategies: ["ternary"] }], - }, -}; diff --git a/.github/workflows/lint-repo.yml b/.github/workflows/lint-repo.yml index 5ee6c4a0..9b9cd382 100644 --- a/.github/workflows/lint-repo.yml +++ b/.github/workflows/lint-repo.yml @@ -30,4 +30,4 @@ jobs: run: npm install - name: 🔬 Lint - run: npm run lint:repo + run: npm run lint diff --git a/app/models/user.server.ts b/app/models/user.server.ts index ffdc0cca..42be9b7f 100644 --- a/app/models/user.server.ts +++ b/app/models/user.server.ts @@ -56,6 +56,7 @@ export async function verifyLogin( return null; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { password: _password, ...userWithoutPassword } = userWithPassword; return userWithoutPassword; diff --git a/app/routes/join.tsx b/app/routes/join.tsx index 5b42ad04..f1ea5660 100644 --- a/app/routes/join.tsx +++ b/app/routes/join.tsx @@ -100,6 +100,7 @@ export default function Join() { ref={emailRef} id="email" required + // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus={true} name="email" type="email" diff --git a/app/routes/login.tsx b/app/routes/login.tsx index aa6250e3..c61981c8 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -95,6 +95,7 @@ export default function LoginPage() { ref={emailRef} id="email" required + // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus={true} name="email" type="email" @@ -160,7 +161,7 @@ export default function LoginPage() {
- Don't have an account?{" "} + Don't have an account?{" "} ( name: string, valueFactory: () => Value, ): Value => { - const g = global as any; + const g = global as unknown as { __singletons: Record }; g.__singletons ??= {}; g.__singletons[name] ??= valueFactory(); - return g.__singletons[name]; + return g.__singletons[name] as Value; }; diff --git a/app/utils.ts b/app/utils.ts index 3a54cc1f..f6da6bd0 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -44,8 +44,13 @@ export function useMatchesData( return route?.data as Record; } -function isUser(user: any): user is User { - return user && typeof user === "object" && typeof user.email === "string"; +function isUser(user: unknown): user is User { + return ( + user != null && + typeof user === "object" && + "email" in user && + typeof user.email === "string" + ); } export function useOptionalUser(): User | undefined { diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 0865e4e1..e7751d1f 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,6 +1,7 @@ import { faker } from "@faker-js/faker"; declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { /** @@ -85,7 +86,7 @@ function deleteUserByEmail(email: string) { // Also added custom types to avoid getting detached // https://github.com/cypress-io/cypress/issues/7306#issuecomment-1152752612 // =========================================================== -function visitAndCheck(url: string, waitTime: number = 1000) { +function visitAndCheck(url: string, waitTime = 1000) { cy.visit(url); cy.location("pathname").should("contain", url).wait(waitTime); } diff --git a/package.json b/package.json index 5a3b55e2..cf1390f3 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,8 @@ "dev:remix": "remix dev --manual -c \"node --require ./mocks ./build/server.js\"", "docker": "docker compose up -d", "format": "prettier --write .", - "format:repo": "npm run format && npm run lint:repo -- --fix", + "format:repo": "npm run format && npm run lint -- --fix", "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .", - "lint:repo": "npm run lint -- --config ./.eslintrc.repo.js", "setup": "prisma generate && prisma migrate deploy && prisma db seed", "start": "cross-env NODE_ENV=production node ./build/server.js", "start:mocks": "cross-env NODE_ENV=production node --require ./mocks --require dotenv/config ./build/server.js", @@ -54,7 +53,6 @@ "devDependencies": { "@faker-js/faker": "^8.0.2", "@remix-run/dev": "*", - "@remix-run/eslint-config": "*", "@testing-library/cypress": "^9.0.0", "@testing-library/jest-dom": "^5.17.0", "@types/bcryptjs": "^2.4.2", @@ -66,6 +64,8 @@ "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@types/source-map-support": "^0.5.6", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", "@vitejs/plugin-react": "^4.0.4", "@vitest/coverage-v8": "^0.34.2", "autoprefixer": "^10.4.15", @@ -73,11 +73,17 @@ "cypress": "12.17.3", "dotenv": "^16.3.1", "esbuild": "^0.19.2", - "eslint": "^8.47.0", + "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", - "eslint-plugin-cypress": "^2.14.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-cypress": "^2.15.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jest": "^27.4.2", + "eslint-plugin-jest-dom": "^5.1.0", + "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-markdown": "^3.0.1", - "eslint-plugin-prefer-let": "^3.0.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-testing-library": "^6.0.2", "happy-dom": "^10.10.4", "msw": "^1.2.3", "npm-run-all": "^4.1.5", diff --git a/remix.init/index.js b/remix.init/index.js index 3835309b..9b8ea999 100644 --- a/remix.init/index.js +++ b/remix.init/index.js @@ -56,30 +56,17 @@ const getPackageManagerVersion = (packageManager) => const getRandomString = (length) => crypto.randomBytes(length).toString("hex"); -const removeUnusedDependencies = (dependencies, unusedDependencies) => - Object.fromEntries( - Object.entries(dependencies).filter( - ([key]) => !unusedDependencies.includes(key), - ), - ); - const updatePackageJson = ({ APP_NAME, packageJson }) => { const { - devDependencies, scripts: { + // eslint-disable-next-line no-unused-vars "format:repo": _repoFormatScript, - "lint:repo": _repoLintScript, ...scripts }, } = packageJson.content; packageJson.update({ name: APP_NAME, - devDependencies: removeUnusedDependencies( - devDependencies, - // packages that are only used for linting the repo - ["eslint-plugin-markdown", "eslint-plugin-prefer-let"], - ), scripts, }); }; @@ -190,11 +177,10 @@ const main = async ({ packageManager, rootDirectory }) => { fs.rm(path.join(rootDirectory, ".github", "workflows", "no-response.yml")), fs.rm(path.join(rootDirectory, ".github", "dependabot.yml")), fs.rm(path.join(rootDirectory, ".github", "PULL_REQUEST_TEMPLATE.md")), - fs.rm(path.join(rootDirectory, ".eslintrc.repo.js")), fs.rm(path.join(rootDirectory, "LICENSE.md")), ]); - execSync(pm.run("format", "--loglevel warn"), { + execSync(pm.run("format", "--log-level warn"), { cwd: rootDirectory, stdio: "inherit", });