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..4abf44e58f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,397 @@ +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 javascriptFiles = [ + '*.js', + '*.mjs', + 'config/**/*.js', + 'packages/syncloungeserver/src/**/*.js', + 'src/**/*.js', + 'test/**/*.js', +]; +const vueFiles = ['src/**/*.vue']; +const lintFiles = [...javascriptFiles, ...vueFiles]; + +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: config.files ?? vueFiles, + })), + { + ...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', + }, + 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', + 'getter-return': ['error', { allowImplicit: true }], + '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-cond-assign': ['error', 'always'], + 'no-console': 'off', + 'no-constant-condition': 'warn', + '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-object-constructor': '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.' }, + { 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.' }, + ], + '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-unsafe-optional-chaining': ['error', { disallowArithmeticOperators: true }], + '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'], + 'valid-typeof': ['error', { requireStringLiterals: true }], + '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/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, + 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': { + node: { + extensions: ['.js', '.mjs', '.json'], + }, + alias: { + map: [['@', path.resolve(projectRoot, 'src')]], + extensions: ['.js', '.vue', '.json'], + }, + }, + }, + }, + { + 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, + }, + }, + { + 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-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..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": "eslint src/ packages/syncloungeserver/src/ poster-proxy.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" @@ -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/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/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 - - diff --git a/src/components/TheSidebarRight.vue b/src/components/TheSidebarRight.vue index 9c2cc916ee..47dd9a3437 100644 --- a/src/components/TheSidebarRight.vue +++ b/src/components/TheSidebarRight.vue @@ -133,7 +133,6 @@ 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/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..8d7ff697a6 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,17 +26,55 @@ 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 { - await fetch(url); - return; + // Sequential polling is intentional: each request observes a later server state. + // eslint-disable-next-line no-await-in-loop + 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 { - await wait(delay); + // 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 + || processToStop.signalCode !== null) return; + + const exited = new Promise((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'); + if (!await waitForExit() + && processToStop.exitCode === null + && processToStop.signalCode === null) { + processToStop.kill('SIGKILL'); + assert.ok(await waitForExit(), 'server did not exit after SIGKILL'); + } +} + function joinClient({ roomId, username }) { return new Promise((resolve, reject) => { const socket = io(BASE, { @@ -76,7 +117,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', @@ -89,10 +130,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 e6058a7653..d08e62614b 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,17 +45,68 @@ 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 { - await fetch(url); - return; + // Sequential polling is intentional: each request observes a later server state. + // eslint-disable-next-line no-await-in-loop + 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 { - await new Promise((r) => setTimeout(r, delay)); + // 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 + || processToStop.signalCode !== null) return; + + const exited = new Promise((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'); + if (!await waitForExit() + && processToStop.exitCode === null + && processToStop.signalCode === null) { + processToStop.kill('SIGKILL'); + assert.ok(await waitForExit(), 'server did not exit after SIGKILL'); + } +} + +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) => { @@ -104,11 +162,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), @@ -125,13 +182,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 --- diff --git a/test/socket-validation.test.js b/test/socket-validation.test.js index 6d27093fdb..897b0cda0c 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,37 @@ 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 + || processToStop.signalCode !== null) return; + + const exited = new Promise((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'); + if (!await waitForExit() + && processToStop.exitCode === null + && processToStop.signalCode === null) { + processToStop.kill('SIGKILL'); + assert.ok(await waitForExit(), 'server did not exit after SIGKILL'); + } +} + function connectClient() { return io(baseUrl, { path: '/socket.io', @@ -110,8 +141,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 () => { 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(),