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:
@@ -59,3 +59,6 @@ jobs:
|
||||
|
||||
- name: Format
|
||||
run: pnpm run format:check
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
pnpm run format:check
|
||||
pnpm run lint
|
||||
# TODO enable
|
||||
# npm test
|
||||
# npm run lint
|
||||
# npx lint-staged
|
||||
|
||||
@@ -87,7 +87,7 @@ export default async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
const safePattern = /^[-\w\/.,]+$/;
|
||||
const safePattern = /^[-\w/.,]+$/;
|
||||
if (
|
||||
(username && !safePattern.test(username)) ||
|
||||
(repo && !safePattern.test(repo)) ||
|
||||
|
||||
@@ -78,7 +78,7 @@ export default async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
const safePattern = /^[-\w\/.,]+$/;
|
||||
const safePattern = /^[-\w/.,]+$/;
|
||||
if (
|
||||
(username && !safePattern.test(username)) ||
|
||||
(repo && !safePattern.test(repo))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import toEmoji from "emoji-name-map";
|
||||
import { CustomError } from "./error.js";
|
||||
|
||||
const OWNER_AFFILIATIONS = ["OWNER", "COLLABORATOR", "ORGANIZATION_MEMBER"];
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
export const PROD = false;
|
||||
|
||||
export const USE_LOGGER = true;
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @param {Array<string>} args list of class names
|
||||
* @returns string classname attribute
|
||||
*/
|
||||
export function classnames(...args) {
|
||||
return args.join(" ");
|
||||
}
|
||||
|
||||
@@ -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,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
@@ -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"
|
||||
|
||||
Generated
+192
-391
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user