From 9cfa232aa71c3cd96ade4602b8dcc98448a2b8f8 Mon Sep 17 00:00:00 2001 From: Adam Alston Date: Sun, 8 Dec 2024 11:07:45 -0500 Subject: [PATCH] feat: upgrade linting capabilities (#90) --- eslint.config.mjs | 58 ++++++++++++++++++++++++++++++++--- package-lock.json | 51 +++++++++++++++++++++++++----- package.json | 1 + src/App/App.tsx | 7 +++-- src/App/AppContext.tsx | 8 +++-- src/App/config.tsx | 2 +- src/Index.test.tsx | 23 ++++++++------ src/__mocks__/matchMedia.ts | 4 +-- src/components/Buttons.tsx | 1 + src/components/Content.tsx | 1 + src/components/Footer.tsx | 1 + src/components/Particles.tsx | 3 +- src/components/Toggle.tsx | 1 + src/index.tsx | 2 +- src/setupTests.ts | 2 +- src/types/config.interface.ts | 2 ++ 16 files changed, 132 insertions(+), 35 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 247b5e5c4..12b67ed2b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,20 +1,68 @@ // @ts-check +import { readdirSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + import eslint from '@eslint/js'; +import perfectionist from 'eslint-plugin-perfectionist'; import tseslint from 'typescript-eslint'; +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); +const baseDir = join(_dirname, 'src'); +const internalDirectories = readdirSync(baseDir, { + withFileTypes: true, +}).flatMap((dirent) => (dirent.isDirectory() ? dirent.name : [])); + export default tseslint.config( eslint.configs.recommended, - tseslint.configs.strict, - tseslint.configs.stylistic, - { ignores: ['build', 'deprecated'] }, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + perfectionist.configs['recommended-natural'], + { ignores: ['build', 'coverage', 'deprecated'] }, { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: _dirname, + }, + }, rules: { eqeqeq: 'error', - '@typescript-eslint/no-empty-function': [ + '@typescript-eslint/no-unnecessary-condition': 'off', + 'perfectionist/sort-imports': [ 'error', - { allow: ['arrowFunctions'] }, + { + customGroups: { + value: { + react: ['^react$', '^react-.+'], + }, + }, + groups: [ + 'react', + 'type', + 'builtin', + 'external', + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + internalPattern: [`^(${internalDirectories.join('|')})(/|$)`], + sortSideEffects: true, + }, ], + 'perfectionist/sort-interfaces': 'off', + 'perfectionist/sort-jsx-props': 'off', + 'perfectionist/sort-modules': 'off', + 'perfectionist/sort-objects': 'off', }, }, + { + files: ['**/*.mjs'], + extends: [tseslint.configs.disableTypeChecked], + }, ); diff --git a/package-lock.json b/package-lock.json index 37b2fadbe..1dea4bad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "eslint": "^9.16.0", + "eslint-plugin-perfectionist": "^4.2.0", "husky": "^9.1.7", "jest-canvas-mock": "^2.5.2", "lint-staged": "^15.2.10", @@ -8472,6 +8473,24 @@ "node": ">= 0.4" } }, + "node_modules/eslint-plugin-perfectionist": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-4.2.0.tgz", + "integrity": "sha512-hEMFx5xfSc/0OLZXJhSaLUKkFxATuRf4yL2iVfxEcxkkb17DfoLZY9eH960dPSw5uB7o+4avqP3rtkNp1Vwo7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "^8.17.0", + "@typescript-eslint/utils": "^8.17.0", + "natural-orderby": "^5.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "eslint": ">=8.0.0" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", @@ -8806,10 +8825,11 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -8830,7 +8850,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -8845,6 +8865,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/array-flatten": { @@ -12973,6 +12997,16 @@ "dev": true, "license": "MIT" }, + "node_modules/natural-orderby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-5.0.0.tgz", + "integrity": "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -13465,10 +13499,11 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "dev": true + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", diff --git a/package.json b/package.json index bc9eda3f0..4c4d655de 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "eslint": "^9.16.0", + "eslint-plugin-perfectionist": "^4.2.0", "husky": "^9.1.7", "jest-canvas-mock": "^2.5.2", "lint-staged": "^15.2.10", diff --git a/src/App/App.tsx b/src/App/App.tsx index 0d6c11431..018ca9ddc 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from 'react'; +import { Buttons, Content, Footer, Particles, Toggle } from 'components'; + import './App.scss'; import { AppProvider } from './AppContext'; -import { Buttons, Content, Footer, Particles, Toggle } from 'components'; import { config } from './config'; export const App = () => { @@ -11,9 +12,9 @@ export const App = () => { const init = () => { if ( - window.matchMedia( + matchMedia( '(max-device-width: 820px) and (-webkit-min-device-pixel-ratio: 2)', - )?.matches + ).matches ) { setIsMobile(true); } diff --git a/src/App/AppContext.tsx b/src/App/AppContext.tsx index 523986dc0..7c18ae06e 100644 --- a/src/App/AppContext.tsx +++ b/src/App/AppContext.tsx @@ -16,7 +16,7 @@ interface AppContextInterface extends AppProviderInterface { const actions = { SET_THEME: 'SET_THEME' } as const; interface AppAction { - type: 'SET_THEME'; + type: typeof actions.SET_THEME; value: string; } @@ -26,7 +26,7 @@ const initialState: AppState = { config: {} as Config, isMobile: false, theme: themes.dark, - setTheme: () => {}, + setTheme: () => undefined, }; export const reducer = (state: AppState, action: AppAction): AppState => { @@ -58,7 +58,9 @@ export const AppProvider = ({ config: state.config, isMobile: state.isMobile, theme: state.theme, - setTheme: (value) => dispatch({ type: actions.SET_THEME, value }), + setTheme: (value) => { + dispatch({ type: actions.SET_THEME, value }); + }, }; return {children}; diff --git a/src/App/config.tsx b/src/App/config.tsx index 7f5c6e218..215fbc0fe 100644 --- a/src/App/config.tsx +++ b/src/App/config.tsx @@ -1,5 +1,5 @@ -import { Config } from 'types'; import { Email, GitHub, LinkedIn, Resume } from 'icons'; +import { Config } from 'types'; export const config: Config = { name: { diff --git a/src/Index.test.tsx b/src/Index.test.tsx index 232be7414..38367b915 100644 --- a/src/Index.test.tsx +++ b/src/Index.test.tsx @@ -1,11 +1,12 @@ -import { configure, fireEvent, render, screen } from '@testing-library/react'; import { act } from 'react'; +import { configure, fireEvent, render, screen } from '@testing-library/react'; + import '__mocks__/matchMedia'; import { App } from 'App/App'; import { AppProvider, reducer } from 'App/AppContext'; -import { Footer } from 'components'; import { themes } from 'appearance'; +import { Footer } from 'components'; configure({ testIdAttribute: 'data-v2' }); @@ -25,12 +26,12 @@ const mockState = { }, isMobile: false, theme: themes.dark, - setTheme: () => {}, + setTheme: () => undefined, }; describe('application tests', () => { beforeEach(async () => { - await act(async () => render()); + await act(() => render()); }); /** @@ -166,7 +167,7 @@ describe('application tests', () => { describe('app context tests', () => { it('should render partial footer on mobile', async () => { - await act(async () => + await act(() => render( { it("should show the dark theme when 'theme' is set to 'true' in local storage", async () => { // set local storage item and render the app localStorage.setItem('theme', 'true'); - await act(async () => render()); + await act(() => render()); // check that the local storage item has been updated correctly expect(localStorage.getItem('theme')).toEqual('dark'); @@ -217,7 +218,7 @@ describe('local storage tests', () => { it("should show the light theme when 'theme' is set to 'false' in local storage", async () => { // set local storage item and render the app localStorage.setItem('theme', 'false'); - await act(async () => render()); + await act(() => render()); // check that the local storage item has been updated correctly expect(localStorage.getItem('theme')).toEqual('light'); @@ -228,13 +229,15 @@ describe('local storage tests', () => { // https://testing-library.com/docs/react-testing-library/api/#rerender it('should persist the light theme through an app re-render', async () => { - const { rerender } = render(); + const { rerender } = await act(() => render()); expect(localStorage.getItem('theme')).toBeNull(); localStorage.setItem('theme', 'light'); // re-render the app and check the theme - await act(async () => rerender()); + act(() => { + rerender(); + }); const particles = screen.getByTestId('particles'); expect(localStorage.getItem('theme')).toEqual('light'); @@ -244,7 +247,7 @@ describe('local storage tests', () => { it('should change local storage value when toggle is clicked', async () => { // set local storage item and render the app localStorage.setItem('theme', 'light'); - await act(async () => render()); + await act(() => render()); // click the toggle const toggle = screen.getByTestId('toggle'); diff --git a/src/__mocks__/matchMedia.ts b/src/__mocks__/matchMedia.ts index 099fa5177..edc818e60 100644 --- a/src/__mocks__/matchMedia.ts +++ b/src/__mocks__/matchMedia.ts @@ -1,6 +1,6 @@ Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation((query) => ({ + value: (query: string) => ({ matches: false, media: query, onchange: null, @@ -9,5 +9,5 @@ Object.defineProperty(window, 'matchMedia', { addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), - })), + }), }); diff --git a/src/components/Buttons.tsx b/src/components/Buttons.tsx index 8f1265205..1c409dd06 100644 --- a/src/components/Buttons.tsx +++ b/src/components/Buttons.tsx @@ -1,4 +1,5 @@ import { useContext } from 'react'; + import styled from 'styled-components'; import { AppContext } from 'App/AppContext'; diff --git a/src/components/Content.tsx b/src/components/Content.tsx index a3e521907..2c298ded8 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,4 +1,5 @@ import { useContext } from 'react'; + import styled, { css } from 'styled-components'; import { AppContext } from 'App/AppContext'; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index f1d00ff4b..a0927dc59 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,4 +1,5 @@ import { useContext } from 'react'; + import styled from 'styled-components'; import { AppContext } from 'App/AppContext'; diff --git a/src/components/Particles.tsx b/src/components/Particles.tsx index 8b08a5894..fb0fc1e9b 100644 --- a/src/components/Particles.tsx +++ b/src/components/Particles.tsx @@ -1,7 +1,8 @@ import { useContext } from 'react'; -import styled from 'styled-components'; import ReactParticles from 'react-tsparticles'; +import styled from 'styled-components'; + import { AppContext } from 'App/AppContext'; import { options } from 'appearance'; import { Theme } from 'types'; diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx index e09ec26f1..e41d214b6 100644 --- a/src/components/Toggle.tsx +++ b/src/components/Toggle.tsx @@ -1,4 +1,5 @@ import { useContext } from 'react'; + import styled from 'styled-components'; import { AppContext } from 'App/AppContext'; diff --git a/src/index.tsx b/src/index.tsx index bef7c1efe..4c684c1b5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,5 @@ -import { createRoot } from 'react-dom/client'; import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; import { App } from 'App/App'; diff --git a/src/setupTests.ts b/src/setupTests.ts index a39c871f0..5e62117da 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -5,6 +5,6 @@ import '@testing-library/jest-dom'; import 'jest-canvas-mock'; -console.error = (message) => { +console.error = (message: string) => { throw new Error(`Console error: ${message}`); }; diff --git a/src/types/config.interface.ts b/src/types/config.interface.ts index e62a1072c..23511c52f 100644 --- a/src/types/config.interface.ts +++ b/src/types/config.interface.ts @@ -1,3 +1,5 @@ +import type { JSX } from 'react'; + export interface Content { display: string; }