From 1da77a640a14fd6a592156aa1b5858617be472f3 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Thu, 27 Nov 2025 21:32:10 -0800 Subject: [PATCH] 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 --- .eslintrc | 96 +++++++++++++----- packages/skin-database/.eslintrc.js | 40 ++++---- .../app/(modern)/scroll/SkinScroller.tsx | 3 +- packages/skin-database/app/graphql/route.ts | 2 +- .../skin-database/legacy-client/src/App.js | 12 ++- .../legacy-client/src/Feedback.js | 4 +- .../src/components/DownloadText.js | 2 +- .../legacy-client/src/hashFile.js | 4 +- .../legacy-client/src/upload/UploadRow.js | 1 + .../skin-database/legacy-client/src/utils.js | 2 +- .../skin-database/tasks/screenshotSkin.ts | 1 + packages/skin-museum-og/pages/api/og.js | 4 +- packages/skin-museum-og/src/og/Frame.js | 2 - packages/webamp-docs/package.json | 1 + packages/webamp/.eslintrc | 97 +------------------ packages/webamp/demo/js/config.ts | 2 +- packages/webamp/demo/js/webampConfig.ts | 2 +- packages/webamp/js/actionCreators/files.ts | 4 +- packages/webamp/js/components/App.tsx | 2 +- packages/webamp/js/components/FFTNullsoft.ts | 8 +- .../PlaylistWindow/PlaylistMenu.tsx | 4 - .../PlaylistWindow/SortContextMenu.tsx | 6 -- .../webamp/js/components/ResizeTarget.tsx | 7 +- packages/webamp/js/components/VisPainter.ts | 19 ++-- .../webamp/js/components/WinampButton.tsx | 2 +- packages/webamp/js/media/elementSource.ts | 2 +- packages/webamp/js/playlistHtml.tsx | 2 +- packages/webamp/js/reducers/tracks.ts | 4 +- packages/webamp/js/reducers/windows.ts | 2 +- packages/webamp/js/resizeUtils.ts | 2 +- packages/webamp/js/selectors.ts | 2 - packages/webamp/js/skinParserUtils.ts | 6 +- packages/webamp/js/webampWithButterchurn.ts | 2 +- turbo.json | 1 + 34 files changed, 146 insertions(+), 204 deletions(-) diff --git a/.eslintrc b/.eslintrc index 213b7eff..4b36e433 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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": "^_" + } + ] } } diff --git a/packages/skin-database/.eslintrc.js b/packages/skin-database/.eslintrc.js index 08ad143d..ca0786ad 100644 --- a/packages/skin-database/.eslintrc.js +++ b/packages/skin-database/.eslintrc.js @@ -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/**"], }; diff --git a/packages/skin-database/app/(modern)/scroll/SkinScroller.tsx b/packages/skin-database/app/(modern)/scroll/SkinScroller.tsx index 6dde3208..c1843e45 100644 --- a/packages/skin-database/app/(modern)/scroll/SkinScroller.tsx +++ b/packages/skin-database/app/(modern)/scroll/SkinScroller.tsx @@ -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); } diff --git a/packages/skin-database/app/graphql/route.ts b/packages/skin-database/app/graphql/route.ts index df50b087..251701b5 100644 --- a/packages/skin-database/app/graphql/route.ts +++ b/packages/skin-database/app/graphql/route.ts @@ -10,7 +10,7 @@ const { handleRequest } = createYogaInstance({ return new UserContext(); }, logger: { - log: (message: string, context: Record) => { + log: (_message: string, _context: Record) => { // console.log(message, context); }, logError: (message: string, context: Record) => { diff --git a/packages/skin-database/legacy-client/src/App.js b/packages/skin-database/legacy-client/src/App.js index 420dae44..83ce90c6 100644 --- a/packages/skin-database/legacy-client/src/App.js +++ b/packages/skin-database/legacy-client/src/App.js @@ -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 ? ( diff --git a/packages/skin-database/legacy-client/src/Feedback.js b/packages/skin-database/legacy-client/src/Feedback.js index cea887a4..4c635c77 100644 --- a/packages/skin-database/legacy-client/src/Feedback.js +++ b/packages/skin-database/legacy-client/src/Feedback.js @@ -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"; diff --git a/packages/skin-database/legacy-client/src/components/DownloadText.js b/packages/skin-database/legacy-client/src/components/DownloadText.js index 36dd4766..d966ac77 100644 --- a/packages/skin-database/legacy-client/src/components/DownloadText.js +++ b/packages/skin-database/legacy-client/src/components/DownloadText.js @@ -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); diff --git a/packages/skin-database/legacy-client/src/hashFile.js b/packages/skin-database/legacy-client/src/hashFile.js index 9ceca8a1..422d6509 100644 --- a/packages/skin-database/legacy-client/src/hashFile.js +++ b/packages/skin-database/legacy-client/src/hashFile.js @@ -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)); diff --git a/packages/skin-database/legacy-client/src/upload/UploadRow.js b/packages/skin-database/legacy-client/src/upload/UploadRow.js index a7347790..dbc13ccc 100644 --- a/packages/skin-database/legacy-client/src/upload/UploadRow.js +++ b/packages/skin-database/legacy-client/src/upload/UploadRow.js @@ -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 diff --git a/packages/skin-database/legacy-client/src/utils.js b/packages/skin-database/legacy-client/src/utils.js index 0b73d741..9f52cd78 100644 --- a/packages/skin-database/legacy-client/src/utils.js +++ b/packages/skin-database/legacy-client/src/utils.js @@ -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], diff --git a/packages/skin-database/tasks/screenshotSkin.ts b/packages/skin-database/tasks/screenshotSkin.ts index 46c2902d..ebc33460 100644 --- a/packages/skin-database/tasks/screenshotSkin.ts +++ b/packages/skin-database/tasks/screenshotSkin.ts @@ -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) { diff --git a/packages/skin-museum-og/pages/api/og.js b/packages/skin-museum-og/pages/api/og.js index 62c18364..5f2e9c96 100644 --- a/packages/skin-museum-og/pages/api/og.js +++ b/packages/skin-museum-og/pages/api/og.js @@ -98,11 +98,10 @@ async function searchImage(query) { }); } -function HeaderGrid({ skins, title }) { +function HeaderGrid({ skins, _title }) { return (
; } -interface State { - selected: boolean; -} - function PlaylistMenu(props: Props) { const [selected, setSelected] = useState(false); diff --git a/packages/webamp/js/components/PlaylistWindow/SortContextMenu.tsx b/packages/webamp/js/components/PlaylistWindow/SortContextMenu.tsx index ca899fc5..9c732639 100644 --- a/packages/webamp/js/components/PlaylistWindow/SortContextMenu.tsx +++ b/packages/webamp/js/components/PlaylistWindow/SortContextMenu.tsx @@ -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() { diff --git a/packages/webamp/js/components/ResizeTarget.tsx b/packages/webamp/js/components/ResizeTarget.tsx index 2af3092c..bd34e4e5 100644 --- a/packages/webamp/js/components/ResizeTarget.tsx +++ b/packages/webamp/js/components/ResizeTarget.tsx @@ -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 diff --git a/packages/webamp/js/components/VisPainter.ts b/packages/webamp/js/components/VisPainter.ts index 645abcd8..edb0540a 100644 --- a/packages/webamp/js/components/VisPainter.ts +++ b/packages/webamp/js/components/VisPainter.ts @@ -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; } } diff --git a/packages/webamp/js/components/WinampButton.tsx b/packages/webamp/js/components/WinampButton.tsx index 8fea9e0f..9e3c5ee0 100644 --- a/packages/webamp/js/components/WinampButton.tsx +++ b/packages/webamp/js/components/WinampButton.tsx @@ -62,7 +62,7 @@ export default function WinampButton({ } setActive(true); - function onRelease(ee: PointerEvent) { + function onRelease(_ee: PointerEvent) { setActive(false); document.removeEventListener("pointerup", onRelease); } diff --git a/packages/webamp/js/media/elementSource.ts b/packages/webamp/js/media/elementSource.ts index e0edc9e1..3e222255 100644 --- a/packages/webamp/js/media/elementSource.ts +++ b/packages/webamp/js/media/elementSource.ts @@ -123,7 +123,7 @@ export default class ElementSource { try { await this._audio.play(); // TODO #race - } catch (err) { + } catch (_err) { // } this._setStatus(MEDIA_STATUS.PLAYING); diff --git a/packages/webamp/js/playlistHtml.tsx b/packages/webamp/js/playlistHtml.tsx index 595f7222..deed977b 100644 --- a/packages/webamp/js/playlistHtml.tsx +++ b/packages/webamp/js/playlistHtml.tsx @@ -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 ; }; diff --git a/packages/webamp/js/reducers/tracks.ts b/packages/webamp/js/reducers/tracks.ts index 2eabf495..512d6b7f 100644 --- a/packages/webamp/js/reducers/tracks.ts +++ b/packages/webamp/js/reducers/tracks.ts @@ -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; } diff --git a/packages/webamp/js/reducers/windows.ts b/packages/webamp/js/reducers/windows.ts index 3a05af7f..e3fc203a 100644 --- a/packages/webamp/js/reducers/windows.ts +++ b/packages/webamp/js/reducers/windows.ts @@ -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, diff --git a/packages/webamp/js/resizeUtils.ts b/packages/webamp/js/resizeUtils.ts index 7eba1566..acace84f 100644 --- a/packages/webamp/js/resizeUtils.ts +++ b/packages/webamp/js/resizeUtils.ts @@ -1,4 +1,4 @@ -import { WindowInfo, WindowId } from "./types"; +import { WindowInfo } from "./types"; interface NewGraph { [key: string]: { diff --git a/packages/webamp/js/selectors.ts b/packages/webamp/js/selectors.ts index f973ac04..a0ec2065 100644 --- a/packages/webamp/js/selectors.ts +++ b/packages/webamp/js/selectors.ts @@ -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"; diff --git a/packages/webamp/js/skinParserUtils.ts b/packages/webamp/js/skinParserUtils.ts index 30e3a31a..b7efa2af 100644 --- a/packages/webamp/js/skinParserUtils.ts +++ b/packages/webamp/js/skinParserUtils.ts @@ -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; } diff --git a/packages/webamp/js/webampWithButterchurn.ts b/packages/webamp/js/webampWithButterchurn.ts index 10ef253e..ec50c056 100644 --- a/packages/webamp/js/webampWithButterchurn.ts +++ b/packages/webamp/js/webampWithButterchurn.ts @@ -1,4 +1,4 @@ -import { Options, Preset } from "./types"; +import { Options } from "./types"; import { PrivateOptions } from "./webampLazy"; import Webamp from "./webamp"; // @ts-ignore diff --git a/turbo.json b/turbo.json index 2d1ffbc9..2e31e31c 100644 --- a/turbo.json +++ b/turbo.json @@ -67,6 +67,7 @@ "ani-cursor#lint": {}, "skin-database#lint": {}, "skin-museum-og#lint": {}, + "webamp-docs#lint": {}, "webamp-modern#lint": {}, "winamp-eqf#lint": {}, "dev": {