From 9759fcc7710515622efa1d2a65440ecb1f69de61 Mon Sep 17 00:00:00 2001 From: chrisae9 Date: Thu, 6 Aug 2026 20:29:30 -0400 Subject: [PATCH 1/7] build(eslint): migrate to supported flat config --- .eslintrc.js | 50 --- eslint.config.mjs | 346 ++++++++++++++++ package-lock.json | 673 +++++++++++++++---------------- package.json | 9 +- src/components/PlexThumbnail.vue | 1 - src/views/RoomJoin.vue | 3 +- 6 files changed, 684 insertions(+), 398 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 eslint.config.mjs diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index f0f298762c..0000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,50 +0,0 @@ -const path = require('node:path'); - -module.exports = { - root: true, - env: { - browser: true, - es2020: true, - }, - extends: ['plugin:vue/vue3-recommended', 'airbnb-base'], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - }, - rules: { - 'max-len': [ - 'error', - { - code: 120, - }, - ], - - 'no-console': 'off', - 'no-param-reassign': ['error', { - props: true, - ignorePropertyModificationsFor: ['state', 'el'], - }], - 'no-restricted-syntax': 'off', - 'no-underscore-dangle': 'off', - 'class-methods-use-this': 'off', - 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], - 'vuejs-accessibility/alt-text': 'off', - 'vuejs-accessibility/mouse-events-have-key-events': 'off', - 'vuejs-accessibility/label-has-for': 'off', - 'vuejs-accessibility/media-has-caption': 'off', - 'vuejs-accessibility/click-events-have-key-events': 'off', - 'vue/no-v-text-v-html-on-component': 'off', - 'vue/no-template-target-blank': 'off', - 'import/no-unresolved': ['error', { - ignore: ['^@/'], - }], - }, - settings: { - 'import/resolver': { - alias: { - map: [['@', path.resolve(__dirname, './src')]], - extensions: ['.js', '.vue', '.json'], - }, - }, - }, -}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..a0d427ad46 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,346 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import js from '@eslint/js'; +import stylistic from '@stylistic/eslint-plugin'; +import confusingBrowserGlobals from 'confusing-browser-globals'; +import importPlugin from 'eslint-plugin-import'; +import vue from 'eslint-plugin-vue'; +import globals from 'globals'; + +const projectRoot = path.dirname(fileURLToPath(import.meta.url)); +const lintFiles = [ + 'src/**/*.{js,vue}', + 'packages/syncloungeserver/src/**/*.js', + 'poster-proxy.js', +]; + +export default [ + { + name: 'synclounge/ignores', + ignores: ['**/dist/**', '**/node_modules/**'], + }, + { + ...js.configs.recommended, + name: 'synclounge/eslint-recommended', + files: lintFiles, + }, + { + ...importPlugin.flatConfigs.recommended, + name: 'synclounge/import-recommended', + files: lintFiles, + }, + ...vue.configs['flat/recommended'].map((config) => ({ + ...config, + files: lintFiles, + })), + { + ...stylistic.configs.customize({ + indent: 2, + quotes: 'single', + semi: true, + jsx: false, + }), + name: 'synclounge/style', + files: lintFiles, + }, + { + name: 'synclounge/project-rules', + files: lintFiles, + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + }, + }, + linterOptions: { + // ESLint 9 reports stale directives by default; retain the previous + // behavior until those comments can be cleaned up independently. + reportUnusedDisableDirectives: 'off', + }, + rules: { + // Preserve the high-value correctness and maintainability rules from the + // unsupported Airbnb preset that are not covered by recommended configs. + 'array-callback-return': ['error', { allowImplicit: true }], + 'arrow-body-style': ['error', 'as-needed', { requireReturnForObjectLiteral: false }], + 'block-scoped-var': 'error', + camelcase: ['error', { + properties: 'never', + ignoreDestructuring: false, + ignoreImports: false, + ignoreGlobals: false, + }], + 'consistent-return': 'error', + curly: ['error', 'multi-line'], + 'default-case': ['error', { commentPattern: '^no default$' }], + 'default-case-last': 'error', + 'default-param-last': 'error', + 'dot-notation': ['error', { allowKeywords: true }], + eqeqeq: ['error', 'always', { null: 'ignore' }], + 'func-names': 'warn', + 'grouped-accessor-pairs': 'error', + 'guard-for-in': 'error', + 'global-require': 'error', + 'max-classes-per-file': ['error', 1], + 'max-len': ['error', { code: 120 }], + 'new-cap': ['error', { + newIsCap: true, + capIsNew: false, + capIsNewExceptions: ['Immutable.Map', 'Immutable.Set', 'Immutable.List'], + properties: true, + }], + 'no-alert': 'warn', + 'no-array-constructor': 'error', + 'no-await-in-loop': 'error', + 'no-bitwise': 'error', + 'no-buffer-constructor': 'error', + 'no-caller': 'error', + 'no-console': 'off', + 'no-constructor-return': 'error', + 'no-continue': 'error', + 'no-else-return': ['error', { allowElseIf: false }], + 'no-empty-function': ['error', { allow: ['arrowFunctions', 'functions', 'methods'] }], + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-implied-eval': 'error', + 'no-inner-declarations': 'error', + 'no-iterator': 'error', + 'no-labels': ['error', { allowLoop: false, allowSwitch: false }], + 'no-label-var': 'error', + 'no-lone-blocks': 'error', + 'no-lonely-if': 'error', + 'no-loop-func': 'error', + 'no-multi-assign': 'error', + 'no-multi-str': 'error', + 'no-nested-ternary': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-require': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-param-reassign': ['error', { + props: true, + ignorePropertyModificationsFor: ['state', 'el'], + }], + 'no-plusplus': 'error', + 'no-promise-executor-return': 'error', + 'no-proto': 'error', + 'no-path-concat': 'error', + 'no-restricted-exports': ['error', { restrictedNamedExports: ['default', 'then'] }], + 'no-restricted-globals': ['error', + ...confusingBrowserGlobals, + { name: 'isFinite', message: 'Use Number.isFinite instead.' }, + { name: 'isNaN', message: 'Use Number.isNaN instead.' }, + ], + 'no-restricted-properties': ['error', + { object: 'arguments', property: 'callee', message: 'arguments.callee is deprecated.' }, + { property: '__defineGetter__', message: 'Use Object.defineProperty instead.' }, + { property: '__defineSetter__', message: 'Use Object.defineProperty instead.' }, + { object: 'Math', property: 'pow', message: 'Use the exponentiation operator (**) instead.' }, + ], + 'no-return-assign': ['error', 'always'], + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-shadow': 'error', + 'no-shadow-restricted-names': 'error', + 'no-throw-literal': 'error', + 'no-template-curly-in-string': 'error', + 'no-underscore-dangle': 'off', + 'no-undef-init': 'error', + 'no-unneeded-ternary': ['error', { defaultAssignment: false }], + 'no-unreachable-loop': 'error', + 'no-use-before-define': ['error', { functions: true, classes: true, variables: true }], + 'no-unused-vars': ['error', { + vars: 'all', + args: 'after-used', + caughtErrors: 'none', + ignoreRestSiblings: true, + }], + 'no-unused-expressions': ['error', { + allowShortCircuit: false, + allowTernary: false, + allowTaggedTemplates: false, + }], + 'no-useless-computed-key': 'error', + 'no-useless-constructor': 'error', + 'no-useless-call': 'error', + 'no-useless-concat': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-var': 'error', + 'no-void': 'error', + 'object-shorthand': ['error', 'always', { avoidQuotes: true }], + 'one-var': ['error', 'never'], + 'operator-assignment': ['error', 'always'], + 'prefer-arrow-callback': ['error', { allowNamedFunctions: false, allowUnboundThis: true }], + 'prefer-const': ['error', { destructuring: 'any', ignoreReadBeforeAssign: true }], + 'prefer-destructuring': ['error', { + VariableDeclarator: { array: false, object: true }, + AssignmentExpression: { array: true, object: false }, + }, { enforceForRenamedProperties: false }], + 'prefer-exponentiation-operator': 'error', + 'prefer-numeric-literals': 'error', + 'prefer-object-spread': 'error', + 'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }], + 'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }], + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + radix: 'error', + strict: ['error', 'never'], + 'symbol-description': 'error', + 'unicode-bom': ['error', 'never'], + 'vars-on-top': 'error', + yoda: 'error', + + // Match the previous Airbnb formatting choices where Stylistic's + // modern defaults intentionally differ. + '@stylistic/arrow-parens': ['error', 'always'], + '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }], + '@stylistic/no-confusing-arrow': ['error', { allowParens: true }], + '@stylistic/comma-dangle': ['error', { + arrays: 'always-multiline', + objects: 'always-multiline', + imports: 'always-multiline', + exports: 'always-multiline', + functions: 'always-multiline', + }], + '@stylistic/comma-style': ['error', 'last'], + '@stylistic/computed-property-spacing': ['error', 'never'], + '@stylistic/function-call-argument-newline': ['error', 'consistent'], + '@stylistic/function-call-spacing': ['error', 'never'], + '@stylistic/function-paren-newline': ['error', 'multiline-arguments'], + '@stylistic/implicit-arrow-linebreak': ['error', 'beside'], + '@stylistic/indent': ['error', 2, { + SwitchCase: 1, + VariableDeclarator: 1, + outerIIFEBody: 1, + FunctionDeclaration: { parameters: 1, body: 1 }, + FunctionExpression: { parameters: 1, body: 1 }, + CallExpression: { arguments: 1 }, + ArrayExpression: 1, + ObjectExpression: 1, + ImportDeclaration: 1, + flatTernaryExpressions: false, + ignoredNodes: [ + 'JSXElement', + 'JSXElement > *', + 'JSXAttribute', + 'JSXIdentifier', + 'JSXNamespacedName', + 'JSXMemberExpression', + 'JSXSpreadAttribute', + 'JSXExpressionContainer', + 'JSXOpeningElement', + 'JSXClosingElement', + 'JSXFragment', + 'JSXOpeningFragment', + 'JSXClosingFragment', + 'JSXText', + 'JSXEmptyExpression', + 'JSXSpreadChild', + ], + ignoreComments: false, + offsetTernaryExpressions: false, + }], + '@stylistic/indent-binary-ops': 'off', + '@stylistic/linebreak-style': ['error', 'unix'], + '@stylistic/lines-between-class-members': ['error', 'always', { + exceptAfterSingleLine: false, + }], + '@stylistic/newline-per-chained-call': ['error', { ignoreChainWithDepth: 4 }], + '@stylistic/max-statements-per-line': 'off', + '@stylistic/member-delimiter-style': 'off', + '@stylistic/multiline-ternary': 'off', + '@stylistic/no-extra-parens': 'off', + '@stylistic/no-extra-semi': 'error', + '@stylistic/no-mixed-operators': ['error', { + groups: [ + ['%', '**'], + ['%', '+'], + ['%', '-'], + ['%', '*'], + ['%', '/'], + ['/', '*'], + ['&', '|', '<<', '>>', '>>>'], + ['==', '!=', '===', '!=='], + ['&&', '||'], + ], + allowSamePrecedence: false, + }], + '@stylistic/operator-linebreak': ['error', 'before', { overrides: { '=': 'none' } }], + '@stylistic/nonblock-statement-body-position': ['error', 'beside'], + '@stylistic/object-curly-newline': ['error', { + ObjectExpression: { minProperties: 4, multiline: true, consistent: true }, + ObjectPattern: { minProperties: 4, multiline: true, consistent: true }, + ImportDeclaration: { minProperties: 4, multiline: true, consistent: true }, + ExportDeclaration: { minProperties: 4, multiline: true, consistent: true }, + }], + '@stylistic/object-property-newline': ['error', { allowAllPropertiesOnSameLine: true }], + '@stylistic/one-var-declaration-per-line': ['error', 'always'], + '@stylistic/padded-blocks': ['error', { + blocks: 'never', + classes: 'never', + switches: 'never', + }, { allowSingleLineBlocks: true }], + '@stylistic/quote-props': ['error', 'as-needed', { + keywords: false, + unnecessary: true, + numbers: false, + }], + '@stylistic/quotes': ['error', 'single', { avoidEscape: true }], + '@stylistic/semi-style': ['error', 'last'], + '@stylistic/spaced-comment': ['error', 'always', { + line: { exceptions: ['-', '+'], markers: ['=', '!', '/'] }, + block: { exceptions: ['-', '+'], markers: ['=', '!', ':', '::'], balanced: true }, + }], + '@stylistic/type-annotation-spacing': 'off', + '@stylistic/type-generic-spacing': 'off', + '@stylistic/type-named-tuple-spacing': 'off', + '@stylistic/switch-colon-spacing': ['error', { after: true, before: false }], + '@stylistic/wrap-iife': ['error', 'outside', { functionPrototypeMethods: false }], + + 'import/extensions': ['error', 'ignorePackages', { + js: 'never', + mjs: 'never', + jsx: 'never', + }], + 'import/first': 'error', + 'import/newline-after-import': 'error', + 'import/no-absolute-path': 'error', + 'import/no-amd': 'error', + 'import/no-cycle': 'error', + 'import/no-duplicates': 'error', + 'import/no-dynamic-require': 'error', + 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], + 'import/no-import-module-exports': 'error', + 'import/no-mutable-exports': 'error', + 'import/no-named-as-default': 'error', + 'import/no-named-as-default-member': 'error', + 'import/no-named-default': 'error', + 'import/no-relative-packages': 'error', + 'import/no-self-import': 'error', + 'import/no-unresolved': ['error', { ignore: ['^@/'] }], + 'import/no-useless-path-segments': ['error', { commonjs: true }], + 'import/no-webpack-loader-syntax': 'error', + 'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }], + 'import/prefer-default-export': 'error', + + 'vue/no-template-target-blank': 'off', + 'vue/no-v-text-v-html-on-component': 'off', + }, + settings: { + 'import/resolver': { + alias: { + map: [['@', path.resolve(projectRoot, 'src')]], + extensions: ['.js', '.vue', '.json'], + }, + }, + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 3d4ed829ce..12893e468e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,17 +24,20 @@ "@babel/core": "^7.29.7", "@babel/preset-env": "^7.29.7", "@delucis/if-env": "^1.1.2", + "@eslint/js": "^9.39.5", + "@stylistic/eslint-plugin": "^5.10.0", "@vitejs/plugin-vue": "^5.2.0", "@vue/test-utils": "^2.4.6", "caf": "^15.0.1", + "confusing-browser-globals": "^1.0.11", "date-fns": "^2.30.0", "detect-browser": "^5.3.0", - "eslint": "^8.57.1", - "eslint-config-airbnb-base": "^15.0.0", + "eslint": "^9.39.5", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-vue": "^9.32.0", + "eslint-plugin-vue": "^10.10.0", "fast-xml-parser": "^5.10.1", + "globals": "^17.9.0", "happy-dom": "^20.11.1", "mux.js": "^6.3.0", "sass": "^1.93.2", @@ -2260,54 +2263,157 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -2324,13 +2430,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -2470,44 +2582,6 @@ ], "license": "MIT" }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@one-ini/wasm": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", @@ -3241,6 +3315,40 @@ "dev": true, "license": "MIT" }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", + "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -3275,6 +3383,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -3307,12 +3422,19 @@ "@types/node": "*" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", - "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "node_modules/@typescript-eslint/types": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", @@ -4642,19 +4764,6 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", @@ -5072,80 +5181,63 @@ } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", - "dev": true, - "license": "MIT", - "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" + "url": "https://eslint.org/donate" }, "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.2" + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-import-resolver-alias": { @@ -5269,26 +5361,35 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", - "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz", + "integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "globals": "^13.24.0", + "@eslint-community/eslint-utils": "^4.9.1", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.15", - "semver": "^7.6.3", - "vue-eslint-parser": "^9.4.3", - "xml-name-validator": "^4.0.0" + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.3.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } } }, "node_modules/eslint-plugin-vue/node_modules/semver": { @@ -5305,9 +5406,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5315,7 +5416,7 @@ "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -5334,19 +5435,45 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -5547,16 +5674,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5576,16 +5693,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -5653,18 +5770,17 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { @@ -5954,16 +6070,13 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", + "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5998,13 +6111,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/happy-dom": { "version": "20.11.1", "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.1.tgz", @@ -6528,16 +6634,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6872,13 +6968,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -7649,9 +7738,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "dev": true, "license": "MIT", "dependencies": { @@ -7737,27 +7826,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7941,56 +8009,6 @@ "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.62.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", @@ -8037,30 +8055,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", @@ -8755,13 +8749,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -8855,19 +8842,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -9363,28 +9337,42 @@ "license": "MIT" }, "node_modules/vue-eslint-parser": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", - "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz", + "integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "eslint": ">=6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/vue-eslint-parser/node_modules/semver": { @@ -9393,6 +9381,7 @@ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -9729,13 +9718,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xml-naming": { diff --git a/package.json b/package.json index 4ab6d75d78..a292289fc8 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "utf-8-validate": "^6.0.4" }, "devDependencies": { + "@eslint/js": "^9.39.5", + "@stylistic/eslint-plugin": "^5.10.0", "@babel/cli": "^7.29.7", "@babel/core": "^7.29.7", "@babel/preset-env": "^7.29.7", @@ -47,14 +49,15 @@ "@vitejs/plugin-vue": "^5.2.0", "@vue/test-utils": "^2.4.6", "caf": "^15.0.1", + "confusing-browser-globals": "^1.0.11", "date-fns": "^2.30.0", "detect-browser": "^5.3.0", - "eslint": "^8.57.1", - "eslint-config-airbnb-base": "^15.0.0", + "eslint": "^9.39.5", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-vue": "^9.32.0", + "eslint-plugin-vue": "^10.10.0", "fast-xml-parser": "^5.10.1", + "globals": "^17.9.0", "happy-dom": "^20.11.1", "mux.js": "^6.3.0", "sass": "^1.93.2", diff --git a/src/components/PlexThumbnail.vue b/src/components/PlexThumbnail.vue index 98c2147de9..fb55200daf 100644 --- a/src/components/PlexThumbnail.vue +++ b/src/components/PlexThumbnail.vue @@ -409,4 +409,3 @@ export default { right: 0; } - diff --git a/src/views/RoomJoin.vue b/src/views/RoomJoin.vue index 06665b1572..e0036965ff 100644 --- a/src/views/RoomJoin.vue +++ b/src/views/RoomJoin.vue @@ -117,8 +117,7 @@ export default { this.loading = true; const startedOnRoomJoin = this.$route.name === 'RoomJoin'; - const redirect = this.$route.query.redirect; - const watching = this.$route.query.watching; + const { redirect, watching } = this.$route.query; const safeRedirect = this.getSafeRoomRedirect(redirect); const syncOnJoin = !safeRedirect || this.$router.resolve(safeRedirect).name === 'WebPlayer'; From 533bd33e3f61914a153fabb98ee0ac9a28e6eb84 Mon Sep 17 00:00:00 2001 From: chrisae9 Date: Thu, 6 Aug 2026 21:20:27 -0400 Subject: [PATCH 2/7] build(eslint): preserve lint policy coverage --- eslint.config.mjs | 56 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index a0d427ad46..749e108324 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,11 +9,16 @@ import vue from 'eslint-plugin-vue'; import globals from 'globals'; const projectRoot = path.dirname(fileURLToPath(import.meta.url)); -const lintFiles = [ - 'src/**/*.{js,vue}', +const javascriptFiles = [ + '*.js', + '*.mjs', + 'config/**/*.js', 'packages/syncloungeserver/src/**/*.js', - 'poster-proxy.js', + 'src/**/*.js', + 'test/**/*.js', ]; +const vueFiles = ['src/**/*.vue']; +const lintFiles = [...javascriptFiles, ...vueFiles]; export default [ { @@ -32,7 +37,7 @@ export default [ }, ...vue.configs['flat/recommended'].map((config) => ({ ...config, - files: lintFiles, + files: config.files ?? vueFiles, })), { ...stylistic.configs.customize({ @@ -50,10 +55,6 @@ export default [ languageOptions: { ecmaVersion: 'latest', sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - }, }, linterOptions: { // ESLint 9 reports stale directives by default; retain the previous @@ -80,6 +81,7 @@ export default [ 'dot-notation': ['error', { allowKeywords: true }], eqeqeq: ['error', 'always', { null: 'ignore' }], 'func-names': 'warn', + 'getter-return': ['error', { allowImplicit: true }], 'grouped-accessor-pairs': 'error', 'guard-for-in': 'error', 'global-require': 'error', @@ -97,7 +99,9 @@ export default [ 'no-bitwise': 'error', 'no-buffer-constructor': 'error', 'no-caller': 'error', + 'no-cond-assign': ['error', 'always'], 'no-console': 'off', + 'no-constant-condition': 'warn', 'no-constructor-return': 'error', 'no-continue': 'error', 'no-else-return': ['error', { allowElseIf: false }], @@ -121,6 +125,7 @@ export default [ 'no-new-func': 'error', 'no-new-require': 'error', 'no-new-wrappers': 'error', + 'no-object-constructor': 'error', 'no-octal-escape': 'error', 'no-param-reassign': ['error', { props: true, @@ -138,6 +143,12 @@ export default [ ], 'no-restricted-properties': ['error', { object: 'arguments', property: 'callee', message: 'arguments.callee is deprecated.' }, + { object: 'global', property: 'isFinite', message: 'Use Number.isFinite instead.' }, + { object: 'self', property: 'isFinite', message: 'Use Number.isFinite instead.' }, + { object: 'window', property: 'isFinite', message: 'Use Number.isFinite instead.' }, + { object: 'global', property: 'isNaN', message: 'Use Number.isNaN instead.' }, + { object: 'self', property: 'isNaN', message: 'Use Number.isNaN instead.' }, + { object: 'window', property: 'isNaN', message: 'Use Number.isNaN instead.' }, { property: '__defineGetter__', message: 'Use Object.defineProperty instead.' }, { property: '__defineSetter__', message: 'Use Object.defineProperty instead.' }, { object: 'Math', property: 'pow', message: 'Use the exponentiation operator (**) instead.' }, @@ -154,6 +165,7 @@ export default [ 'no-undef-init': 'error', 'no-unneeded-ternary': ['error', { defaultAssignment: false }], 'no-unreachable-loop': 'error', + 'no-unsafe-optional-chaining': ['error', { disallowArithmeticOperators: true }], 'no-use-before-define': ['error', { functions: true, classes: true, variables: true }], 'no-unused-vars': ['error', { vars: 'all', @@ -195,6 +207,7 @@ export default [ strict: ['error', 'never'], 'symbol-description': 'error', 'unicode-bom': ['error', 'never'], + 'valid-typeof': ['error', { requireStringLiterals: true }], 'vars-on-top': 'error', yoda: 'error', @@ -288,6 +301,12 @@ export default [ classes: 'never', switches: 'never', }, { allowSingleLineBlocks: true }], + '@stylistic/padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: 'directive', next: '*' }, + { blankLine: 'always', prev: '*', next: 'directive' }, + { blankLine: 'any', prev: 'directive', next: 'directive' }, + ], '@stylistic/quote-props': ['error', 'as-needed', { keywords: false, unnecessary: true, @@ -343,4 +362,25 @@ export default [ }, }, }, + { + name: 'synclounge/browser-globals', + files: ['src/**/*.{js,vue}'], + languageOptions: { + globals: globals.browser, + }, + }, + { + name: 'synclounge/test-globals', + files: ['src/__tests__/**/*.js'], + languageOptions: { + globals: globals.node, + }, + }, + { + name: 'synclounge/node-globals', + files: javascriptFiles.filter((file) => !file.startsWith('src/')), + languageOptions: { + globals: globals.node, + }, + }, ]; From 92dc32c9748be1694c9dd6c462384a07d37f738e Mon Sep 17 00:00:00 2001 From: chrisae9 Date: Thu, 6 Aug 2026 12:31:07 -0400 Subject: [PATCH 3/7] test: bring Node suites under lint rules --- test/cache.test.js | 4 ++-- test/combineurl.test.js | 2 ++ test/kick.test.js | 14 ++++++++++---- test/server.test.js | 25 ++++++++++++++++++------- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/test/cache.test.js b/test/cache.test.js index 57a2f7aca9..df71d3a0b2 100644 --- a/test/cache.test.js +++ b/test/cache.test.js @@ -74,7 +74,7 @@ describe('cache', () => { const { setMetadata, getMetadata, metadataCache } = createCache(); // Fill cache to max - for (let i = 0; i < METADATA_MAX_SIZE; i++) { + for (let i = 0; i < METADATA_MAX_SIZE; i += 1) { setMetadata(`key${i}`, { title: `Entry ${i}` }); } assert.equal(metadataCache.size, METADATA_MAX_SIZE); @@ -90,7 +90,7 @@ describe('cache', () => { it('does not evict when updating an existing key', () => { const { setMetadata, getMetadata, metadataCache } = createCache(); - for (let i = 0; i < METADATA_MAX_SIZE; i++) { + for (let i = 0; i < METADATA_MAX_SIZE; i += 1) { setMetadata(`key${i}`, { title: `Entry ${i}` }); } diff --git a/test/combineurl.test.js b/test/combineurl.test.js index b792a0d50e..0630e3387d 100644 --- a/test/combineurl.test.js +++ b/test/combineurl.test.js @@ -7,6 +7,8 @@ let combineRelativeUrlParts; describe('combineurl', () => { before(async () => { + // Node ESM resolution requires the extension for this CommonJS dynamic import. + // eslint-disable-next-line import/extensions const mod = await import('../src/utils/combineurl.js'); combineUrl = mod.combineUrl; combineRelativeUrlParts = mod.combineRelativeUrlParts; diff --git a/test/kick.test.js b/test/kick.test.js index 4552942b21..08d33538d0 100644 --- a/test/kick.test.js +++ b/test/kick.test.js @@ -1,4 +1,6 @@ -const { describe, it, before, after } = require('node:test'); +const { + describe, it, before, after, +} = require('node:test'); const assert = require('node:assert/strict'); const { spawn } = require('node:child_process'); const { io } = require('socket.io-client'); @@ -10,11 +12,12 @@ const wait = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }); function waitForSocketEvent(socket, eventName, timeoutMs = 2000) { return new Promise((resolve, reject) => { + let onEvent; const timeout = setTimeout(() => { socket.off(eventName, onEvent); reject(new Error(`Timed out waiting for ${eventName}`)); }, timeoutMs); - const onEvent = (data) => { + onEvent = (data) => { clearTimeout(timeout); resolve(data); }; @@ -23,11 +26,14 @@ function waitForSocketEvent(socket, eventName, timeoutMs = 2000) { } async function waitForServer(url, retries = 30, delay = 200) { - for (let i = 0; i < retries; i++) { + for (let i = 0; i < retries; i += 1) { try { + // Sequential polling is intentional: each request observes a later server state. + // eslint-disable-next-line no-await-in-loop await fetch(url); return; } catch { + // eslint-disable-next-line no-await-in-loop await wait(delay); } } @@ -76,7 +82,7 @@ function joinClient({ roomId, username }) { describe('kick socket event', () => { before(async () => { serverProcess = spawn('node', ['server.js'], { - cwd: __dirname + '/..', + cwd: `${__dirname}/..`, env: { ...process.env, PORT: '18089', diff --git a/test/server.test.js b/test/server.test.js index e6058a7653..56e59e6e10 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -1,5 +1,8 @@ -const { describe, it, before, after } = require('node:test'); +const { + describe, it, before, after, +} = require('node:test'); const assert = require('node:assert/strict'); +const { spawn } = require('node:child_process'); const http = require('node:http'); let baseUrl; @@ -13,9 +16,13 @@ let resolveSlowPosterClosed; async function getFreePort() { const server = http.createServer(); - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + await new Promise((resolve) => { + server.listen(0, '127.0.0.1', resolve); + }); const { port } = server.address(); - await new Promise((resolve) => server.close(resolve)); + await new Promise((resolve) => { + server.close(resolve); + }); return port; } @@ -38,12 +45,17 @@ function metadataBodyWithExactSize(size) { } async function waitForServer(url, retries = 30, delay = 200) { - for (let i = 0; i < retries; i++) { + for (let i = 0; i < retries; i += 1) { try { + // Sequential polling is intentional: each request observes a later server state. + // eslint-disable-next-line no-await-in-loop await fetch(url); return; } catch { - await new Promise((r) => setTimeout(r, delay)); + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, delay); + }); } } throw new Error('Server did not start in time'); @@ -104,11 +116,10 @@ describe('server', () => { }); posterFixtureBase = `http://127.0.0.1:${posterFixtureServer.address().port}`; - const { spawn } = require('node:child_process'); const port = await getFreePort(); baseUrl = `http://127.0.0.1:${port}`; serverProcess = spawn('node', ['server.js'], { - cwd: __dirname + '/..', + cwd: `${__dirname}/..`, env: { ...process.env, PORT: String(port), From c2f53bcbc88755cefe6ec7241a8706364da2fac7 Mon Sep 17 00:00:00 2001 From: chrisae9 Date: Thu, 6 Aug 2026 12:36:25 -0400 Subject: [PATCH 4/7] test: await spawned resource cleanup --- test/kick.test.js | 34 ++++++++++++++++++++------- test/server.test.js | 57 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/test/kick.test.js b/test/kick.test.js index 08d33538d0..c74511b0bd 100644 --- a/test/kick.test.js +++ b/test/kick.test.js @@ -30,16 +30,36 @@ async function waitForServer(url, retries = 30, delay = 200) { try { // Sequential polling is intentional: each request observes a later server state. // eslint-disable-next-line no-await-in-loop - await fetch(url); - return; - } catch { + const response = await fetch(url, { signal: AbortSignal.timeout(500) }); + const healthy = response.ok; // eslint-disable-next-line no-await-in-loop - await wait(delay); + await response.body?.cancel(); + if (healthy) return; + } catch { + // Retry connection failures and per-attempt timeouts. } + // eslint-disable-next-line no-await-in-loop + await wait(delay); } throw new Error('Server did not start in time'); } +async function stopServer(processToStop) { + if (!processToStop || processToStop.exitCode !== null) return; + const exited = new Promise((resolve) => { + processToStop.once('exit', resolve); + }); + processToStop.kill('SIGTERM'); + const forceKill = setTimeout(() => { + if (processToStop.exitCode === null) processToStop.kill('SIGKILL'); + }, 2000); + try { + await exited; + } finally { + clearTimeout(forceKill); + } +} + function joinClient({ roomId, username }) { return new Promise((resolve, reject) => { const socket = io(BASE, { @@ -95,10 +115,8 @@ describe('kick socket event', () => { await waitForServer(`${BASE}/health`); }); - after(() => { - if (serverProcess) { - serverProcess.kill('SIGTERM'); - } + after(async () => { + await stopServer(serverProcess); }); it('server removes kicked users even if the kicked client does not disconnect itself', async () => { diff --git a/test/server.test.js b/test/server.test.js index 56e59e6e10..d40f015e9d 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -49,18 +49,49 @@ async function waitForServer(url, retries = 30, delay = 200) { try { // Sequential polling is intentional: each request observes a later server state. // eslint-disable-next-line no-await-in-loop - await fetch(url); - return; - } catch { + const response = await fetch(url, { signal: AbortSignal.timeout(500) }); + const healthy = response.ok; // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { - setTimeout(resolve, delay); - }); + await response.body?.cancel(); + if (healthy) return; + } catch { + // Retry connection failures and per-attempt timeouts. } + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, delay); + }); } throw new Error('Server did not start in time'); } +async function stopServer(processToStop) { + if (!processToStop || processToStop.exitCode !== null) return; + const exited = new Promise((resolve) => { + processToStop.once('exit', resolve); + }); + processToStop.kill('SIGTERM'); + const forceKill = setTimeout(() => { + if (processToStop.exitCode === null) processToStop.kill('SIGKILL'); + }, 2000); + try { + await exited; + } finally { + clearTimeout(forceKill); + } +} + +const closeServer = (server) => new Promise((resolve, reject) => { + if (!server?.listening) { + resolve(); + return; + } + server.close((error) => { + if (error) reject(error); + else resolve(); + }); +}); + describe('server', () => { before(async () => { slowPosterStarted = new Promise((resolve) => { @@ -136,13 +167,13 @@ describe('server', () => { await waitForServer(`${baseUrl}/health`); }); - after(() => { - if (serverProcess) { - serverProcess.kill('SIGTERM'); - } - if (posterFixtureServer) { - posterFixtureServer.close(); - } + after(async () => { + const outcomes = await Promise.allSettled([ + stopServer(serverProcess), + closeServer(posterFixtureServer), + ]); + const failedCleanup = outcomes.find(({ status }) => status === 'rejected'); + if (failedCleanup) throw failedCleanup.reason; }); // --- SPA fallback --- From d3f790b2de0a03b8aa5afe1da4e66b255b461afd Mon Sep 17 00:00:00 2001 From: chrisae9 Date: Thu, 6 Aug 2026 12:45:25 -0400 Subject: [PATCH 5/7] build: lint all first-party JavaScript --- package.json | 2 +- server.js | 6 ++++-- src/__tests__/vuetify3-migration.test.js | 7 ++++--- vite.config.js | 2 -- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a292289fc8..0ad2002644 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "npm run build:server && vite build", "build:server": "babel packages/syncloungeserver/src --out-dir packages/syncloungeserver/dist --config-file ./packages/syncloungeserver/.babelrc.json", "preview": "vite preview", - "lint": "eslint src/ packages/syncloungeserver/src/ poster-proxy.js", + "lint": "npm run build:server --silent && eslint src/ packages/syncloungeserver/src/ config/ test/ *.js", "audit": "npm audit --audit-level=high", "test": "npm run build && node --test test/*.test.js && vitest run", "prepare": "if-env SKIP_BUILD=true && echo 'Skipping build' || npm run build" diff --git a/server.js b/server.js index 224e5447eb..6f58d8c7e2 100755 --- a/server.js +++ b/server.js @@ -94,8 +94,10 @@ function injectOgTags(html, meta) { ].filter(Boolean).join('\n '); // Remove existing OG/Twitter meta tags from the static HTML so we replace rather than duplicate - const existingTags = /]*\/?\s*>\s*\n?/g; - const cleaned = html.replace(existingTags, ''); + const cleaned = html.replace( + /]*\/?\s*>\s*\n?/g, + '', + ); return cleaned.replace('', ` ${tags}\n `); } diff --git a/src/__tests__/vuetify3-migration.test.js b/src/__tests__/vuetify3-migration.test.js index 67a0b7bfea..faf2d4c23e 100644 --- a/src/__tests__/vuetify3-migration.test.js +++ b/src/__tests__/vuetify3-migration.test.js @@ -14,6 +14,10 @@ import { createVuetify } from 'vuetify'; import * as components from 'vuetify/components'; import * as directives from 'vuetify/directives'; +vi.mock('vanilla-tilt', () => ({ + default: { init: vi.fn() }, +})); + // Load all .vue files as raw source for template auditing and source verification const vueFileSources = import.meta.glob('@/**/*.vue', { query: '?raw', import: 'default', eager: true }); @@ -495,9 +499,6 @@ describe('PlexThumbnail Component', () => { let PlexThumbnail; beforeEach(async () => { - vi.mock('vanilla-tilt', () => ({ - default: { init: vi.fn() }, - })); const mod = await import('@/components/PlexThumbnail.vue'); PlexThumbnail = mod.default; }); diff --git a/vite.config.js b/vite.config.js index 3575afe841..cff0339215 100644 --- a/vite.config.js +++ b/vite.config.js @@ -39,8 +39,6 @@ export function generateConfigPlugin({ }; } -const pkg = require('./package.json'); - export default defineConfig({ plugins: [ patchLibjassPlugin(), From 417e3788b536cbc85297f8ada5fe2ba538bb0932 Mon Sep 17 00:00:00 2001 From: chrisae9 Date: Thu, 6 Aug 2026 21:24:03 -0400 Subject: [PATCH 6/7] build: enforce warning-free repository lint --- eslint.config.mjs | 11 +++++ package.json | 2 +- src/components/PlexItem.vue | 1 - src/components/PlexMediaPlayDialog.vue | 16 +++--- src/components/TheSidebarRight.vue | 1 - src/views/PlexLibrary.vue | 4 +- src/views/WebPlayer.vue | 68 ++++++++++++++++---------- test/socket-validation.test.js | 24 +++++++-- 8 files changed, 84 insertions(+), 43 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 749e108324..4abf44e58f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -355,6 +355,9 @@ export default [ }, settings: { 'import/resolver': { + node: { + extensions: ['.js', '.mjs', '.json'], + }, alias: { map: [['@', path.resolve(projectRoot, 'src')]], extensions: ['.js', '.vue', '.json'], @@ -383,4 +386,12 @@ export default [ globals: globals.node, }, }, + { + name: 'synclounge/vite-config-interop', + files: ['vite.config.js'], + rules: { + // eslint-plugin-import does not resolve this package's ESM default through its exports map. + 'import/default': 'off', + }, + }, ]; diff --git a/package.json b/package.json index 0ad2002644..f4119e9352 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "npm run build:server && vite build", "build:server": "babel packages/syncloungeserver/src --out-dir packages/syncloungeserver/dist --config-file ./packages/syncloungeserver/.babelrc.json", "preview": "vite preview", - "lint": "npm run build:server --silent && eslint src/ packages/syncloungeserver/src/ config/ test/ *.js", + "lint": "npm run build:server --silent && eslint --max-warnings=0 src/ packages/syncloungeserver/src/ config/ test/ *.js *.mjs", "audit": "npm audit --audit-level=high", "test": "npm run build && node --test test/*.test.js && vitest run", "prepare": "if-env SKIP_BUILD=true && echo 'Skipping build' || npm run build" diff --git a/src/components/PlexItem.vue b/src/components/PlexItem.vue index b7062b98ce..01ee2d96e0 100644 --- a/src/components/PlexItem.vue +++ b/src/components/PlexItem.vue @@ -40,7 +40,6 @@ Stop - Party Pausing is currently {{ IS_PARTY_PAUSING_ENABLED ? 'enabled' : 'disabled' }} by the host - diff --git a/src/views/PlexLibrary.vue b/src/views/PlexLibrary.vue index 88aa061f3a..8c9df2095b 100644 --- a/src/views/PlexLibrary.vue +++ b/src/views/PlexLibrary.vue @@ -83,11 +83,11 @@ > - mdi-volume-off + + mdi-volume-off + Click to unmute @@ -62,29 +64,36 @@ no-gutters class="pa-3 d-none d-sm-flex hoverBar" > - - - - - - - - -
-
{{ GET_TITLE }}
-
{{ GET_SECONDARY_TITLE }}
-
- Playing from {{ GET_PLEX_SERVER?.name }} -
+ + + + + + + + +
+
+ {{ GET_TITLE }}
- - - - - +
+ {{ GET_SECONDARY_TITLE }} +
+
+ Playing from {{ GET_PLEX_SERVER?.name }} +
+
+
+
+
+
+
- +
-
{{ GET_TITLE }}
-
{{ GET_SECONDARY_TITLE }}
+
+ {{ GET_TITLE }} +
+
+ {{ GET_SECONDARY_TITLE }} +
Playing from {{ GET_PLEX_SERVER?.name }}
diff --git a/test/socket-validation.test.js b/test/socket-validation.test.js index 6d27093fdb..f8932bb625 100644 --- a/test/socket-validation.test.js +++ b/test/socket-validation.test.js @@ -29,13 +29,13 @@ async function waitForServer(url, retries = 30, delay = 200) { try { // Sequential polling is intentional: each request observes a later server state. // eslint-disable-next-line no-await-in-loop - const response = await fetch(url); + const response = await fetch(url, { signal: AbortSignal.timeout(500) }); const healthy = response.ok; // eslint-disable-next-line no-await-in-loop await response.body?.cancel(); if (healthy) return; } catch { - // Retry until the child process starts listening. + // Retry connection failures and per-attempt timeouts. } // eslint-disable-next-line no-await-in-loop await wait(delay); @@ -43,6 +43,22 @@ async function waitForServer(url, retries = 30, delay = 200) { throw new Error('Server did not start in time'); } +async function stopServer(processToStop) { + if (!processToStop || processToStop.exitCode !== null) return; + const exited = new Promise((resolve) => { + processToStop.once('exit', resolve); + }); + processToStop.kill('SIGTERM'); + const forceKill = setTimeout(() => { + if (processToStop.exitCode === null) processToStop.kill('SIGKILL'); + }, 2000); + try { + await exited; + } finally { + clearTimeout(forceKill); + } +} + function connectClient() { return io(baseUrl, { path: '/socket.io', @@ -110,8 +126,8 @@ describe('socket event validation', () => { await waitForServer(`${baseUrl}/health`); }); - after(() => { - if (serverProcess) serverProcess.kill('SIGTERM'); + after(async () => { + await stopServer(serverProcess); }); it('disconnects a client that does not answer the application ping', async () => { From de193d5345d2233a928321f25c965fd704155393 Mon Sep 17 00:00:00 2001 From: chrisae9 Date: Thu, 6 Aug 2026 23:24:53 -0400 Subject: [PATCH 7/7] test: bound child process cleanup --- test/kick.test.js | 33 ++++++++++++++++++++++++--------- test/server.test.js | 33 ++++++++++++++++++++++++--------- test/socket-validation.test.js | 33 ++++++++++++++++++++++++--------- 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/test/kick.test.js b/test/kick.test.js index c74511b0bd..8d7ff697a6 100644 --- a/test/kick.test.js +++ b/test/kick.test.js @@ -45,18 +45,33 @@ async function waitForServer(url, retries = 30, delay = 200) { } async function stopServer(processToStop) { - if (!processToStop || processToStop.exitCode !== null) return; + if (!processToStop + || processToStop.exitCode !== null + || processToStop.signalCode !== null) return; + const exited = new Promise((resolve) => { - processToStop.once('exit', resolve); + processToStop.once('error', resolve); + processToStop.once('close', resolve); }); + const waitForExit = async () => { + let timeout; + const didExit = await Promise.race([ + exited.then(() => true), + new Promise((resolve) => { + timeout = setTimeout(() => resolve(false), 2000); + }), + ]); + clearTimeout(timeout); + return didExit; + }; + + if (processToStop.exitCode !== null || processToStop.signalCode !== null) return; processToStop.kill('SIGTERM'); - const forceKill = setTimeout(() => { - if (processToStop.exitCode === null) processToStop.kill('SIGKILL'); - }, 2000); - try { - await exited; - } finally { - clearTimeout(forceKill); + if (!await waitForExit() + && processToStop.exitCode === null + && processToStop.signalCode === null) { + processToStop.kill('SIGKILL'); + assert.ok(await waitForExit(), 'server did not exit after SIGKILL'); } } diff --git a/test/server.test.js b/test/server.test.js index d40f015e9d..d08e62614b 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -66,18 +66,33 @@ async function waitForServer(url, retries = 30, delay = 200) { } async function stopServer(processToStop) { - if (!processToStop || processToStop.exitCode !== null) return; + if (!processToStop + || processToStop.exitCode !== null + || processToStop.signalCode !== null) return; + const exited = new Promise((resolve) => { - processToStop.once('exit', resolve); + processToStop.once('error', resolve); + processToStop.once('close', resolve); }); + const waitForExit = async () => { + let timeout; + const didExit = await Promise.race([ + exited.then(() => true), + new Promise((resolve) => { + timeout = setTimeout(() => resolve(false), 2000); + }), + ]); + clearTimeout(timeout); + return didExit; + }; + + if (processToStop.exitCode !== null || processToStop.signalCode !== null) return; processToStop.kill('SIGTERM'); - const forceKill = setTimeout(() => { - if (processToStop.exitCode === null) processToStop.kill('SIGKILL'); - }, 2000); - try { - await exited; - } finally { - clearTimeout(forceKill); + if (!await waitForExit() + && processToStop.exitCode === null + && processToStop.signalCode === null) { + processToStop.kill('SIGKILL'); + assert.ok(await waitForExit(), 'server did not exit after SIGKILL'); } } diff --git a/test/socket-validation.test.js b/test/socket-validation.test.js index f8932bb625..897b0cda0c 100644 --- a/test/socket-validation.test.js +++ b/test/socket-validation.test.js @@ -44,18 +44,33 @@ async function waitForServer(url, retries = 30, delay = 200) { } async function stopServer(processToStop) { - if (!processToStop || processToStop.exitCode !== null) return; + if (!processToStop + || processToStop.exitCode !== null + || processToStop.signalCode !== null) return; + const exited = new Promise((resolve) => { - processToStop.once('exit', resolve); + processToStop.once('error', resolve); + processToStop.once('close', resolve); }); + const waitForExit = async () => { + let timeout; + const didExit = await Promise.race([ + exited.then(() => true), + new Promise((resolve) => { + timeout = setTimeout(() => resolve(false), 2000); + }), + ]); + clearTimeout(timeout); + return didExit; + }; + + if (processToStop.exitCode !== null || processToStop.signalCode !== null) return; processToStop.kill('SIGTERM'); - const forceKill = setTimeout(() => { - if (processToStop.exitCode === null) processToStop.kill('SIGKILL'); - }, 2000); - try { - await exited; - } finally { - clearTimeout(forceKill); + if (!await waitForExit() + && processToStop.exitCode === null + && processToStop.signalCode === null) { + processToStop.kill('SIGKILL'); + assert.ok(await waitForExit(), 'server did not exit after SIGKILL'); } }