├── .husky └── pre-commit ├── .gitignore ├── src ├── components │ ├── App │ │ ├── index.ts │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ └── react.svg │ └── RandomNumber │ │ ├── index.ts │ │ ├── RandomNumber.tsx │ │ └── RandomNumber.test.tsx ├── styles │ └── index.css ├── utils │ ├── random │ │ ├── index.ts │ │ ├── random.test.ts │ │ └── random.ts │ └── seededRandom │ │ ├── index.ts │ │ ├── seededRandom.test.ts │ │ └── seededRandom.ts ├── config │ └── constants.ts ├── static │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── browserconfig.xml │ ├── site.webmanifest │ └── safari-pinned-tab.svg ├── index.tsx ├── index.html └── types │ └── staticAssets.d.ts ├── postcss.config.cjs ├── prettier.config.mjs ├── test ├── setup.ts ├── transformers │ └── importPathTransformer.cjs ├── mocks │ └── mockRandom.ts ├── helpers │ └── userEventSetup.ts └── teardown.ts ├── .browserslistrc ├── cspell.json ├── tsconfig.json ├── .ncurc.cjs ├── webpack.config.mjs ├── LICENSE ├── jest.config.mjs ├── lint-staged.config.mjs ├── webpack.dev.mjs ├── babel.config.cjs ├── webpack.prod.mjs ├── webpack.common.mjs ├── package.json ├── README.md └── eslint.config.mjs /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no lint-staged 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | /dist 4 | -------------------------------------------------------------------------------- /src/components/App/index.ts: -------------------------------------------------------------------------------- 1 | export { App } from "./App"; 2 | -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | .center { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/random/index.ts: -------------------------------------------------------------------------------- 1 | export { Random, random } from "./random"; 2 | -------------------------------------------------------------------------------- /src/utils/seededRandom/index.ts: -------------------------------------------------------------------------------- 1 | export { SeededRandom } from "./seededRandom"; 2 | -------------------------------------------------------------------------------- /src/components/RandomNumber/index.ts: -------------------------------------------------------------------------------- 1 | export { RandomNumber } from "./RandomNumber"; 2 | -------------------------------------------------------------------------------- /src/config/constants.ts: -------------------------------------------------------------------------------- 1 | export const randomDefaults = { 2 | MIN: 0, 3 | MAX: 100, 4 | }; 5 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeTechGuy/ReactTemplate/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeTechGuy/ReactTemplate/HEAD/src/static/favicon-16x16.png -------------------------------------------------------------------------------- /src/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeTechGuy/ReactTemplate/HEAD/src/static/favicon-32x32.png -------------------------------------------------------------------------------- /src/static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeTechGuy/ReactTemplate/HEAD/src/static/mstile-150x150.png -------------------------------------------------------------------------------- /src/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeTechGuy/ReactTemplate/HEAD/src/static/apple-touch-icon.png -------------------------------------------------------------------------------- /src/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeTechGuy/ReactTemplate/HEAD/src/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeTechGuy/ReactTemplate/HEAD/src/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require("postcss-preset-env"); 2 | 3 | module.exports = { 4 | plugins: [postcssPresetEnv()], 5 | }; 6 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | printWidth: 120, 3 | tabWidth: 4, 4 | singleQuote: false, 5 | bracketSameLine: false, 6 | trailingComma: "es5", 7 | }; 8 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import { configure } from "@testing-library/react"; 2 | import "@testing-library/jest-dom/jest-globals"; 3 | import "./mocks/mockRandom"; 4 | 5 | configure({ 6 | throwSuggestions: true, 7 | }); 8 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 3 Chrome major versions 2 | last 3 Firefox major versions 3 | last 3 Edge major versions 4 | last 2 Safari major versions 5 | last 2 iOS major versions 6 | last 3 ChromeAndroid major versions 7 | Firefox ESR 8 | -------------------------------------------------------------------------------- /test/transformers/importPathTransformer.cjs: -------------------------------------------------------------------------------- 1 | const path = require("node:path"); 2 | 3 | module.exports = { 4 | process(sourceText, sourcePath) { 5 | return { 6 | code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`, 7 | }; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2d89ef 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/mocks/mockRandom.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, jest } from "@jest/globals"; 2 | import { SeededRandom } from "~/utils/seededRandom"; 3 | 4 | const random = new SeededRandom(0); 5 | 6 | beforeEach(() => { 7 | random.setSeed(123456789); 8 | jest.spyOn(Math, "random").mockImplementation(() => { 9 | return random.random(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/helpers/userEventSetup.ts: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals"; 2 | import { userEvent, type Options, type UserEvent } from "@testing-library/user-event"; 3 | 4 | /** 5 | * Setup user-event instance with custom defaults 6 | */ 7 | export function userEventSetup(options: Options = {}): UserEvent { 8 | return userEvent.setup({ 9 | advanceTimers: jest.advanceTimersByTime.bind(jest), 10 | ...options, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import "~/styles/index.css"; 3 | import { StrictMode } from "react"; 4 | import reactDOMClient from "react-dom/client"; 5 | import { App } from "~/components/App"; 6 | 7 | const rootContainer = document.createElement("div"); 8 | document.body.appendChild(rootContainer); 9 | const root = reactDOMClient.createRoot(rootContainer); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/components/App/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "@jest/globals"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { App } from "./App"; 4 | 5 | describe("App", () => { 6 | test("is rendered", () => { 7 | render(); 8 | screen.getByText("Hello World", { 9 | exact: false, 10 | }); 11 | expect(screen.getByRole("img")).toHaveAttribute("src", "react.svg"); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/teardown.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export default function teardown(): void { 4 | console.log(` 5 | ================================================================================ 6 | View Coverage Report (Open in Browser): ${path.relative( 7 | process.cwd(), 8 | path.join(__dirname, "../coverage/lcov-report/index.html") 9 | )} 10 | ================================================================================ 11 | `); 12 | } 13 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "words": ["autofix", "autofixable", "browserconfig", "corejs", "lcov", "msapplication", "mstile"], 4 | "ignorePaths": ["package.json", "package-lock.json"], 5 | "useGitignore": true, 6 | "validateDirectives": true, 7 | "cache": { 8 | "useCache": true, 9 | "cacheLocation": "./node_modules/.cache/cspell/.cspell" 10 | }, 11 | "language": "en", 12 | "dictionaries": ["html", "typescript", "css", "en-gb", "fonts", "npm"] 13 | } 14 | -------------------------------------------------------------------------------- /src/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png?v=1", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png?v=1", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from "react"; 2 | import { RandomNumber } from "~/components/RandomNumber"; 3 | import { randomDefaults } from "~/config/constants"; 4 | import reactLogo from "./react.svg"; 5 | 6 | export function App(): ReactElement { 7 | return ( 8 |
9 | React Logo 10 |
11 | Hello World 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/random/random.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { random } from "./random"; 3 | 4 | describe("random", () => { 5 | test("randInt generates random integer in range", () => { 6 | expect(random.randInt(10, 20)).toBe(12); 7 | }); 8 | 9 | test("randFloat generates random float in range", () => { 10 | expect(random.randFloat(10, 20)).toBeCloseTo(12.577907438389957); 11 | }); 12 | 13 | test("pick random item from array", () => { 14 | expect(random.pick([1, 2, 3, 4, 5, 6])).toBe(2); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/RandomNumber/RandomNumber.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, type ReactElement } from "react"; 2 | import { random } from "~/utils/random"; 3 | 4 | type RandomNumberProps = { 5 | min: number; 6 | max: number; 7 | }; 8 | 9 | export function RandomNumber(props: RandomNumberProps): ReactElement { 10 | const [number, setNumber] = useState(() => random.randInt(props.min, props.max)); 11 | 12 | const onClick = useCallback(() => { 13 | setNumber(random.randInt(props.min, props.max)); 14 | }, [props.min, props.max]); 15 | 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "target": "ESNext", 6 | "lib": ["ES2024", "DOM", "DOM.Iterable"], 7 | "allowJs": true, 8 | "jsx": "preserve", 9 | "esModuleInterop": true, 10 | "module": "ESNext", 11 | "moduleResolution": "bundler", 12 | "forceConsistentCasingInFileNames": true, 13 | "useUnknownInCatchVariables": true, 14 | "noImplicitOverride": true, 15 | "verbatimModuleSyntax": true, 16 | "paths": { 17 | "test/*": ["test/*"], 18 | "~/*": ["src/*"] 19 | }, 20 | "typeRoots": ["src/types", "node_modules/@types"], 21 | "baseUrl": "." 22 | }, 23 | "include": ["src", "test"] 24 | } 25 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ReactTemplate 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/RandomNumber/RandomNumber.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { userEventSetup } from "test/helpers/userEventSetup"; 4 | import { RandomNumber } from "./RandomNumber"; 5 | 6 | describe("RandomNumber", () => { 7 | test("is generated", () => { 8 | render(); 9 | 10 | screen.getByRole("button", { 11 | name: "13", 12 | }); 13 | }); 14 | 15 | test("is regenerated on mouse click", async () => { 16 | render(); 17 | const user = userEventSetup(); 18 | 19 | await user.click( 20 | screen.getByRole("button", { 21 | name: "2", 22 | }) 23 | ); 24 | screen.getByRole("button", { 25 | name: "10", 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /.ncurc.cjs: -------------------------------------------------------------------------------- 1 | const blockMajorVersion = [ 2 | // Must match the version of Node this package uses 3 | "@types/node", 4 | ]; 5 | 6 | module.exports = { 7 | target: (packageName) => { 8 | if (blockMajorVersion.includes(packageName)) { 9 | return "minor"; 10 | } 11 | return "latest"; 12 | }, 13 | groupFunction: (packageName, defaultGroup) => { 14 | // TypeScript doesn't use SemVer. Both minor and major version changes are major. 15 | if (packageName === "typescript" && defaultGroup === "minor") { 16 | return "major"; 17 | } 18 | // Create custom group for eslint packages since you'll want to review new and changed rules to add. 19 | if (packageName.includes("eslint-plugin") || packageName === "eslint" || packageName.endsWith("/eslint")) { 20 | return "ESLint Packages"; 21 | } 22 | return defaultGroup; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/random/random.ts: -------------------------------------------------------------------------------- 1 | export class Random { 2 | private readonly rng: () => number; 3 | public constructor(rng: () => number) { 4 | this.rng = rng; 5 | } 6 | /** 7 | * Generates a random integer [min, max] 8 | */ 9 | public randInt(min: number, max: number): number { 10 | return Math.floor(this.rng() * (max - min + 1) + min); 11 | } 12 | /** 13 | * Generates a random float [min, max] 14 | */ 15 | public randFloat(min: number, max: number): number { 16 | return this.rng() * (max - min) + min; 17 | } 18 | /** 19 | * Picks a random item from an array 20 | */ 21 | public pick(arr: Item[]): Item { 22 | return arr[this.randInt(0, arr.length - 1)]; 23 | } 24 | /** 25 | * Generates a random float [0, 1) 26 | */ 27 | public random(): number { 28 | return this.rng(); 29 | } 30 | } 31 | 32 | export const random = new Random(() => Math.random()); 33 | -------------------------------------------------------------------------------- /webpack.config.mjs: -------------------------------------------------------------------------------- 1 | import process from "node:process"; 2 | import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; 3 | import { merge as webpackMerge } from "webpack-merge"; 4 | import webpackCommon from "./webpack.common.mjs"; 5 | import webpackDev from "./webpack.dev.mjs"; 6 | import webpackProd from "./webpack.prod.mjs"; 7 | 8 | export default function (env = {}) { 9 | const isProduction = process.env.NODE_ENV === "production"; 10 | 11 | const commonWebpackConfig = webpackCommon(); 12 | 13 | if (env.showBundleAnalysis) { 14 | return webpackMerge(commonWebpackConfig, webpackProd(), { 15 | plugins: [ 16 | new BundleAnalyzerPlugin({ 17 | defaultSizes: "parsed", 18 | openAnalyzer: true, 19 | }), 20 | ], 21 | }); 22 | } 23 | if (isProduction) { 24 | return webpackMerge(commonWebpackConfig, webpackProd()); 25 | } 26 | return webpackMerge(commonWebpackConfig, webpackDev()); 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/seededRandom/seededRandom.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { Random } from "~/utils/random"; 3 | import { SeededRandom } from "./seededRandom"; 4 | 5 | describe("seededRandom", () => { 6 | test("seed creates a deterministic series", () => { 7 | const random = new SeededRandom(100000); 8 | expect(random.random()).toBeCloseTo(0.45954178320243955); 9 | }); 10 | 11 | test("can update seed", () => { 12 | const random = new SeededRandom(100000); 13 | random.setSeed(9999999999); 14 | expect(random.random()).toBeCloseTo(0.8015434315893799); 15 | }); 16 | 17 | test("seeded random updates seed", () => { 18 | const random = new SeededRandom(100000); 19 | expect(random.random()).toBeCloseTo(0.45954178320243955); 20 | expect(random.random()).toBeCloseTo(0.7448947750963271); 21 | }); 22 | 23 | test("is instance of Random", () => { 24 | const random = new SeededRandom(100000); 25 | expect(random).toBeInstanceOf(Random); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/utils/seededRandom/seededRandom.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | // mulberry32 3 | 4 | import { Random } from "~/utils/random"; 5 | 6 | export class SeededRandom extends Random { 7 | private seed: number; 8 | /** 9 | * Set the seed of the random number generator 10 | */ 11 | public constructor(seed: number) { 12 | super((): number => { 13 | return this.next(); 14 | }); 15 | this.seed = seed; 16 | } 17 | /** 18 | * Reset the seed of the random number generator 19 | */ 20 | public setSeed(seed: number): void { 21 | this.seed = seed; 22 | } 23 | /** 24 | * Get the next number in the series - equivalent to Math.random() 25 | */ 26 | private next(): number { 27 | this.seed |= 0; 28 | this.seed = (this.seed + 0x6d2b79f5) | 0; 29 | let t = Math.imul(this.seed ^ (this.seed >>> 15), 1 | this.seed); 30 | // eslint-disable-next-line operator-assignment 31 | t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; 32 | return ((t ^ (t >>> 14)) >>> 0) / 4294967296; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jason O'Neill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | import inspector from "node:inspector"; 2 | const isDebuggerAttached = inspector.url() !== undefined; 3 | 4 | export default { 5 | restoreMocks: true, 6 | collectCoverage: true, 7 | collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"], 8 | coverageDirectory: "coverage", 9 | coverageReporters: ["text-summary", "lcov"], 10 | coverageThreshold: { 11 | global: { 12 | branches: 100, 13 | functions: 100, 14 | lines: 100, 15 | statements: 100, 16 | }, 17 | }, 18 | errorOnDeprecated: true, 19 | moduleNameMapper: { 20 | "\\.(css)$": "identity-obj-proxy", 21 | "test/(.*)$": "/test/$1", 22 | "~/(.*)$": "/src/$1", 23 | }, 24 | transform: { 25 | "\\.[jt]sx?$": "babel-jest", 26 | "\\.(jpg|jpeg|png|gif|webp|svg|bmp|woff|woff2|ttf)$": "/test/transformers/importPathTransformer.cjs", 27 | }, 28 | setupFilesAfterEnv: ["/test/setup.ts"], 29 | globalTeardown: "/test/teardown.ts", 30 | fakeTimers: { 31 | enableGlobally: true, 32 | }, 33 | verbose: true, 34 | testEnvironment: "jsdom", 35 | testTimeout: isDebuggerAttached ? 10000000 : 5000, 36 | randomize: true, 37 | }; 38 | -------------------------------------------------------------------------------- /lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export default { 4 | "*.{js,ts,jsx,tsx,cjs,mjs,json,html,xml,svg,css,md}": "cspell --no-progress --no-must-find-files", 5 | "*.{js,ts,jsx,tsx,cjs,mjs}": "eslint --no-warn-ignored --max-warnings 0 --fix", 6 | "*": "prettier --write --ignore-unknown --log-level warn", 7 | "src/*/**/*.{js,ts,jsx,tsx}": (paths) => { 8 | const relativePaths = paths 9 | .filter((filePath) => !filePath.endsWith(".d.ts")) 10 | .map((filePath) => 11 | path 12 | .relative(import.meta.dirname, filePath) 13 | .split(path.sep) 14 | .join(path.posix.sep) 15 | ); 16 | if (relativePaths.length === 0) { 17 | return []; 18 | } 19 | // Running unit tests is the largest contributor to the speed of a commit. 20 | // If you are running all of your validations in PR before merging then remove this and depend on PR checks. 21 | // This is only useful if you are directly pushing code and not creating PRs. 22 | return `jest --runInBand --bail --collectCoverageFrom '(${relativePaths.join( 23 | "|" 24 | )})' --coverage --findRelatedTests ${paths.join(" ")}`; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/types/staticAssets.d.ts: -------------------------------------------------------------------------------- 1 | // Stylesheet formats 2 | 3 | declare module "*.module.css" { 4 | const classes: Record; 5 | export default classes; 6 | } 7 | 8 | declare module "*.css" { 9 | const content: string; 10 | export default content; 11 | } 12 | 13 | // Image formats 14 | 15 | declare module "*.jpg" { 16 | const content: string; 17 | export default content; 18 | } 19 | 20 | declare module "*.jpeg" { 21 | const content: string; 22 | export default content; 23 | } 24 | 25 | declare module "*.png" { 26 | const content: string; 27 | export default content; 28 | } 29 | 30 | declare module "*.gif" { 31 | const content: string; 32 | export default content; 33 | } 34 | 35 | declare module "*.bmp" { 36 | const content: string; 37 | export default content; 38 | } 39 | 40 | declare module "*.webp" { 41 | const content: string; 42 | export default content; 43 | } 44 | 45 | declare module "*.svg" { 46 | const content: string; 47 | export default content; 48 | } 49 | 50 | // Font formats 51 | 52 | declare module "*.woff" { 53 | const content: string; 54 | export default content; 55 | } 56 | 57 | declare module "*.woff2" { 58 | const content: string; 59 | export default content; 60 | } 61 | 62 | declare module "*.ttf" { 63 | const content: string; 64 | export default content; 65 | } 66 | -------------------------------------------------------------------------------- /webpack.dev.mjs: -------------------------------------------------------------------------------- 1 | // cspell:words pmmmwh 2 | import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin"; 3 | 4 | export default function () { 5 | return { 6 | mode: "development", 7 | plugins: [ 8 | new ReactRefreshWebpackPlugin({ 9 | overlay: false, 10 | }), 11 | ], 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(s?)css$/, 16 | use: [ 17 | "style-loader", 18 | { 19 | loader: "css-loader", 20 | options: { 21 | modules: { 22 | auto: true, 23 | localIdentName: "[local]_[hash:base64:5]", 24 | }, 25 | }, 26 | }, 27 | "postcss-loader", 28 | ], 29 | sideEffects: true, 30 | }, 31 | ], 32 | }, 33 | devServer: { 34 | hot: true, 35 | port: 7579, 36 | static: false, 37 | compress: true, 38 | host: "localhost", 39 | allowedHosts: "all", 40 | client: { 41 | logging: "warn", 42 | overlay: false, 43 | }, 44 | historyApiFallback: true, 45 | }, 46 | watchOptions: { 47 | ignored: /node_modules/, 48 | }, 49 | devtool: "source-map", 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | const packageJson = require("./package.json"); 2 | const allPackages = { 3 | ...packageJson.dependencies, 4 | ...packageJson.devDependencies, 5 | }; 6 | const coreJSVersion = allPackages["core-js"].match(/\d\.\d+/)[0]; 7 | 8 | module.exports = (api) => { 9 | const isTest = api.env("test"); 10 | const isDev = api.env("development"); 11 | return { 12 | plugins: [...(isDev ? ["react-refresh/babel"] : [])], 13 | presets: [ 14 | [ 15 | "@babel/env", 16 | { 17 | bugfixes: true, 18 | useBuiltIns: "usage", 19 | corejs: { 20 | version: coreJSVersion, 21 | proposals: true, 22 | }, 23 | shippedProposals: true, 24 | ...(isTest 25 | ? { 26 | targets: { 27 | node: "current", 28 | }, 29 | } 30 | : {}), 31 | }, 32 | ], 33 | [ 34 | "@babel/react", 35 | { 36 | useBuiltIns: true, 37 | runtime: "automatic", 38 | }, 39 | ], 40 | [ 41 | "@babel/typescript", 42 | { 43 | allowDeclareFields: true, 44 | onlyRemoveTypeImports: true, 45 | }, 46 | ], 47 | ], 48 | retainLines: isTest, 49 | assumptions: { 50 | ignoreFunctionLength: true, 51 | constantReexports: true, 52 | noClassCalls: true, 53 | noDocumentAll: true, 54 | }, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /webpack.prod.mjs: -------------------------------------------------------------------------------- 1 | import CssMinimizerPlugin from "css-minimizer-webpack-plugin"; 2 | import HTMLMinimizerPlugin from "html-minimizer-webpack-plugin"; 3 | import { LicenseWebpackPlugin } from "license-webpack-plugin"; 4 | import MiniCssExtractPlugin from "mini-css-extract-plugin"; 5 | import TerserPlugin from "terser-webpack-plugin"; 6 | import packageJson from "./package.json" with { type: "json" }; 7 | 8 | const ACCEPTABLE_LICENSES = ["MIT", "0BSD", "BSD-2-Clause", "BSD-3-Clause", "APACHE-2.0", "ISC", "Unlicense"]; 9 | 10 | export default function () { 11 | return { 12 | mode: "production", 13 | plugins: [ 14 | new MiniCssExtractPlugin({ 15 | filename: "[name].[contenthash].css", 16 | }), 17 | new LicenseWebpackPlugin({ 18 | outputFilename: "third-party-licenses.txt", 19 | unacceptableLicenseTest: (licenseIdentifier) => { 20 | const licenses = licenseIdentifier.replace(/[()]/g, "").split(" OR "); 21 | return licenses.every((licenseName) => { 22 | return !ACCEPTABLE_LICENSES.map((name) => name.toLowerCase()).includes( 23 | licenseName.toLowerCase() 24 | ); 25 | }); 26 | }, 27 | perChunkOutput: false, 28 | skipChildCompilers: true, 29 | excludedPackageTest: (packageName) => { 30 | return packageName === packageJson.name; 31 | }, 32 | }), 33 | ], 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.(s?)css$/, 38 | use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"], 39 | sideEffects: true, 40 | }, 41 | ], 42 | }, 43 | optimization: { 44 | minimizer: [ 45 | new TerserPlugin({ 46 | extractComments: false, 47 | terserOptions: { 48 | format: { 49 | comments: false, 50 | preamble: "/* See third-party-licenses.txt for licenses of any bundled software. */", 51 | }, 52 | }, 53 | }), 54 | new CssMinimizerPlugin(), 55 | new HTMLMinimizerPlugin(), 56 | ], 57 | splitChunks: { 58 | chunks: "all", 59 | }, 60 | }, 61 | devtool: false, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /webpack.common.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import CopyWebpackPlugin from "copy-webpack-plugin"; 3 | import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; 4 | import HtmlWebpackPlugin from "html-webpack-plugin"; 5 | import svgToMiniDataURI from "mini-svg-data-uri"; 6 | 7 | export default function () { 8 | return { 9 | context: path.resolve("./src"), 10 | entry: ["./index"], 11 | output: { 12 | path: path.resolve("./dist"), 13 | filename: "[name].[contenthash].js", 14 | assetModuleFilename: "[name].[contenthash][ext]", 15 | publicPath: "/", 16 | hashDigestLength: 10, 17 | }, 18 | plugins: [ 19 | new ForkTsCheckerWebpackPlugin({ 20 | typescript: { 21 | configFile: path.resolve("./tsconfig.json"), 22 | configOverwrite: { 23 | compilerOptions: { 24 | skipLibCheck: true, 25 | sourceMap: false, 26 | inlineSourceMap: false, 27 | declarationMap: false, 28 | }, 29 | exclude: ["**/*.test.js", "**/*.test.jsx", "**/*.test.ts", "**/*.test.tsx", "test/**"], 30 | }, 31 | }, 32 | }), 33 | new HtmlWebpackPlugin({ 34 | template: "./index.html", 35 | }), 36 | new CopyWebpackPlugin({ 37 | patterns: [{ from: "static", noErrorOnMissing: true }], 38 | }), 39 | ], 40 | resolve: { 41 | alias: { 42 | "~": path.resolve("./src"), 43 | }, 44 | extensionAlias: { 45 | ".mjs": [".mts", ".mjs"], 46 | }, 47 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json"], 48 | }, 49 | node: false, 50 | module: { 51 | rules: [ 52 | { 53 | test: /\.(j|t)s(x?)$/, 54 | exclude: /node_modules/, 55 | use: ["babel-loader"], 56 | }, 57 | { 58 | test: /\.(woff2|woff|ttf|png|jpg|jpeg|gif|bmp|webp)$/, 59 | type: "asset", 60 | }, 61 | { 62 | test: /\.svg$/, 63 | type: "asset", 64 | generator: { 65 | dataUrl: (content) => svgToMiniDataURI(content.toString()), 66 | }, 67 | }, 68 | ], 69 | }, 70 | stats: "errors-warnings", 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-template", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "", 6 | "scripts": { 7 | "start": "webpack serve", 8 | "release": "npm run verify && npm run build", 9 | "verify": "npm run lint && npm run format:check && npm run ts-check && npm run spellcheck && npm run test", 10 | "build": "npm run build:production", 11 | "build:production": "npm run clean:dist && webpack --node-env production", 12 | "build:development": "npm run clean:dist && webpack --node-env development", 13 | "bundle-analysis": "webpack --node-env production --env showBundleAnalysis", 14 | "autofix": "npm run lint:fix && npm run format", 15 | "lint": "eslint --max-warnings 0", 16 | "lint:fix": "npm run lint -- --fix", 17 | "test": "jest --runInBand", 18 | "ts-check": "tsc", 19 | "format": "prettier . --write --cache --log-level warn", 20 | "format:check": "prettier . --check --cache --log-level warn", 21 | "spellcheck": "cspell --no-progress --dot \"**/*.{js,ts,jsx,tsx,cjs,mjs,json,html,xml,svg,css,md}\"", 22 | "list-outdated-dependencies": "npm-check-updates --format repo,group --peer --cooldown 1", 23 | "update-dependencies": "npm run list-outdated-dependencies -- -u && npm install && npm update && npm dedupe && npm run autofix && npm run release", 24 | "clean": "rimraf node_modules coverage dist", 25 | "clean:dist": "rimraf dist", 26 | "prepare": "husky" 27 | }, 28 | "dependencies": { 29 | "react": "^19.2.0", 30 | "react-dom": "^19.2.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.28.5", 34 | "@babel/preset-env": "^7.28.5", 35 | "@babel/preset-react": "^7.28.5", 36 | "@babel/preset-typescript": "^7.28.5", 37 | "@jest/globals": "^30.2.0", 38 | "@pmmmwh/react-refresh-webpack-plugin": "^0.6.1", 39 | "@testing-library/dom": "^10.4.1", 40 | "@testing-library/jest-dom": "^6.9.1", 41 | "@testing-library/react": "^16.3.0", 42 | "@testing-library/user-event": "^14.6.1", 43 | "@types/node": "^22.18.12", 44 | "@types/react": "^19.2.2", 45 | "@types/react-dom": "^19.2.2", 46 | "@typescript-eslint/eslint-plugin": "^8.46.2", 47 | "@typescript-eslint/parser": "^8.46.2", 48 | "babel-loader": "^10.0.0", 49 | "copy-webpack-plugin": "^13.0.1", 50 | "core-js": "^3.46.0", 51 | "cspell": "^9.2.2", 52 | "css-loader": "^7.1.2", 53 | "css-minimizer-webpack-plugin": "^7.0.2", 54 | "eslint": "^9.38.0", 55 | "eslint-plugin-import": "^2.32.0", 56 | "eslint-plugin-jest": "^29.0.1", 57 | "eslint-plugin-jsx-a11y": "^6.10.2", 58 | "eslint-plugin-react": "^7.37.5", 59 | "eslint-plugin-react-hooks": "^7.0.1", 60 | "eslint-plugin-testing-library": "^7.13.3", 61 | "fork-ts-checker-webpack-plugin": "^9.1.0", 62 | "globals": "^16.4.0", 63 | "html-minimizer-webpack-plugin": "^5.0.3", 64 | "html-webpack-plugin": "^5.6.4", 65 | "husky": "^9.1.7", 66 | "identity-obj-proxy": "^3.0.0", 67 | "jest": "^30.2.0", 68 | "jest-environment-jsdom": "^30.2.0", 69 | "license-webpack-plugin": "^4.0.2", 70 | "lint-staged": "^16.2.6", 71 | "mini-css-extract-plugin": "^2.9.4", 72 | "mini-svg-data-uri": "^1.4.4", 73 | "npm-check-updates": "^19.1.1", 74 | "postcss": "^8.5.6", 75 | "postcss-loader": "^8.2.0", 76 | "postcss-preset-env": "^10.4.0", 77 | "prettier": "^3.6.2", 78 | "react-refresh": "^0.18.0", 79 | "rimraf": "^6.0.1", 80 | "style-loader": "^4.0.0", 81 | "typescript": "^5.9.3", 82 | "webpack": "^5.102.1", 83 | "webpack-bundle-analyzer": "^4.10.2", 84 | "webpack-cli": "^6.0.1", 85 | "webpack-dev-server": "^5.2.2" 86 | }, 87 | "engines": { 88 | "node": ">=22.12.0" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Template 2 | 3 | This repository exists as a starting point for a new React 19 application (with hooks). The build system, testing, linting, formatting, compiling, spellchecking and more are all pre-configured. 4 | 5 | This repository should be generic enough that most people can use it out of the box. It comes with an existing "hello world" application which you can build and run right away. 6 | 7 | It also includes all of the nice-to-haves to ensure that you code is high quality and follows best practices. This is very helpful for a beginner who needs nudges in the right direction but also helps an expert focus on the higher level problems and not worry about missing smaller errors. 8 | 9 | ## Setup 10 | 11 | - Be sure you have [the current LTS version of Node.js installed](https://nodejs.org/) 12 | - If you are on Windows, you probably want to be using either [GitBash which comes with Git](https://git-scm.com/download/win) or [WSL](https://docs.microsoft.com/en-us/windows/wsl/install). 13 | - Run `npm ci` to install dependencies 14 | - Run `npm run start` to start the dev server and visit the link provided in the terminal to view it in your browser 15 | 16 | ## Core Dependencies Included 17 | 18 | - [React](https://react.dev/learn) (JavaScript UI framework) 19 | - [Webpack](https://webpack.js.org/) (Asset bundling) 20 | - [TypeScript](https://www.typescriptlang.org/docs/handbook/intro.html) (JavaScript with Types) 21 | - [Babel](https://babeljs.io/docs/en/) (Transpiling JavaScript for older browsers) 22 | - [ESLint](https://eslint.org/) (Identifying and reporting errors in code) 23 | - [Prettier](https://prettier.io/docs/en/index.html) (Code formatter) 24 | - [CSpell](https://github.com/streetsidesoftware/cspell) (Code Spellchecker) 25 | - [Jest](https://jestjs.io/docs/en/getting-started) (Unit test framework) 26 | - [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) (React unit test utilities) 27 | - [Husky](https://typicode.github.io/husky) (Git hooks - run commands on commit) 28 | 29 | ## NPM scripts 30 | 31 | - `npm clean-install` - install all dependencies. _Do this first before anything else_ 32 | - `npm run start` - starts a local server which can be accessed at http://localhost:7579. As long as this server is running it'll auto refresh whenever you save changes. 33 | - `npm run release` - creates a release build of your application. All output files will be located in the dist folder. This also runs all of the checks to ensure the code works, is formatted, etc. 34 | - `npm run verify` - checks the application without building 35 | - `npm run bundle-analysis` - opens a bundle analysis UI showing the file size of each dependency in your output JavaScript bundle. 36 | - `npm run lint` - runs ESLint enforcing good coding style and habits and erroring if any are broken. 37 | - `npm run lint:fix` - fixes any auto-fixable ESLint errors 38 | - `npm run format` - runs Prettier to reformat all files 39 | - `npm run autofix` - fix all autofixable issues 40 | - `npm run ts-check` - runs the TypeScript compiler to see TypeScript errors 41 | - `npm run spellcheck` - runs CSpell to see any typos. If the word is misidentified, add it to `cspell.json`. 42 | - `npm run test` - runs Jest and all unit tests 43 | - `npm run clean` - removes all auto-generated files and dependencies 44 | - `npm run list-outdated-dependencies` - lists the dependencies that have newer versions available with links to their repository and changelog 45 | - `npm run update-dependencies` - update and install all outdated dependencies 46 | 47 | ## Why use this instead of XYZ? 48 | 49 | Tools that are designed for mass adoption as a quick start guide bring everything and the kitchen sink when setting up a new project. They are great to start quickly, but as soon as you want to customize or understand how it all works you'll have trouble. My goal is to expose all of the tools and show how easy it can be to configure from scratch. This makes it easier to debug and tweak settings to fit your needs. Nothing is abstracted into custom dependencies and any features which are included in this template can be easily removed/changed if you don't need them. 50 | -------------------------------------------------------------------------------- /src/components/App/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // cspell:words setstate, eqeqeq, backreference, isnan, nonoctal, textnodes, nonconstructor, typedefs 2 | 3 | import typescriptPlugin from "@typescript-eslint/eslint-plugin"; 4 | import typescriptEsLintParser from "@typescript-eslint/parser"; 5 | import importPlugin from "eslint-plugin-import"; 6 | import jestPlugin from "eslint-plugin-jest"; 7 | import jsxA11yPlugin from "eslint-plugin-jsx-a11y"; 8 | import reactPlugin from "eslint-plugin-react"; 9 | import reactHooksPlugin from "eslint-plugin-react-hooks"; 10 | import testingLibraryPlugin from "eslint-plugin-testing-library"; 11 | import globals from "globals"; 12 | 13 | /* 14 | Rules in this file are in the same order as they appear in the docs sites to make it easy to find. (Usually this is alphabetical but sometimes there's subgroups.) 15 | ESLint Rule Documentation Sites: 16 | https://eslint.org/docs/latest/rules/ 17 | https://github.com/jsx-eslint/eslint-plugin-react 18 | https://github.com/import-js/eslint-plugin-import 19 | https://github.com/testing-library/eslint-plugin-testing-library 20 | https://github.com/jest-community/eslint-plugin-jest 21 | https://typescript-eslint.io/rules/ 22 | https://github.com/jsx-eslint/eslint-plugin-jsx-a11y 23 | */ 24 | 25 | const baseRestrictedImports = { 26 | patterns: [ 27 | { 28 | group: ["../*"], 29 | message: "Usage of relative parent imports is not allowed.", 30 | }, 31 | ], 32 | paths: [ 33 | { 34 | name: ".", 35 | message: "Usage of local index imports is not allowed.", 36 | }, 37 | { 38 | name: "./index", 39 | message: "Import from the source file instead.", 40 | }, 41 | ], 42 | }; 43 | 44 | export default [ 45 | { 46 | linterOptions: { 47 | reportUnusedDisableDirectives: "warn", 48 | reportUnusedInlineConfigs: "warn", 49 | }, 50 | languageOptions: { 51 | parser: typescriptEsLintParser, 52 | sourceType: "module", 53 | globals: { 54 | ...globals.es2025, 55 | ...globals.browser, 56 | }, 57 | }, 58 | }, 59 | { 60 | ignores: ["**/dist/**", "**/coverage/**"], 61 | }, 62 | { 63 | files: ["**/*.js", "**/*.cjs", "**/*.mjs", "**/*.jsx", "**/*.ts", "**/*.cts", "**/*.mts", "**/*.tsx"], 64 | plugins: { 65 | import: importPlugin, 66 | react: reactPlugin, 67 | "react-hooks": reactHooksPlugin, 68 | }, 69 | rules: { 70 | // Possible Problems - https://eslint.org/docs/latest/rules/#possible-problems 71 | "array-callback-return": [ 72 | "error", 73 | { 74 | checkForEach: true, 75 | }, 76 | ], 77 | "constructor-super": "error", 78 | "for-direction": "error", 79 | "getter-return": "error", 80 | "no-async-promise-executor": "error", 81 | "no-class-assign": "error", 82 | "no-compare-neg-zero": "error", 83 | "no-cond-assign": ["error", "always"], 84 | "no-const-assign": "error", 85 | "no-constant-binary-expression": "warn", 86 | "no-constant-condition": "error", 87 | "no-constructor-return": "error", 88 | "no-control-regex": "error", 89 | "no-debugger": "warn", 90 | "no-dupe-args": "error", 91 | "no-dupe-class-members": "error", 92 | "no-dupe-else-if": "error", 93 | "no-dupe-keys": "error", 94 | "no-duplicate-case": "error", 95 | "no-empty-character-class": "error", 96 | "no-empty-pattern": "error", 97 | "no-ex-assign": "error", 98 | "no-fallthrough": "error", 99 | "no-func-assign": "error", 100 | "no-import-assign": "error", 101 | "no-inner-declarations": ["error", "both"], 102 | "no-invalid-regexp": "error", 103 | "no-irregular-whitespace": [ 104 | "error", 105 | { 106 | skipStrings: false, 107 | skipTemplates: false, 108 | skipJSXText: false, 109 | }, 110 | ], 111 | "no-loss-of-precision": "error", 112 | "no-misleading-character-class": "error", 113 | "no-new-native-nonconstructor": "error", 114 | "no-obj-calls": "error", 115 | "no-prototype-builtins": "error", 116 | "no-self-assign": "warn", 117 | "no-self-compare": "warn", 118 | "no-setter-return": "error", 119 | "no-sparse-arrays": "error", 120 | "no-template-curly-in-string": "error", 121 | "no-this-before-super": "error", 122 | "no-undef": "error", 123 | "no-unexpected-multiline": "error", 124 | "no-unmodified-loop-condition": "error", 125 | "no-unreachable": "warn", 126 | "no-unsafe-finally": "error", 127 | "no-unsafe-negation": [ 128 | "error", 129 | { 130 | enforceForOrderingRelations: true, 131 | }, 132 | ], 133 | "no-unsafe-optional-chaining": [ 134 | "error", 135 | { 136 | disallowArithmeticOperators: true, 137 | }, 138 | ], 139 | "no-unused-private-class-members": "warn", 140 | "no-unused-vars": [ 141 | "warn", 142 | { 143 | varsIgnorePattern: "^_", 144 | argsIgnorePattern: "^_", 145 | reportUsedIgnorePattern: true, 146 | }, 147 | ], 148 | "no-use-before-define": [ 149 | "warn", 150 | { 151 | functions: false, 152 | classes: false, 153 | variables: true, 154 | allowNamedExports: false, 155 | }, 156 | ], 157 | "no-useless-backreference": "error", 158 | "use-isnan": "error", 159 | "valid-typeof": "error", 160 | // Suggestions - https://eslint.org/docs/latest/rules/#suggestions 161 | "consistent-return": "error", 162 | curly: "warn", 163 | "default-param-last": "error", 164 | eqeqeq: "error", 165 | "func-names": ["warn", "never"], 166 | "func-style": ["warn", "declaration"], 167 | "no-array-constructor": "error", 168 | "no-bitwise": "error", 169 | "no-case-declarations": "error", 170 | "no-delete-var": "error", 171 | "no-else-return": "warn", 172 | "no-empty": "warn", 173 | "no-empty-function": "warn", 174 | "no-empty-static-block": "warn", 175 | "no-eval": "error", 176 | "no-extend-native": "error", 177 | "no-extra-bind": "error", 178 | "no-extra-boolean-cast": [ 179 | "warn", 180 | { 181 | enforceForLogicalOperands: true, 182 | }, 183 | ], 184 | "no-global-assign": "error", 185 | "no-implicit-coercion": "error", 186 | "no-implicit-globals": "error", 187 | "no-implied-eval": "error", 188 | "no-invalid-this": [ 189 | "error", 190 | { 191 | capIsConstructor: false, 192 | }, 193 | ], 194 | "no-labels": "error", 195 | "no-lone-blocks": "error", 196 | "no-multi-assign": "warn", 197 | "no-new": "error", 198 | "no-new-func": "error", 199 | "no-new-wrappers": "error", 200 | "no-nonoctal-decimal-escape": "error", 201 | "no-object-constructor": "error", 202 | "no-octal": "error", 203 | "no-octal-escape": "error", 204 | "no-proto": "error", 205 | "no-redeclare": "error", 206 | "no-regex-spaces": "warn", 207 | "no-restricted-imports": ["warn", baseRestrictedImports], 208 | "no-restricted-syntax": [ 209 | "warn", 210 | { 211 | selector: "CallExpression[callee.name='Number']", 212 | message: "Don't use the Number function. Use parseInt or parseFloat instead.", 213 | }, 214 | { 215 | selector: "CallExpression[callee.name='Boolean']", 216 | message: "Don't use the Boolean function. Use a strict comparison instead.", 217 | }, 218 | { 219 | selector: "TSEnumDeclaration", 220 | message: "Use a type with a union of strings instead.", 221 | }, 222 | { 223 | selector: "TSTypeReference Identifier[name='React']", 224 | message: "Import the type explicitly instead of using the React global.", 225 | }, 226 | { 227 | selector: "TSTypeReference Identifier[name='PropsWithChildren']", 228 | message: "Explicitly declare children in your props type.", 229 | }, 230 | ], 231 | "no-return-assign": ["warn", "always"], 232 | "no-script-url": "error", 233 | "no-sequences": [ 234 | "warn", 235 | { 236 | allowInParentheses: false, 237 | }, 238 | ], 239 | "no-shadow": [ 240 | "error", 241 | { 242 | ignoreOnInitialization: true, 243 | }, 244 | ], 245 | "no-shadow-restricted-names": "error", 246 | "no-throw-literal": "error", 247 | "no-unused-expressions": [ 248 | "warn", 249 | { 250 | enforceForJSX: true, 251 | }, 252 | ], 253 | "no-useless-call": "error", 254 | "no-useless-catch": "warn", 255 | "no-useless-computed-key": [ 256 | "warn", 257 | { 258 | enforceForClassMembers: true, 259 | }, 260 | ], 261 | "no-useless-concat": "error", 262 | "no-useless-escape": "warn", 263 | "no-useless-rename": "warn", 264 | "no-useless-return": "warn", 265 | "no-var": "error", 266 | "no-with": "error", 267 | "one-var": ["warn", "never"], 268 | "operator-assignment": "warn", 269 | "prefer-arrow-callback": "warn", 270 | "prefer-const": "warn", 271 | "prefer-numeric-literals": "warn", 272 | "prefer-object-spread": "warn", 273 | "prefer-promise-reject-errors": "error", 274 | "prefer-rest-params": "warn", 275 | "prefer-spread": "warn", 276 | "prefer-template": "warn", 277 | radix: "error", 278 | "require-await": "error", 279 | "require-yield": "error", 280 | // Layout & Formatting - https://eslint.org/docs/latest/rules/#layout--formatting 281 | // ---- Nothing in this category. Defer to Prettier. ---- 282 | // React Hooks - https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks 283 | ...reactHooksPlugin.configs.flat.recommended.rules, 284 | // React - https://github.com/jsx-eslint/eslint-plugin-react 285 | "react/jsx-filename-extension": [ 286 | "error", 287 | { 288 | extensions: [".jsx", ".tsx"], 289 | allow: "as-needed", 290 | }, 291 | ], 292 | // Import - https://github.com/import-js/eslint-plugin-import 293 | "import/enforce-node-protocol-usage": ["warn", "always"], 294 | "import/no-duplicates": ["warn", { "prefer-inline": true }], 295 | "import/no-namespace": "warn", 296 | "import/order": [ 297 | "warn", 298 | { 299 | groups: ["builtin", "external", "parent", ["sibling", "internal", "index"]], 300 | pathGroups: [ 301 | { 302 | pattern: "~/**", 303 | group: "parent", 304 | }, 305 | { 306 | pattern: "test/**", 307 | group: "parent", 308 | }, 309 | ], 310 | alphabetize: { 311 | order: "asc", 312 | orderImportKind: "desc", 313 | caseInsensitive: true, 314 | }, 315 | "newlines-between": "never", 316 | }, 317 | ], 318 | }, 319 | }, 320 | { 321 | // Run the JS rules above on these file types in node environment 322 | files: ["**/*.cjs", "**/*.mjs"], 323 | languageOptions: { 324 | globals: { 325 | ...globals.node, 326 | }, 327 | }, 328 | }, 329 | { 330 | languageOptions: { 331 | parserOptions: { 332 | project: "./tsconfig.json", 333 | tsconfigRootDir: import.meta.dirname, 334 | }, 335 | }, 336 | files: ["**/*.ts", "**/*.cts", "**/*.mts", "**/*.tsx"], 337 | plugins: { 338 | "@typescript-eslint": typescriptPlugin, 339 | import: importPlugin, 340 | }, 341 | rules: { 342 | // TypeScript ESLint Core Disables - https://typescript-eslint.io/docs/linting/configs#eslint-recommended 343 | "constructor-super": "off", 344 | "getter-return": "off", 345 | "no-class-assign": "off", 346 | "no-const-assign": "off", 347 | "no-dupe-args": "off", 348 | "no-dupe-keys": "off", 349 | "no-func-assign": "off", 350 | "no-import-assign": "off", 351 | "no-new-native-nonconstructor": "off", 352 | "no-obj-calls": "off", 353 | "no-setter-return": "off", 354 | "no-this-before-super": "off", 355 | "no-undef": "off", 356 | "no-unreachable": "off", 357 | "no-unsafe-negation": "off", 358 | "valid-typeof": "off", 359 | // TypeScript - https://typescript-eslint.io/rules/ 360 | "@typescript-eslint/adjacent-overload-signatures": "error", 361 | "@typescript-eslint/array-type": "warn", 362 | "@typescript-eslint/await-thenable": "error", 363 | "@typescript-eslint/ban-ts-comment": "error", 364 | "@typescript-eslint/consistent-generic-constructors": ["warn", "constructor"], 365 | "@typescript-eslint/consistent-type-assertions": [ 366 | "warn", 367 | { 368 | assertionStyle: "as", 369 | objectLiteralTypeAssertions: "allow-as-parameter", 370 | }, 371 | ], 372 | "@typescript-eslint/consistent-type-definitions": ["warn", "type"], 373 | "@typescript-eslint/consistent-type-exports": "error", 374 | "@typescript-eslint/consistent-type-imports": [ 375 | "warn", 376 | { 377 | fixStyle: "inline-type-imports", 378 | }, 379 | ], 380 | "@typescript-eslint/explicit-function-return-type": [ 381 | "error", 382 | { 383 | allowTypedFunctionExpressions: true, 384 | }, 385 | ], 386 | "@typescript-eslint/explicit-member-accessibility": "warn", 387 | "@typescript-eslint/method-signature-style": "warn", 388 | "@typescript-eslint/naming-convention": [ 389 | "warn", 390 | { 391 | selector: [ 392 | "classProperty", 393 | "objectLiteralProperty", 394 | "typeProperty", 395 | "classMethod", 396 | "objectLiteralMethod", 397 | "typeMethod", 398 | "accessor", 399 | "enumMember", 400 | ], 401 | format: null, 402 | modifiers: ["requiresQuotes"], 403 | }, 404 | { 405 | selector: "default", 406 | format: ["camelCase"], 407 | }, 408 | { 409 | selector: "import", 410 | format: ["camelCase", "PascalCase"], 411 | }, 412 | { 413 | selector: ["function", "method", "enumMember", "property"], 414 | format: ["camelCase", "PascalCase"], 415 | }, 416 | { 417 | selector: "parameter", 418 | format: ["camelCase"], 419 | leadingUnderscore: "allow", 420 | }, 421 | { 422 | selector: "variable", 423 | modifiers: ["const"], 424 | format: ["camelCase", "PascalCase", "UPPER_CASE"], 425 | leadingUnderscore: "allow", 426 | }, 427 | { 428 | selector: "typeLike", 429 | format: ["PascalCase"], 430 | }, 431 | { 432 | selector: "typeProperty", 433 | format: ["camelCase", "PascalCase", "UPPER_CASE"], 434 | }, 435 | ], 436 | "@typescript-eslint/no-array-delete": "error", 437 | "@typescript-eslint/no-base-to-string": [ 438 | "error", 439 | { 440 | checkUnknown: true, 441 | }, 442 | ], 443 | "@typescript-eslint/no-confusing-non-null-assertion": "error", 444 | "@typescript-eslint/no-confusing-void-expression": [ 445 | "error", 446 | { 447 | ignoreVoidReturningFunctions: true, 448 | }, 449 | ], 450 | "@typescript-eslint/no-deprecated": "warn", 451 | "@typescript-eslint/no-duplicate-type-constituents": "warn", 452 | "@typescript-eslint/no-empty-interface": "warn", 453 | "@typescript-eslint/no-empty-object-type": "warn", 454 | "@typescript-eslint/no-explicit-any": "warn", 455 | "@typescript-eslint/no-extra-non-null-assertion": "error", 456 | "@typescript-eslint/no-extraneous-class": "error", 457 | "@typescript-eslint/no-floating-promises": "error", 458 | "@typescript-eslint/no-for-in-array": "error", 459 | "@typescript-eslint/no-inferrable-types": "warn", 460 | "@typescript-eslint/no-invalid-void-type": "error", 461 | "@typescript-eslint/no-meaningless-void-operator": "warn", 462 | "@typescript-eslint/no-misused-new": "error", 463 | "@typescript-eslint/no-misused-promises": "error", 464 | "@typescript-eslint/no-misused-spread": "error", 465 | "@typescript-eslint/no-namespace": "warn", 466 | "@typescript-eslint/no-non-null-asserted-nullish-coalescing": "warn", 467 | "@typescript-eslint/no-non-null-asserted-optional-chain": "error", 468 | "@typescript-eslint/no-redundant-type-constituents": "warn", 469 | "@typescript-eslint/no-require-imports": "error", 470 | "@typescript-eslint/no-this-alias": "warn", 471 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn", 472 | "@typescript-eslint/no-unnecessary-condition": [ 473 | "warn", 474 | { 475 | checkTypePredicates: true, 476 | }, 477 | ], 478 | "@typescript-eslint/no-unnecessary-qualifier": "warn", 479 | "@typescript-eslint/no-unnecessary-template-expression": "warn", 480 | "@typescript-eslint/no-unnecessary-type-arguments": "warn", 481 | "@typescript-eslint/no-unnecessary-type-assertion": "warn", 482 | "@typescript-eslint/no-unnecessary-type-constraint": "warn", 483 | "@typescript-eslint/no-unnecessary-type-conversion": "warn", 484 | "@typescript-eslint/no-unsafe-argument": "error", 485 | "@typescript-eslint/no-unsafe-assignment": "error", 486 | "@typescript-eslint/no-unsafe-call": "error", 487 | "@typescript-eslint/no-unsafe-enum-comparison": "warn", 488 | "@typescript-eslint/no-unsafe-function-type": "error", 489 | "@typescript-eslint/no-unsafe-declaration-merging": "error", 490 | "@typescript-eslint/no-unsafe-member-access": "error", 491 | "@typescript-eslint/no-unsafe-return": "error", 492 | "@typescript-eslint/no-unsafe-unary-minus": "error", 493 | "@typescript-eslint/no-useless-empty-export": "warn", 494 | "@typescript-eslint/no-var-requires": "error", 495 | "@typescript-eslint/no-wrapper-object-types": "error", 496 | "@typescript-eslint/non-nullable-type-assertion-style": "warn", 497 | "@typescript-eslint/parameter-properties": "error", 498 | "@typescript-eslint/prefer-as-const": "warn", 499 | "@typescript-eslint/prefer-find": "warn", 500 | "@typescript-eslint/prefer-for-of": "warn", 501 | "@typescript-eslint/prefer-includes": "warn", 502 | "@typescript-eslint/prefer-namespace-keyword": "warn", 503 | "@typescript-eslint/prefer-nullish-coalescing": [ 504 | "warn", 505 | { 506 | ignoreTernaryTests: false, 507 | }, 508 | ], 509 | "@typescript-eslint/prefer-optional-chain": "warn", 510 | "@typescript-eslint/prefer-readonly": "warn", 511 | "@typescript-eslint/prefer-reduce-type-parameter": "warn", 512 | "@typescript-eslint/prefer-return-this-type": "error", 513 | "@typescript-eslint/prefer-string-starts-ends-with": "warn", 514 | "@typescript-eslint/prefer-ts-expect-error": "warn", 515 | "@typescript-eslint/related-getter-setter-pairs": "error", 516 | "@typescript-eslint/require-array-sort-compare": "error", 517 | "@typescript-eslint/restrict-plus-operands": "error", 518 | "@typescript-eslint/restrict-template-expressions": "error", 519 | "@typescript-eslint/strict-boolean-expressions": [ 520 | "error", 521 | { 522 | allowString: false, 523 | allowNumber: false, 524 | allowNullableObject: false, 525 | allowNullableEnum: false, 526 | }, 527 | ], 528 | "@typescript-eslint/switch-exhaustiveness-check": "error", 529 | "@typescript-eslint/triple-slash-reference": "warn", 530 | "@typescript-eslint/unbound-method": "error", 531 | "@typescript-eslint/unified-signatures": "warn", 532 | "@typescript-eslint/use-unknown-in-catch-callback-variable": "warn", 533 | // TypeScript Extension Rules - https://typescript-eslint.io/rules/#extension-rules 534 | "consistent-return": "off", 535 | "@typescript-eslint/consistent-return": "error", 536 | "default-param-last": "off", 537 | "@typescript-eslint/default-param-last": "error", 538 | "prefer-promise-reject-errors": "off", 539 | "@typescript-eslint/prefer-promise-reject-errors": "error", 540 | "no-array-constructor": "off", 541 | "@typescript-eslint/no-array-constructor": "error", 542 | "no-dupe-class-members": "off", 543 | "no-empty-function": "off", 544 | "@typescript-eslint/no-empty-function": "warn", 545 | "no-implied-eval": "off", 546 | "@typescript-eslint/no-implied-eval": "error", 547 | "no-invalid-this": "off", 548 | "no-redeclare": "off", 549 | "@typescript-eslint/no-redeclare": [ 550 | "error", 551 | { 552 | ignoreDeclarationMerge: false, 553 | }, 554 | ], 555 | "no-shadow": "off", 556 | "@typescript-eslint/no-shadow": [ 557 | "error", 558 | { 559 | ignoreOnInitialization: true, 560 | }, 561 | ], 562 | "no-throw-literal": "off", 563 | "@typescript-eslint/only-throw-error": [ 564 | "error", 565 | { 566 | allowThrowingAny: false, 567 | allowThrowingUnknown: false, 568 | }, 569 | ], 570 | "no-unused-expressions": "off", 571 | "@typescript-eslint/no-unused-expressions": [ 572 | "warn", 573 | { 574 | enforceForJSX: true, 575 | }, 576 | ], 577 | "no-unused-vars": "off", 578 | "@typescript-eslint/no-unused-vars": [ 579 | "warn", 580 | { 581 | varsIgnorePattern: "^_", 582 | argsIgnorePattern: "^_", 583 | reportUsedIgnorePattern: true, 584 | }, 585 | ], 586 | "no-use-before-define": "off", 587 | "@typescript-eslint/no-use-before-define": [ 588 | "warn", 589 | { 590 | functions: false, 591 | classes: false, 592 | variables: true, 593 | allowNamedExports: false, 594 | // TS extension options 595 | enums: true, 596 | typedefs: true, 597 | ignoreTypeReferences: true, 598 | }, 599 | ], 600 | "require-await": "off", 601 | "@typescript-eslint/require-await": "error", 602 | }, 603 | }, 604 | { 605 | files: ["**/*.jsx", "**/*.tsx"], 606 | ignores: ["**/*.test.jsx", "**/*.test.tsx"], 607 | plugins: { "jsx-a11y": jsxA11yPlugin }, 608 | rules: { 609 | // JSX A11y - This plugin is being extended because there's an extensive amount of custom options automatically configured. - https://github.com/jsx-eslint/eslint-plugin-jsx-a11y 610 | ...jsxA11yPlugin.flatConfigs.recommended.rules, 611 | }, 612 | }, 613 | { 614 | settings: { 615 | react: { 616 | version: "detect", 617 | }, 618 | }, 619 | files: ["**/*.jsx", "**/*.js", "**/*.tsx", "**/*.ts"], 620 | plugins: { 621 | react: reactPlugin, 622 | "jsx-a11y": jsxA11yPlugin, 623 | }, 624 | rules: { 625 | // React - https://github.com/jsx-eslint/eslint-plugin-react#list-of-supported-rules 626 | "react/checked-requires-onchange-or-readonly": "error", 627 | "react/function-component-definition": [ 628 | "warn", 629 | { 630 | unnamedComponents: "arrow-function", 631 | }, 632 | ], 633 | "react/hook-use-state": "warn", 634 | "react/iframe-missing-sandbox": "warn", 635 | "react/no-access-state-in-setstate": "error", 636 | "react/no-array-index-key": "error", 637 | "react/no-children-prop": "error", 638 | "react/no-danger": "error", 639 | "react/no-danger-with-children": "error", 640 | "react/no-deprecated": "error", 641 | "react/no-did-mount-set-state": "error", 642 | "react/no-did-update-set-state": "error", 643 | "react/no-direct-mutation-state": "error", 644 | "react/no-find-dom-node": "error", 645 | "react/no-invalid-html-attribute": "warn", 646 | "react/no-is-mounted": "error", 647 | "react/no-object-type-as-default-prop": "error", 648 | "react/no-redundant-should-component-update": "error", 649 | "react/no-render-return-value": "error", 650 | "react/no-string-refs": "error", 651 | "react/no-this-in-sfc": "error", 652 | "react/no-typos": "error", 653 | "react/no-unknown-property": "error", 654 | "react/no-unused-state": "warn", 655 | "react/require-render-return": "error", 656 | "react/self-closing-comp": "warn", 657 | "react/void-dom-elements-no-children": "error", 658 | // JSX-specific rules - https://github.com/jsx-eslint/eslint-plugin-react#jsx-specific-rules 659 | "react/jsx-boolean-value": ["warn", "always"], 660 | "react/jsx-curly-brace-presence": ["warn", "never"], 661 | "react/jsx-fragments": "warn", 662 | "react/jsx-key": [ 663 | "error", 664 | { 665 | checkFragmentShorthand: true, 666 | checkKeyMustBeforeSpread: true, 667 | warnOnDuplicates: true, 668 | }, 669 | ], 670 | "react/jsx-no-comment-textnodes": "error", 671 | "react/jsx-no-duplicate-props": "error", 672 | "react/jsx-no-script-url": "error", 673 | "react/jsx-no-target-blank": "warn", 674 | "react/jsx-no-undef": "error", 675 | "react/jsx-no-useless-fragment": [ 676 | "warn", 677 | { 678 | allowExpressions: true, 679 | }, 680 | ], 681 | "react/jsx-pascal-case": "warn", 682 | "react/jsx-props-no-spread-multi": "warn", 683 | "react/jsx-props-no-spreading": "warn", 684 | "react/jsx-uses-react": "error", 685 | "react/jsx-uses-vars": "error", 686 | // Other 687 | "no-restricted-imports": [ 688 | "warn", 689 | { 690 | ...baseRestrictedImports, 691 | paths: [ 692 | { 693 | name: "react", 694 | importNames: ["default"], 695 | message: 696 | "'import React' is not needed due to the new JSX transform: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html\n\nIf you need a named export, use: 'import { Something } from \"react\"'", 697 | }, 698 | ], 699 | }, 700 | ], 701 | }, 702 | }, 703 | { 704 | files: ["**/*.test.js", "**/*.test.ts", "**/*.test.jsx", "**/*.test.tsx", "**/test/**"], 705 | plugins: { 706 | jest: jestPlugin, 707 | }, 708 | languageOptions: { 709 | globals: { 710 | ...jestPlugin.environments.globals.globals, 711 | ...globals["shared-node-browser"], 712 | }, 713 | }, 714 | rules: { 715 | // Jest - https://github.com/jest-community/eslint-plugin-jest#rules 716 | "jest/consistent-test-it": [ 717 | "warn", 718 | { 719 | withinDescribe: "test", 720 | }, 721 | ], 722 | "jest/expect-expect": "warn", 723 | "jest/no-alias-methods": "warn", 724 | "jest/no-commented-out-tests": "warn", 725 | "jest/no-conditional-expect": "error", 726 | "jest/no-conditional-in-test": "error", 727 | "jest/no-confusing-set-timeout": "error", 728 | "jest/no-deprecated-functions": "error", 729 | "jest/no-disabled-tests": "warn", 730 | "jest/no-done-callback": "error", 731 | "jest/no-export": "error", 732 | "jest/no-focused-tests": "warn", 733 | "jest/no-identical-title": "error", 734 | "jest/no-interpolation-in-snapshots": "error", 735 | "jest/no-jasmine-globals": "error", 736 | "jest/no-mocks-import": "error", 737 | "jest/no-standalone-expect": "error", 738 | "jest/no-test-prefixes": "warn", 739 | "jest/no-test-return-statement": "error", 740 | "jest/prefer-comparison-matcher": "warn", 741 | "jest/prefer-each": "warn", 742 | "jest/prefer-ending-with-an-expect": "warn", 743 | "jest/prefer-equality-matcher": "warn", 744 | "jest/prefer-importing-jest-globals": "error", 745 | "jest/prefer-jest-mocked": "warn", 746 | "jest/prefer-lowercase-title": [ 747 | "warn", 748 | { 749 | ignoreTopLevelDescribe: true, 750 | }, 751 | ], 752 | "jest/prefer-mock-promise-shorthand": "warn", 753 | "jest/prefer-spy-on": "warn", 754 | "jest/prefer-strict-equal": "error", 755 | "jest/prefer-to-be": "warn", 756 | "jest/prefer-to-contain": "warn", 757 | "jest/prefer-to-have-length": "warn", 758 | "jest/valid-describe-callback": "error", 759 | "jest/valid-expect": "error", 760 | "jest/valid-expect-in-promise": "error", 761 | "jest/valid-title": "warn", 762 | }, 763 | }, 764 | { 765 | files: ["**/*.test.ts", "**/*.test.tsx", "**/test/**/*.ts"], 766 | plugins: { 767 | "@typescript-eslint": typescriptPlugin, 768 | jest: jestPlugin, 769 | }, 770 | rules: { 771 | // TypeScript-specific test overrides 772 | "@typescript-eslint/naming-convention": "off", 773 | "@typescript-eslint/unbound-method": "off", 774 | "jest/unbound-method": "error", 775 | }, 776 | }, 777 | { 778 | files: ["**/*.test.jsx", "**/*.test.tsx"], 779 | plugins: { 780 | "testing-library": testingLibraryPlugin, 781 | jest: jestPlugin, 782 | }, 783 | rules: { 784 | // React Testing Library - https://github.com/testing-library/eslint-plugin-testing-library 785 | "testing-library/await-async-queries": "error", 786 | "testing-library/await-async-utils": "error", 787 | "testing-library/no-await-sync-queries": "error", 788 | "testing-library/no-container": "error", 789 | "testing-library/no-debugging-utils": "warn", 790 | "testing-library/no-dom-import": ["error", "react"], 791 | "testing-library/no-global-regexp-flag-in-query": "warn", 792 | "testing-library/no-node-access": "warn", 793 | "testing-library/no-test-id-queries": "warn", 794 | "testing-library/no-unnecessary-act": "warn", 795 | "testing-library/no-wait-for-multiple-assertions": "error", 796 | "testing-library/no-wait-for-side-effects": "error", 797 | "testing-library/no-wait-for-snapshot": "error", 798 | "testing-library/prefer-find-by": "warn", 799 | "testing-library/prefer-implicit-assert": "warn", 800 | "testing-library/prefer-presence-queries": "error", 801 | "testing-library/prefer-query-by-disappearance": "error", 802 | "testing-library/prefer-screen-queries": "warn", 803 | "testing-library/prefer-user-event": "error", 804 | "testing-library/render-result-naming-convention": "error", 805 | // Jest - https://github.com/jest-community/eslint-plugin-jest 806 | "jest/expect-expect": [ 807 | "warn", 808 | { 809 | assertFunctionNames: ["expect", "*.getBy*", "*.getAllBy*", "*.findBy*", "*.findAllBy*"], 810 | }, 811 | ], 812 | "jest/prefer-ending-with-an-expect": [ 813 | "warn", 814 | { 815 | assertFunctionNames: ["expect", "*.getBy*", "*.getAllBy*", "*.findBy*", "*.findAllBy*"], 816 | }, 817 | ], 818 | }, 819 | }, 820 | ]; 821 | --------------------------------------------------------------------------------