feat: setup eslint at root level (#27)

* feat: setup eslint at root level

* chore: disable craco eslint-plugin

* fix: add missing extension to import

* fix: apply review

* chore: use gitignore to exclude files from eslint

* chore: disable eslint step in craco via config rather than env variable

* chore: remove npmrc

* chore: remove `eslint-plugin-react` override
This commit is contained in:
Marco Pasqualetti
2026-01-21 22:48:08 +01:00
committed by GitHub
parent 74b314ab0f
commit dff4bfcecb
30 changed files with 352 additions and 501 deletions
+3
View File
@@ -59,3 +59,6 @@ jobs:
- name: Format
run: pnpm run format:check
- name: Lint
run: pnpm run lint
+1 -1
View File
@@ -1,5 +1,5 @@
pnpm run format:check
pnpm run lint
# TODO enable
# npm test
# npm run lint
# npx lint-staged
+1 -1
View File
@@ -87,7 +87,7 @@ export default async (req, res) => {
);
}
const safePattern = /^[-\w\/.,]+$/;
const safePattern = /^[-\w/.,]+$/;
if (
(username && !safePattern.test(username)) ||
(repo && !safePattern.test(repo)) ||
+1 -1
View File
@@ -78,7 +78,7 @@ export default async (req, res) => {
);
}
const safePattern = /^[-\w\/.,]+$/;
const safePattern = /^[-\w/.,]+$/;
if (
(username && !safePattern.test(username)) ||
(repo && !safePattern.test(repo))
-5
View File
@@ -35,16 +35,11 @@
"devDependencies": {
"@actions/core": "^2.0.1",
"@actions/github": "^6.0.1",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@uppercod/css-to-object": "^1.1.1",
"axios-mock-adapter": "^2.1.0",
"color-contrast-checker": "^2.1.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^61.5.0",
"express": "^5.2.1",
"globals": "^16.5.0",
"hjson": "^3.2.2",
+8 -5
View File
@@ -9,11 +9,14 @@
* @returns {string} Encoded string.
*/
const encodeHTML = (str) => {
return str
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
return "&#" + i.charCodeAt(0) + ";";
})
.replace(/\u0008/gim, "");
return (
str
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i) => {
return "&#" + i.charCodeAt(0) + ";";
})
// eslint-disable-next-line no-control-regex
.replace(/\u0008/gim, "")
);
};
export { encodeHTML };
+1
View File
@@ -1,6 +1,7 @@
// @ts-check
import toEmoji from "emoji-name-map";
import { CustomError } from "./error.js";
const OWNER_AFFILIATIONS = ["OWNER", "COLLABORATOR", "ORGANIZATION_MEMBER"];
+12 -2
View File
@@ -2,8 +2,18 @@
import { CustomError } from "./error.js";
import { logger } from "./log.js";
import { getUserAccessByKey, getUserAccessByName } from "./database.js";
import { getUserAccessByName } from "./database.js";
/**
* Returns a random integer from 0 (inclusive) to `max` (exclusive).
*
* The value is generated using `Math.random()` and uniformly distributed
* across the range.
*
* @param {number} max The upper bound (exclusive). Must be a positive number.
*
* @returns {number} A random integer `n` such that `0 <= n < max`.
*/
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
@@ -17,7 +27,7 @@ function getRandomInt(max) {
* Try to execute the fetcher function until it succeeds or the max number of retries is reached.
*
* @param {FetcherFunction} fetcher The fetcher function.
* @param username GitHub username of the user whose PAT to use, if available
* @param {string?} username GitHub username of the user whose PAT to use, if available
* @param {any} variables Object with arguments to pass to the fetcher function.
* @returns {Promise<any>} The response from the fetcher function.
*/
+42 -5
View File
@@ -42,11 +42,22 @@ const measurePerformance = async (fn) => {
* Computes basic & extended statistics.
*
* @param {bigint[]} samples Array of samples in nanoseconds.
* @returns {object} Stats
* @returns {{
* runs: number
* min: number
* max: number
* average: number
* median: number
* p75: number
* p95: number
* p99: number
* stdev: number
* totalTime: number
* }} Stats
*/
const computeStats = (samples) => {
const sorted = [...samples].sort((a, b) => (a < b ? -1 : 1));
const toNumber = (b) => Number(b); // safe for typical short benches
const toNumber = (/** @type {bigint} */ b) => Number(b); // safe for typical short benches
const n = sorted.length;
const sum = sorted.reduce((a, b) => a + b, 0n);
const avg = Number(sum) / n;
@@ -54,7 +65,7 @@ const computeStats = (samples) => {
n % 2
? toNumber(sorted[(n - 1) / 2])
: (toNumber(sorted[n / 2 - 1]) + toNumber(sorted[n / 2])) / 2;
const p = (q) => {
const p = (/** @type {number} */ q) => {
const idx = Math.min(n - 1, Math.floor((q / 100) * n));
return toNumber(sorted[idx]);
};
@@ -120,7 +131,7 @@ export const runAndLogStats = async (
const stats = computeStats(processed);
const fmt = (ns) => formatTime(BigInt(Math.round(ns)));
const fmt = (/** @type {number} */ ns) => formatTime(BigInt(Math.round(ns)));
console.log(
`${fnName} | runs=${stats.runs} avg=${fmt(stats.average)} median=${fmt(
stats.median,
@@ -132,11 +143,37 @@ export const runAndLogStats = async (
return stats;
};
/**
* Creates an asymmetric matcher for approximate numeric equality.
*
* This helper is intended for use in test frameworks (e.g., Jest) where
* values need to be compared within a configurable decimal precision
* instead of strict equality.
*
* The comparison succeeds when:
*
* |actual - expected| < 10^(-precision)
*
* For example, with `precision = 3`, values must be within `0.001`.
*
* @param {number} expected The expected numeric value to compare against.
*
* @param {number} [precision=10]
* The number of decimal places of tolerance. Higher values mean stricter
* comparison. Internally converted to epsilon = 10^-precision.
*
* @returns {{
* asymmetricMatch(actual: unknown): boolean,
* toAsymmetricMatcher(): string
* }} An object implementing Jest-style asymmetric matcher methods.
*
*/
export function approxNumber(expected, precision = 10) {
return {
asymmetricMatch(actual) {
if (typeof actual !== "number" || typeof expected !== "number")
if (typeof actual !== "number" || typeof expected !== "number") {
return false;
}
const epsilon = Math.pow(10, -precision);
return Math.abs(actual - expected) < epsilon;
},
-39
View File
@@ -1,39 +0,0 @@
module.exports = {
env: {
browser: true,
es6: true,
},
extends: ["plugin:prettier/recommended"],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2020,
sourceType: "module",
},
plugins: ["prettier"],
rules: {
"react/jsx-filename-extension": "off",
"react/forbid-prop-types": "off",
"react/destructuring-assignment": "off",
"import/prefer-default-export": "off",
"react/function-component-definition": "off",
"react/no-unstable-nested-components": "off",
"no-console": "off",
radix: "off",
"prettier/prettier": [
"error",
{
endOfLine: "auto",
},
],
},
overrides: [
{
files: ["./src/backend/**/*"],
rules: {
"prettier/prettier": "off",
},
},
],
};
+3
View File
@@ -2,6 +2,9 @@ const path = require("path");
const webpack = require("webpack");
module.exports = {
eslint: {
enable: false,
},
webpack: {
alias: {
dotenv: path.resolve(__dirname, "src/dotenv-browser-stub.js"),
-7
View File
@@ -40,13 +40,6 @@
"devDependencies": {
"@craco/craco": "^7.1.0",
"autoprefixer": "^10.4.16",
"eslint": "8.53.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"postcss": "^8.4.31",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.5"
-3
View File
@@ -1,6 +1,3 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/no-danger */
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
@@ -1,4 +1,3 @@
/* eslint-disable react/jsx-props-no-spreading */
import React from "react";
import PropTypes from "prop-types";
@@ -1,5 +1,3 @@
/* eslint-disable react/no-danger */
import React from "react";
import PropTypes from "prop-types";
@@ -1,5 +1,3 @@
/* eslint-disable react/no-danger */
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
@@ -1,5 +1,3 @@
/* eslint-disable react/no-danger */
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-nested-ternary */
export const PROD = false;
export const USE_LOGGER = true;
+9 -3
View File
@@ -21,9 +21,15 @@ import { clearAxiosCache } from "../../axios-override";
function App() {
const toMessage = (input) => {
if (typeof input === "string") return input;
if (input.reason?.message) return input.reason.message;
if (input.message) return input.message;
if (typeof input === "string") {
return input;
}
if (input.reason?.message) {
return input.reason.message;
}
if (input.message) {
return input.message;
}
try {
return JSON.stringify(input);
} catch {
+1 -4
View File
@@ -1,5 +1,4 @@
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
@@ -56,8 +55,6 @@ MobileLink.propTypes = propTypes;
MobileLink.defaultProps = defaultProps;
const Header = ({ stage, setStage }) => {
const dispatch = useDispatch();
return (
<>
<div className="text-gray-100 bg-gray-800 shadow-md body-font z-50">
+7 -2
View File
@@ -76,6 +76,8 @@ const HomeScreen = ({ stage, setStage }) => {
const [enableAnimations, setEnableAnimations] = useState(true);
const [usePercent, setUsePercent] = useState(false);
const [theme, setTheme] = useState("default");
const resetCustomization = () => {
if (selectedCard === CardTypes.TOP_LANGS) {
setLangsCount(4);
@@ -204,7 +206,6 @@ const HomeScreen = ({ stage, setStage }) => {
}
// for stage four
const [theme, setTheme] = useState("default");
let themeSuffix = fullSuffix;
if (
@@ -419,6 +420,7 @@ const HomeScreen = ({ stage, setStage }) => {
)}
{stage === 4 && (
<DisplayStage
// eslint-disable-next-line consistent-return
filename={(() => {
switch (selectedCard) {
case CardTypes.STATS:
@@ -432,17 +434,20 @@ const HomeScreen = ({ stage, setStage }) => {
return `${wakatimeUser}_card`;
}
})()}
// eslint-disable-next-line consistent-return
link={(() => {
switch (selectedCard) {
case CardTypes.STATS:
case CardTypes.TOP_LANGS:
return `https://${HOST}/api${themeSuffix}`;
case CardTypes.PIN:
case CardTypes.PIN: {
let myRepo = repo;
if (!myRepo.includes("/")) {
myRepo = `${userId}/${myRepo}`;
}
return `https://github.com/${myRepo}`;
}
case CardTypes.GIST:
return gistUrl;
case CardTypes.WAKATIME:
@@ -13,8 +13,6 @@ import { classnames } from "../../../utils";
import { HOST } from "../../../constants";
const DisplayStage = ({ filename, link, themeSuffix, guestHint }) => {
const card = themeSuffix.split("?")[0];
const downloadPNG = () => {
saveSvgAsPng(
document.getElementById("svgWrapper").shadowRoot.firstElementChild
-1
View File
@@ -7,7 +7,6 @@ const initialState = {
privateAccess: null,
};
// eslint-disable-next-line default-param-last
export default (state = initialState, action) => {
switch (action.type) {
case types.LOGIN:
+6 -2
View File
@@ -4,7 +4,11 @@ import loggerMiddleware from "./logger";
import rootReducer from "./reducers";
import { USE_LOGGER } from "../constants";
export default function configureStore(intialState) {
/**
* @param {unknown?} initialState store initial state
* @returns {import('redux').Store} store
*/
export default function configureStore(initialState) {
let middlewares = [];
if (USE_LOGGER) {
middlewares = [loggerMiddleware];
@@ -14,7 +18,7 @@ export default function configureStore(intialState) {
const enhancers = [middlewareEnhancer];
const composedEnhancers = compose(...enhancers);
const store = createStore(rootReducer, intialState, composedEnhancers);
const store = createStore(rootReducer, initialState, composedEnhancers);
return store;
}
+4
View File
@@ -1,3 +1,7 @@
/**
* @param {Array<string>} args list of class names
* @returns string classname attribute
*/
export function classnames(...args) {
return args.join(" ");
}
+2
View File
@@ -1,6 +1,8 @@
import axios from "axios";
import { HOST } from "./constants";
// See https://github.com/stats-organization/github-stats-extended/pull/27#discussion_r2712184285
// eslint-disable-next-line no-unused-vars
const fetchWakatimeStats = async ({ username, api_domain }) => {
if (!username) {
throw new Error("missing parameter: username");
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable global-require */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
@@ -1,32 +1,32 @@
import globals from "globals";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "eslint/config";
import globals from "globals";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import jsdoc from "eslint-plugin-jsdoc";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import { includeIgnoreFile } from "@eslint/compat";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
const gitignorePath = fileURLToPath(new URL(".gitignore", import.meta.url));
export default [
...compat.extends("prettier"),
export default defineConfig(
includeIgnoreFile(gitignorePath, "Imported .gitignore patterns"),
js.configs.recommended,
{
linterOptions: {
reportUnusedDisableDirectives: "error",
},
languageOptions: {
globals: {
...globals.node,
...globals.browser,
},
ecmaVersion: 2022,
sourceType: "module",
},
plugins: {
jsdoc,
react,
"react-hooks": reactHooks,
},
rules: {
"no-unexpected-multiline": "error",
@@ -80,4 +80,33 @@ export default [
"jsdoc/require-jsdoc": "warn",
},
},
];
{
files: ["apps/backend/**/*.{js}"],
languageOptions: {
globals: {
...globals.node,
},
},
},
{
files: ["apps/frontend/**/*.{js,jsx}"],
plugins: {
react,
"react-hooks": reactHooks,
},
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/no-array-index-key": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
},
);
+14 -2
View File
@@ -1,17 +1,29 @@
{
"name": "root",
"private": true,
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",
"type": "module",
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316",
"devDependencies": {
"@eslint/compat": "2.0.1",
"@eslint/js": "9.39.2",
"eslint": "9.39.2",
"eslint-plugin-jsdoc": "61.7.1",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"globals": "16.5.0",
"husky": "9.1.7",
"prettier": "3.7.4"
},
"scripts": {
"prepare": "husky",
"format": "prettier --write .",
"format:check": "prettier --check ."
"format:check": "prettier --check .",
"lint": "eslint",
"lint:fix": "eslint --fix"
},
"resolutions": {
"eslint": "9.39.2",
"eslint-plugin-react-hooks": "7.0.1",
"@svgr/webpack": "^6.0.0",
"resolve-url-loader": "^5.0.0",
"webpack-dev-server": "^5.2.2"
+192 -391
View File
File diff suppressed because it is too large Load Diff