Consolidate ESLint configs into root (#1324)

Move general-purpose lint rules from packages/webamp/.eslintrc to the root
.eslintrc so they apply to all packages consistently. This includes:

- Core JavaScript best practices (no-var, prefer-const, eqeqeq, etc.)
- TypeScript-specific rules (@typescript-eslint/no-unused-vars with patterns)
- Prettier integration

Package-specific configs now only contain rules unique to their needs:
- webamp: React, import, and react-hooks plugin rules
- skin-database: Extends @typescript-eslint/recommended, disables rules that
  conflict with existing code style
- webamp-modern: Unchanged (has root: true for isolation)

Also fixes lint errors in skin-database:
- Consolidate duplicate imports in App.js and Feedback.js
- Add radix parameter to parseInt
- Prefix unused function parameters with underscore
- Convert var to let/const
- Fix type import for Shooter
This commit is contained in:
Jordan Eldredge 2025-11-27 21:32:10 -08:00 committed by GitHub
parent 642fb964d6
commit 1da77a640a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 146 additions and 204 deletions

View file

@ -4,15 +4,11 @@
"jsx": true,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
"jsx": true
}
},
"plugins": ["prettier"],
"plugins": ["prettier", "@typescript-eslint"],
"settings": {
"react": {
"version": "15.2"
},
"import/resolver": {
"node": {
"extensions": [".js", ".ts", ".tsx"]
@ -21,27 +17,81 @@
},
"env": {
"node": true,
"amd": true,
"es6": true,
"jest": true
},
"globals": {
"window": true,
"document": true,
"console": true,
"navigator": true,
"alert": true,
"Blob": true,
"fetch": true,
"FileReader": true,
"Element": true,
"AudioNode": true,
"MutationObserver": true,
"Image": true,
"location": true
},
"rules": {
"prettier/prettier": "error",
"no-constant-binary-expression": "error"
"no-constant-binary-expression": "error",
"block-scoped-var": "warn",
"camelcase": "error",
"constructor-super": "error",
"dot-notation": "error",
"eqeqeq": ["error", "smart"],
"guard-for-in": "error",
"max-depth": ["warn", 4],
"new-cap": "error",
"no-caller": "error",
"no-const-assign": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-div-regex": "warn",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "error",
"no-else-return": "error",
"no-empty-character-class": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extend-native": "warn",
"no-extra-boolean-cast": "error",
"no-extra-semi": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-implied-eval": "error",
"no-inner-declarations": "error",
"no-irregular-whitespace": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-multi-str": "error",
"no-nested-ternary": "warn",
"no-new-object": "error",
"no-new-symbol": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-shadow": "warn",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-undef-init": "error",
"no-unneeded-ternary": "error",
"no-unreachable": "error",
"no-unused-expressions": "error",
"no-useless-rename": "error",
"no-var": "error",
"no-with": "error",
"prefer-arrow-callback": "warn",
"prefer-const": "error",
"prefer-spread": "error",
"prefer-template": "warn",
"radix": "error",
"no-return-await": "error",
"use-isnan": "error",
"valid-typeof": "error",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
]
}
}

View file

@ -1,32 +1,28 @@
module.exports = {
env: {
node: true,
es2021: true,
jest: true,
},
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
plugins: ["@typescript-eslint"],
extends: ["plugin:@typescript-eslint/recommended"],
rules: {
// "no-console": "warn",
// Disable rules that conflict with the project's style
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-require-imports": "off", // Allow require() in JS files
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
// Override the base no-shadow rule since it conflicts with TypeScript
"no-shadow": "off",
// Relax rules for this project's existing style
camelcase: "off",
"dot-notation": "off",
eqeqeq: "off",
"no-undef-init": "off",
"no-return-await": "off",
"prefer-arrow-callback": "off",
"no-div-regex": "off",
"guard-for-in": "off",
"prefer-template": "off",
"no-else-return": "off",
"prefer-const": "off",
"new-cap": "off",
},
ignorePatterns: ["dist/**"],
};

View file

@ -64,7 +64,8 @@ export default function SkinScroller({
// When an element becomes mostly visible (> 50% intersecting)
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
const index = parseInt(
entry.target.getAttribute("skin-index") || "0"
entry.target.getAttribute("skin-index") || "0",
10
);
setVisibleSkinIndex(index);
}

View file

@ -10,7 +10,7 @@ const { handleRequest } = createYogaInstance({
return new UserContext();
},
logger: {
log: (message: string, context: Record<string, any>) => {
log: (_message: string, _context: Record<string, any>) => {
// console.log(message, context);
},
logError: (message: string, context: Record<string, any>) => {

View file

@ -1,18 +1,21 @@
"use client";
import React, { useCallback } from "react";
import { connect } from "react-redux";
import { connect, useSelector } from "react-redux";
import About from "./About";
import Feedback from "./Feedback";
import Header from "./Header";
import Overlay from "./Overlay";
import SkinTable from "./SkinTable";
import FocusedSkin from "./FocusedSkin";
import { useSelector } from "react-redux";
import * as Selectors from "./redux/selectors";
import * as Actions from "./redux/actionCreators";
import { ABOUT_PAGE, REVIEW_PAGE } from "./constants";
import {
ABOUT_PAGE,
REVIEW_PAGE,
SCREENSHOT_WIDTH,
SKIN_RATIO,
} from "./constants";
import { useWindowSize, useScrollbarWidth, useActionCreator } from "./hooks";
import { SCREENSHOT_WIDTH, SKIN_RATIO } from "./constants";
import UploadGrid from "./upload/UploadGrid";
import Metadata from "./components/Metadata";
import SkinReadme from "./SkinReadme";
@ -78,6 +81,7 @@ function App(props) {
windowWidth={windowWidthWithScrollabar}
/>
)}
{/* eslint-disable-next-line no-nested-ternary -- legacy code */}
{props.showFeedbackForm ? (
<Overlay>
<Feedback />

View file

@ -1,9 +1,7 @@
import React, { useState } from "react";
import React, { useState, useCallback } from "react";
import { getUrl } from "./redux/selectors";
import * as Actions from "./redux/actionCreators";
import { useActionCreator } from "./hooks";
import { useCallback } from "react";
import { useSelector } from "react-redux";
import { fetchGraphql, gql } from "./utils";

View file

@ -3,7 +3,7 @@ import React, { useLayoutEffect, useState } from "react";
function DownloadText({ text, children, ...restProps }) {
const [url, setUrl] = useState(null);
useLayoutEffect(() => {
var blob = new Blob([text], {
let blob = new Blob([text], {
type: "text/plain;charset=utf-8",
});
const url = URL.createObjectURL(blob);

View file

@ -1,7 +1,7 @@
import SparkMD5 from "spark-md5";
export function hashFile(file) {
var blobSlice =
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice;
@ -26,7 +26,7 @@ export function hashFile(file) {
fileReader.onerror = reject;
function loadNext() {
var start = currentChunk * chunkSize,
let start = currentChunk * chunkSize,
end = start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));

View file

@ -19,6 +19,7 @@ function Row({ name, loading, right, complete }) {
position: "absolute",
left: 0,
top: 0,
// eslint-disable-next-line no-nested-ternary -- legacy code
width: loading ? `90%` : complete ? `100%` : `0%`,
transitionProperty: "all",
// TODO: Try to learn how long it really takes

View file

@ -13,7 +13,7 @@ export function museumUrlFromHash(hash) {
}
export function getWindowSize() {
var w = window,
let w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName("body")[0],

View file

@ -7,6 +7,7 @@ import * as Skins from "../data/skins";
import * as CloudFlare from "../CloudFlare";
import SkinModel from "../data/SkinModel";
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-require-imports
const Shooter = require("../shooter");
export async function screenshot(skin: SkinModel, shooter: typeof Shooter) {

View file

@ -98,11 +98,10 @@ async function searchImage(query) {
});
}
function HeaderGrid({ skins, title }) {
function HeaderGrid({ skins, _title }) {
return (
<div
style={{
background: "black",
color: "white",
width: "100%",
height: "100%",
@ -110,7 +109,6 @@ function HeaderGrid({ skins, title }) {
flexDirection: "column",
textAlign: "center",
alignItems: "center",
justifyContent: "center",
justifyContent: "space-between",
background: "linear-gradient(45deg,#000,#191927 66%,#000)",
}}

View file

@ -2,7 +2,6 @@ export default function Frame({ children }) {
return (
<div
style={{
background: "black",
color: "white",
width: "100%",
height: "100%",
@ -10,7 +9,6 @@ export default function Frame({ children }) {
flexDirection: "column",
textAlign: "center",
alignItems: "center",
justifyContent: "center",
justifyContent: "space-between",
background: "linear-gradient(45deg,#000,#191927 66%,#000)",
}}

View file

@ -13,6 +13,7 @@
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"lint": "eslint src --ext ts,tsx,js",
"type-check": "tsc",
"typecheck": "tsc"
},

View file

@ -1,42 +1,10 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"jsx": true,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
}
},
"plugins": [
"react",
"prettier",
"import",
"@typescript-eslint",
"react-hooks"
],
"plugins": ["react", "import", "react-hooks"],
"settings": {
"react": {
"version": "15.2"
},
"import/resolver": {
"node": {
"extensions": [".js", ".ts", ".tsx"]
}
}
},
"env": {
"node": true,
"amd": true,
"es6": true,
"jest": true
},
// TODO: Consider removing some of these.
// Allow common browser globals for better compatibility:
// * document
// * window
// * console
// * navigator
"globals": {
"window": true,
"document": true,
@ -61,79 +29,18 @@
"maxBOF": 0
}
],
"block-scoped-var": "warn",
"camelcase": "error",
"constructor-super": "error",
"dot-notation": "error",
"eqeqeq": ["error", "smart"],
"guard-for-in": "error",
"lines-between-class-members": [
"warn",
"always",
{ "exceptAfterSingleLine": true }
],
"max-depth": ["warn", 4],
"max-params": ["warn", 5],
"new-cap": "error",
"no-caller": "error",
"no-catch-shadow": "error",
"no-const-assign": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-div-regex": "warn",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "error",
"no-else-return": "error",
"no-empty-character-class": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extend-native": "warn",
"no-extra-boolean-cast": "error",
"no-extra-semi": "error",
"no-fallthrough": "error",
"no-floating-decimal": "error",
"no-func-assign": "error",
"no-implied-eval": "error",
"no-inner-declarations": "error",
"no-irregular-whitespace": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-multi-spaces": "warn",
"no-multi-str": "error",
"no-native-reassign": "error",
"no-negated-in-lhs": "warn",
"no-nested-ternary": "warn",
"no-new-object": "error",
"no-new-symbol": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-shadow": "warn",
"no-spaced-func": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-unneeded-ternary": "error",
"no-unreachable": "error",
"no-unused-expressions": "error",
"no-useless-rename": "error",
"no-var": "error",
"no-with": "error",
"prefer-arrow-callback": "warn",
"prefer-const": "error",
"prefer-spread": "error",
"prefer-template": "warn",
"radix": "error",
"no-return-await": "error",
"react/no-string-refs": "error",
"react/jsx-boolean-value": "error",
"react/jsx-uses-react": "off",
@ -143,8 +50,6 @@
"react/require-render-return": "error",
"react/self-closing-comp": "error",
"react/no-unescaped-entities": "error",
"use-isnan": "error",
"valid-typeof": "error",
"import/default": "error",
"import/export": "error",
"import/first": "error",

View file

@ -15,7 +15,7 @@ let config: Config = {};
if (hash) {
try {
config = JSON.parse(decodeURIComponent(hash).slice(1));
} catch (e) {
} catch (_e) {
console.error("Failed to decode config from hash: ", hash);
}
}

View file

@ -111,7 +111,7 @@ export async function getWebampConfig(
try {
const track = JSON.parse(trackJson);
return [track];
} catch (err) {
} catch (_err) {
return null;
}
},

View file

@ -168,7 +168,7 @@ export function fetchMediaDuration(url: string, id: number): Thunk {
try {
const duration = await genMediaDuration(url);
dispatch({ type: "SET_MEDIA_DURATION", duration, id });
} catch (e) {
} catch (_e) {
// TODO: Should we update the state to indicate that we don't know the length?
}
},
@ -328,7 +328,7 @@ export function fetchMediaTags(file: string | Blob, id: number): Thunk {
sampleRate,
id,
});
} catch (e) {
} catch (_e) {
dispatch({ type: "MEDIA_TAG_REQUEST_FAILED", id });
}
};

View file

@ -20,7 +20,7 @@ import PlaylistWindow from "./PlaylistWindow";
import EqualizerWindow from "./EqualizerWindow";
import Skin from "./Skin";
import Media, { IMedia } from "../media";
import { IMedia } from "../media";
import { useTypedSelector, useActionCreator } from "../hooks";
import Css from "./Css";

View file

@ -17,9 +17,9 @@ export class FFT {
// Assuming these are your hardcoded values:
const samplesIn = 1024; // hardcoded value
const samplesOut = 512; // hardcoded value
const bEqualize = true; // hardcoded value
const _bEqualize = true; // hardcoded value
const envelopePower = 1.0; // hardcoded value
const mode = false; // hardcoded value
const _mode = false; // hardcoded value
const NFREQ = samplesOut * 2;
@ -28,13 +28,13 @@ export class FFT {
this.cossintable = this.initCosSinTable(NFREQ);
this.envelope = this.initEnvelopeTable(samplesIn, envelopePower);
this.equalize = this.initEqualizeTable(NFREQ, mode);
this.equalize = this.initEqualizeTable(NFREQ, _mode);
this.temp1 = new Float32Array(NFREQ);
this.temp2 = new Float32Array(NFREQ);
}
private initEqualizeTable(NFREQ: number, mode: boolean): Float32Array {
private initEqualizeTable(NFREQ: number, _mode: boolean): Float32Array {
const equalize = new Float32Array(NFREQ / 2);
let bias = 0.04; // FFT.INITIAL_BIAS

View file

@ -9,10 +9,6 @@ interface Props {
children: ReactNode | Array<ReactNode>;
}
interface State {
selected: boolean;
}
function PlaylistMenu(props: Props) {
const [selected, setSelected] = useState(false);

View file

@ -4,12 +4,6 @@ import { Hr, Node } from "../ContextMenu";
import ContextMenuTarget from "../ContextMenuTarget";
import { useActionCreator } from "../../hooks";
interface DispatchProps {
sortListByTitle: () => void;
reverseList: () => void;
randomizeList: () => void;
}
/* eslint-disable no-alert */
/* TODO: This should really be kitty-corner to the upper right hand corner of the MiscMenu */
export default function SortContextMenu() {

View file

@ -15,7 +15,12 @@ interface Props {
}
function ResizeTarget(props: Props) {
const { currentSize, setWindowSize, widthOnly, ...passThroughProps } = props;
const {
currentSize,
setWindowSize: _setWindowSize,
widthOnly,
...passThroughProps
} = props;
const [mouseDown, setMouseDown] = useState(false);
const [mouseStart, setMouseStart] = useState<null | { x: number; y: number }>(
null

View file

@ -280,8 +280,8 @@ export class BarPaintHandler extends VisPaintHandler {
paintAnalyzer() {
if (!this._ctx) return;
const ctx = this._ctx;
const w = ctx.canvas.width;
const h = ctx.canvas.height;
const _w = ctx.canvas.width;
const _h = ctx.canvas.height;
ctx.fillStyle = this._color;
const maxFreqIndex = 512;
@ -605,8 +605,8 @@ export class WavePaintHandler extends VisPaintHandler {
this._dataArray = this._dataArray.slice(0, 576);
const bandwidth = this._dataArray.length;
const width = this._ctx!.canvas.width;
const height = this._ctx!.canvas.height;
const _width = this._ctx!.canvas.width;
const _height = this._ctx!.canvas.height;
// width would technically be correct, but if the main window is
// in windowshade mode, it is set to 150, making sliceWidth look
@ -693,14 +693,9 @@ export class WavePaintHandler extends VisPaintHandler {
// clamp y to be within a certain range, here it would be 0..10 if both windowShade and pixelDensity apply
// else we clamp y to 0..15 or 0..3, depending on renderHeight
if (this._vis.smallVis && this._vis.pixelDensity === 2) {
y = y < 0 ? 0 : y > 10 - 1 ? 10 - 1 : y;
y = Math.max(0, Math.min(10 - 1, y));
} else {
y =
y < 0
? 0
: y > this._vis.renderHeight - 1
? this._vis.renderHeight - 1
: y;
y = Math.max(0, Math.min(this._vis.renderHeight - 1, y));
}
const v = y;
if (x === 0) this._lastY = y;
@ -774,7 +769,7 @@ export class NoVisualizerHandler extends VisPaintHandler {
paintFrame() {
if (!this._ctx) return;
const ctx = this._ctx;
const _ctx = this._ctx;
this.cleared = true;
}
}

View file

@ -62,7 +62,7 @@ export default function WinampButton({
}
setActive(true);
function onRelease(ee: PointerEvent) {
function onRelease(_ee: PointerEvent) {
setActive(false);
document.removeEventListener("pointerup", onRelease);
}

View file

@ -123,7 +123,7 @@ export default class ElementSource {
try {
await this._audio.play();
// TODO #race
} catch (err) {
} catch (_err) {
//
}
this._setStatus(MEDIA_STATUS.PLAYING);

View file

@ -22,7 +22,7 @@ const noshadeStyle = {
// We use all kinds of non-standard attributes and tags. So we create these fake
// components to trick Typescript.
const Body = (props: any) => {
const _Body = (props: any) => {
// @ts-ignore
return <body {...props} />;
};

View file

@ -29,9 +29,9 @@ function massageKbps(kbps: number) {
// from Justin Frankel directly:
// IIRC H was for "hundred" and "C" was thousand,
// though why it was for thousand I have no idea lol, maybe it was a mistake...
if (bitrateNum >= 1000) finalKbps = String(bitrateNum).slice(0, 2) + "H";
if (bitrateNum >= 1000) finalKbps = `${String(bitrateNum).slice(0, 2)}H`;
if (bitrateNum >= 10000)
finalKbps = String(bitrateNum).slice(0, 1).padStart(2, " ") + "C";
finalKbps = `${String(bitrateNum).slice(0, 1).padStart(2, " ")}C`;
return finalKbps;
}

View file

@ -215,7 +215,7 @@ const windows = (
return w;
}
// Pull out `hidden` since it's been removed from our state.
const { hidden, ...rest } = serializedW;
const { hidden: _hidden, ...rest } = serializedW;
return { ...w, ...rest };
}),
focused,

View file

@ -1,4 +1,4 @@
import { WindowInfo, WindowId } from "./types";
import { WindowInfo } from "./types";
interface NewGraph {
[key: string]: {

View file

@ -21,7 +21,6 @@ import {
import { createSelector, defaultMemoize } from "reselect";
import * as Utils from "./utils";
import {
BANDS,
TRACK_HEIGHT,
WINDOW_RESIZE_SEGMENT_WIDTH,
WINDOW_RESIZE_SEGMENT_HEIGHT,
@ -30,7 +29,6 @@ import {
MEDIA_TAG_REQUEST_STATUS,
WINDOWS,
VISUALIZERS,
PLAYER_MEDIA_STATUS,
} from "./constants";
import { createPlaylistURL } from "./playlistHtml";
import * as fromTracks from "./reducers/tracks";

View file

@ -47,7 +47,7 @@ export async function getFileFromZip(
try {
const contents = await lastFile.async(mode);
return { contents, name: lastFile.name };
} catch (e) {
} catch (_e) {
console.warn(
`Failed to extract "${fileName}.${ext}" from the skin archive.`
);
@ -66,10 +66,10 @@ export async function getImgFromBlob(
// Use this faster native browser API if available.
// NOTE: In some browsers `window.createImageBitmap` may not exist so this will throw.
return await window.createImageBitmap(blob);
} catch (e) {
} catch (_e) {
try {
return await fallbackGetImgFromBlob(blob);
} catch (ee) {
} catch (_ee) {
// Like Winamp we will silently fail on images that don't parse.
return null;
}

View file

@ -1,4 +1,4 @@
import { Options, Preset } from "./types";
import { Options } from "./types";
import { PrivateOptions } from "./webampLazy";
import Webamp from "./webamp";
// @ts-ignore

View file

@ -67,6 +67,7 @@
"ani-cursor#lint": {},
"skin-database#lint": {},
"skin-museum-og#lint": {},
"webamp-docs#lint": {},
"webamp-modern#lint": {},
"winamp-eqf#lint": {},
"dev": {