diff --git a/.github/workflows/__deploy.yml b/.github/workflows/__deploy.yml index b67e3d2d..2fb2e896 100644 --- a/.github/workflows/__deploy.yml +++ b/.github/workflows/__deploy.yml @@ -22,7 +22,7 @@ env: jobs: cf-pages: - name: CloudFlare Pages 📃 + name: Cloudflare Pages 📃 runs-on: ubuntu-latest environment: name: ${{ inputs.branch == 'master' && 'Production' || 'Preview' }} diff --git a/.github/workflows/__job_messages.yml b/.github/workflows/__job_messages.yml index d9995c1d..2556ef0a 100644 --- a/.github/workflows/__job_messages.yml +++ b/.github/workflows/__job_messages.yml @@ -29,7 +29,7 @@ env: jobs: cf_pages_msg: - name: CloudFlare Pages deployment 📃🚀 + name: Cloudflare Pages deployment 📃🚀 runs-on: ubuntu-latest steps: diff --git a/.github/workflows/__quality_checks.yml b/.github/workflows/__quality_checks.yml index a976c6ec..2ee99734 100644 --- a/.github/workflows/__quality_checks.yml +++ b/.github/workflows/__quality_checks.yml @@ -36,10 +36,9 @@ jobs: command: - lint - check:types - - analyze:cycles - defaults: - run: - working-directory: frontend + include: + - workspace: frontend + command: analyze:cycles steps: - name: Checkout ⬇️ @@ -59,7 +58,7 @@ jobs: run: npm ci --no-audit - name: Run ${{ matrix.command }} ⚙️ - run: npm run ${{ matrix.command }} + run: npm run ${{ matrix.command }} ${{ matrix.workspace && format('-w {0}', matrix.workspace) || '' }} commits_checks: name: Commit linting 💬✅ diff --git a/.vscode/settings.json b/.vscode/settings.json index 3304c48b..41d55b4b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,6 +28,9 @@ "javascript", "typescript" ], + "eslint.options": { + "flags": ["unstable_ts_config"] + }, "eslint.format.enable": true, "eslint.useFlatConfig": true, "eslint.workingDirectories": [ diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index eae854b0..00000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,403 +0,0 @@ -import jsdoc from 'eslint-plugin-jsdoc'; -import unicorn from 'eslint-plugin-unicorn'; -import eslintImportX from 'eslint-plugin-import-x'; -import fileProgress from 'eslint-plugin-file-progress'; -import promise from 'eslint-plugin-promise'; -import js from '@eslint/js'; -import globals from 'globals'; -import vueScopedCSS from 'eslint-plugin-vue-scoped-css'; -import css from 'eslint-plugin-css'; -import vue from 'eslint-plugin-vue'; -import i18n from '@intlify/eslint-plugin-vue-i18n'; -import gitignore from 'eslint-config-flat-gitignore'; -import stylistic from '@stylistic/eslint-plugin'; -import sonarjs from 'eslint-plugin-sonarjs'; -import tseslint from 'typescript-eslint'; -import jsonc from 'eslint-plugin-jsonc'; -import regexp from 'eslint-plugin-regexp'; -import { configs as dependConfigs } from 'eslint-plugin-depend'; -import unocss from '@unocss/eslint-config/flat'; - -const tsFiles = ['*.ts', '**/*.ts']; -const vueFiles = ['*.vue', '**/*.vue']; -const vueAndTsFiles = [...vueFiles, ...tsFiles]; -const CI_environment = process.env.CI ? 0 : 1; -const jsoncRecommended = jsonc.configs['flat/recommended-with-json']; -const eqeqeqConfig = ['error', 'always', { null: 'ignore' }]; - -/** - * Util functions - */ -const flatArrayOfObjects = obj => Object.assign({}, ...obj); - -export default tseslint.config( - /** Global settings */ - { ...js.configs.recommended, - name: '(eslint) Extended config from plugin' - }, - { - ...unicorn.configs['flat/recommended'], - name: '(unicorn) Extended config from plugin' - }, - { - ...dependConfigs['flat/recommended'], - name: '(depend) Extended config from plugin' - }, - { - ...unocss, - name: '(unocss) Extended config from plugin' - }, - { - ...stylistic.configs.customize({ - quotes: 'single', - semi: true, - commaDangle: 'never', - braceStyle: '1tbs', - arrowParens: false, - blockSpacing: true - }), - name: '(@stylistic) Extended config from plugin' - }, - { - name: 'Common settings', - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.browser - } - }, - rules: { - 'no-empty': ['error', { allowEmptyCatch: true }], - 'no-extend-native': 'error', - 'curly': ['error', 'all'], - 'prefer-arrow-callback': 'error', - 'multiline-comment-style': 'error', - 'unicode-bom': ['error', 'never'], - 'eqeqeq': eqeqeqConfig, - '@stylistic/quotes': ['error', 'single', { avoidEscape: true }], - '@stylistic/linebreak-style': ['error', 'unix'], - 'unicorn/import-style': 'off', - 'unicorn/filename-case': 'off', - 'unicorn/consistent-function-scoping': 'off', - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/no-await-expression-member': 'off', - /** - * See https://github.com/jellyfin/jellyfin-vue/pull/2361 - */ - 'unicorn/explicit-length-check': 'off', - '@stylistic/padding-line-between-statements': [ - 'error', - // Always require blank lines after import, except between imports - { blankLine: 'always', prev: 'import', next: '*' }, - { blankLine: 'never', prev: 'import', next: 'import' }, - // Always require blank lines before and after every sequence of variable declarations and export - { - blankLine: 'always', - prev: '*', - next: ['const', 'let', 'var', 'export'] - }, - { - blankLine: 'always', - prev: ['const', 'let', 'var', 'export'], - next: '*' - }, - { - blankLine: 'any', - prev: ['const', 'let', 'var', 'export'], - next: ['const', 'let', 'var', 'export'] - }, - { - blankLine: 'any', - prev: ['block-like'], - next: '*' - }, - // Always require blank lines before and after class declaration, if, do/while, switch, try - { - blankLine: 'always', - prev: '*', - next: ['if', 'class', 'for', 'do', 'while', 'switch', 'try'] - }, - { - blankLine: 'always', - prev: ['if', 'class', 'for', 'do', 'while', 'switch', 'try'], - next: '*' - }, - // Always require blank lines before return statements - { blankLine: 'always', prev: '*', next: 'return' } - ] - } - }, - /** Common TypeScript rules */ - { - name: '(TypeScript & Vue) - Parser config', - files: vueAndTsFiles, - languageOptions: { - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - warnOnUnsupportedTypeScriptVersion: false, - extraFileExtensions: ['.vue'] - } - } - }, - { - ...flatArrayOfObjects(tseslint.configs.strictTypeChecked), - name: '(typescript-eslint) Extended config from plugin (strict type checking)', - files: vueAndTsFiles - }, - { - ...flatArrayOfObjects(tseslint.configs.stylisticTypeChecked), - name: '(typescript-eslint) Extended config from plugin (stylistic type checking)', - files: vueAndTsFiles - }, - { - ...tseslint.configs.eslintRecommended, - files: vueAndTsFiles, - name: '(typescript-eslint) Extended config from plugin (ESLint rules with type checking)' - }, - { - ...regexp.configs['flat/recommended'], - name: '(regexp) Extended config from plugin', - files: vueAndTsFiles - }, - { - ...promise.configs['flat/recommended'], - name: '(promise) Extended config from plugin', - files: vueAndTsFiles - }, - { - name: '(promise) Custom config', - files: vueAndTsFiles, - rules: { - 'promise/prefer-await-to-callbacks': 'error', - 'promise/prefer-await-to-then': 'error' - } - }, - { - name: '(import) Custom config', - // TODO: Remove .js after: https://github.com/eslint/eslint/pull/18134 - files: [...vueAndTsFiles, '**/*.js'], - plugins: { - 'import-x': eslintImportX - }, - rules: { - 'import-x/no-extraneous-dependencies': 'error', - 'import-x/order': 'error', - 'import-x/no-cycle': 'error', - 'import-x/no-nodejs-modules': 'error', - 'import-x/no-duplicates': ['error', { 'prefer-inline': true, 'considerQueryString': true }], - // From the recommended preset - 'import-x/named': 'error', - 'import-x/export': 'error' - } - }, - { - files: vueAndTsFiles, - name: '(JSDoc) Custom config', - plugins: { - jsdoc - }, - rules: { - 'jsdoc/require-hyphen-before-param-description': 'error', - 'jsdoc/require-description': 'error', - 'jsdoc/no-types': 'error', - 'jsdoc/require-jsdoc': 'error', - 'jsdoc/informative-docs': 'error' - } - }, - { - name: '(TypeScript & Vue) Custom config', - files: vueAndTsFiles, - rules: { - '@typescript-eslint/no-redundant-type-constituents': 'off', - '@typescript-eslint/restrict-template-expressions': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-dynamic-delete': 'off', - '@typescript-eslint/no-import-type-side-effects': 'error', - '@typescript-eslint/consistent-type-exports': 'error', - '@typescript-eslint/explicit-member-accessibility': 'error', - '@typescript-eslint/consistent-type-imports': ['error', { - prefer: 'type-imports', - fixStyle: 'inline-type-imports' - }], - '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], - '@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }], - 'vue/return-in-computed-property': 'off' - } - }, - { - ...sonarjs.configs.recommended, - name: '(sonarcloud) Extended config from plugin', - files: vueAndTsFiles - }, - /** SFC rules */ - ...vue.configs['flat/recommended'].map((config) => { - /** - * Specified above, unnecessary to overwrite - */ - delete config.languageOptions?.globals; - /** - * DEPRECATED: See https://eslint.vuejs.org/rules/component-tags-order.html#vue-component-tags-order - * TODO: Remove when it's removed from the recommended rules - */ - delete config.rules?.['vue/component-tags-order']; - config.name = `(${config.name}) - Extended config from plugin`; - - return config; - }), - { - ...flatArrayOfObjects(vueScopedCSS.configs['flat/recommended']), - name: '(Vue - Scoped CSS) Extended config from plugin' - }, - { - ...css.configs['flat/recommended'], - name: '(Vue - CSS) Extended config from plugin', - files: [...vueFiles] - }, - { - name: '(Vue) Custom config', - files: vueFiles, - languageOptions: { - parserOptions: { - parser: tseslint.parser - } - }, - rules: { - 'vue/component-name-in-template-casing': [ - 'error', - 'PascalCase', - { - registeredComponentsOnly: false - } - ], - 'vue/define-macros-order': ['error', { - order: ['defineOptions', 'defineProps', 'defineEmits', 'defineSlots'] - }], - 'vue/html-closing-bracket-newline': ['error', { multiline: 'never' }], - 'vue/block-order': ['error', { - order: ['template', 'script:not([setup])', 'script[setup]'] - }], - 'vue/multi-word-component-names': 'off', - 'vue/require-default-prop': 'off', - 'vue/eqeqeq': eqeqeqConfig - } - }, - { - /* First index is just the plugin definition */ - ...jsoncRecommended.at(0), - ...jsoncRecommended.at(1), - name: '(JSON) Custom config', - rules: { - ...jsoncRecommended.at(1).rules, - ...jsoncRecommended.at(2).rules, - 'jsonc/auto': 'error', - '@stylistic/quotes': ['error', 'double'], - '@stylistic/semi': 'off', - '@stylistic/quote-props': 'off' - } - }, - { - /* First index is just the plugin definition */ - ...i18n.configs['flat/recommended'].at(0), - /* Last contains the rule definitions */ - ...i18n.configs['flat/recommended'].at(-1), - name: '(@intlify/vue-i18n) Extended config (plugin & settings)', - settings: { - 'vue-i18n': { - localeDir: 'locales/en.json', - messageSyntaxVersion: '^9.0.0' - } - }, - files: vueAndTsFiles - }, - { - name: '(@intlify/vue-i18n) Custom config', - files: vueAndTsFiles, - rules: { - '@intlify/vue-i18n/no-unused-keys': ['error', { - extensions: ['.ts', '.vue', '.json'], - enableFix: true - }], - '@intlify/vue-i18n/no-raw-text': ['error', { - ignorePattern: '^[-#:()&.]+$' - }], - '@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error', - '@intlify/vue-i18n/no-dynamic-keys': 'error', - '@intlify/vue-i18n/key-format-style': 'error' - } - }, - /** - * See the following: - * - https://en.wikipedia.org/wiki/History_of_sentence_spacing#French_and_English_spacing - * - https://docs.weblate.org/en/weblate-4.14.1/user/checks.html#check-punctuation-spacing - */ - { - name: '(@intlify/vue-i18n - fr) Config exceptions for linguistic rules', - files: ['locales/fr.json'], - rules: { - 'no-irregular-whitespace': 'off' - } - }, - /** Settings for all the files that run in development */ - { - name: 'Environment config - Node.js and development-related files', - files: ['*.config.*', 'scripts/**/*.ts'], - languageOptions: { - globals: { - ...globals.node, - ...globals.nodeBuiltin - } - }, - rules: { - 'import-x/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true - } - ], - 'import-x/no-nodejs-modules': 'off' - } - }, - /** Settings for WebWorkers (the pattern matches any file that ends in .worker.ts) */ - { - name: 'Environment config - WebWorkers', - files: ['**/*.worker.ts'], - languageOptions: { - globals: { - ...globals.worker - } - } - }, - { - ...stylistic.configs['disable-legacy'], - name: 'Environment config - Disable legacy rules' - }, - /** - * Extra files to include and ignores that should override all the others - */ - { - name: 'Environment config - Ignored files', - ignores: [ - '**/.git', - 'types/global/routes.d.ts', - 'types/global/components.d.ts', - ...gitignore().ignores - ] - }, - /** File progress plugin */ - { - name: '(eslint) Linting progress report', - settings: { - progress: { - successMessage: 'Linting done!' - } - }, - plugins: { - 'file-progress': fileProgress - }, - rules: { - 'file-progress/activate': CI_environment - } - } -); diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts new file mode 100644 index 00000000..cea82774 --- /dev/null +++ b/frontend/eslint.config.ts @@ -0,0 +1,11 @@ +import type { Linter } from 'eslint'; +import { getBaseConfig, getTSVueConfig, getNodeFiles, unocss, getWorkerFiles } from '@jellyfin-vue/configs/eslint'; + +// TODO: Add missing rules for i18n and json +export default [ + ...getBaseConfig('@jellyfin-vue/frontend'), + ...getTSVueConfig(true, import.meta.dirname), + ...unocss, + ...getNodeFiles(), + ...getWorkerFiles() +] satisfies Linter.Config[]; diff --git a/frontend/package.json b/frontend/package.json index 5ef80099..87c805a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,8 +8,8 @@ "scripts": { "analyze:bundle": "vite build --mode analyze:bundle", "analyze:cycles": "vite build --mode analyze:cycles", - "lint": "eslint . --max-warnings=0 --cache --cache-location ../node_modules/.cache/eslint", - "lint:fix": "eslint . --fix --max-warnings=0 --cache --cache-location ../node_modules/.cache/eslint", + "lint": "eslint . --max-warnings=0 --flag unstable_ts_config", + "lint:fix": "eslint . --fix --max-warnings=0 --flag unstable_ts_config", "lint:inspect": "eslint-config-inspector", "build": "vite build", "check": "npm run lint && npm run check:types", @@ -47,45 +47,22 @@ "vuetify": "3.7.1" }, "devDependencies": { - "@eslint/config-inspector": "0.5.4", - "@eslint/js": "9.10.0", "@iconify/json": "2.2.246", - "@intlify/eslint-plugin-vue-i18n": "3.0.0", "@intlify/unplugin-vue-i18n": "4.0.0", "@jellyfin-vue/configs": "*", "@jellyfin-vue/vite-plugins": "*", "@rollup/plugin-virtual": "3.0.2", - "@stylistic/eslint-plugin": "2.8.0", "@types/dompurify": "3.0.5", - "@types/node": "22.5.4", "@types/sortablejs": "1.15.8", "@types/uuid": "10.0.0", - "@unocss/eslint-config": "0.62.3", "@vitejs/plugin-vue": "5.1.3", "browserslist": "4.23.3", - "eslint-config-flat-gitignore": "0.3.0", - "eslint-plugin-css": "0.11.0", - "eslint-plugin-depend": "0.11.0", - "eslint-plugin-file-progress": "1.5.0", - "eslint-plugin-import-x": "4.2.1", - "eslint-plugin-jsdoc": "50.2.2", - "eslint-plugin-jsonc": "2.16.0", - "eslint-plugin-promise": "7.1.0", - "eslint-plugin-regexp": "2.6.0", - "eslint-plugin-sonarjs": "1.0.4", - "eslint-plugin-unicorn": "55.0.0", - "eslint-plugin-vue": "9.28.0", - "eslint-plugin-vue-scoped-css": "2.8.1", - "globals": "15.9.0", "lightningcss": "1.26.0", - "typescript-eslint": "8.5.0", "unocss": "0.62.3", "unplugin-icons": "0.19.3", "unplugin-vue-components": "0.27.4", "unplugin-vue-router": "0.10.8", "vite": "5.4.6", - "vite-plugin-vue-devtools": "7.4.4", - "vue-eslint-parser": "9.4.3", - "vue-tsc": "2.1.6" + "vite-plugin-vue-devtools": "7.4.4" } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index ecad4a91..14e4df0d 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@jellyfin-vue/configs/tsconfig.json", + "extends": "@jellyfin-vue/configs/typescript/base.json", "compilerOptions": { "types": [ "axios", diff --git a/frontend/uno.config.ts b/frontend/uno.config.ts index b4c4111b..b62c5c76 100644 --- a/frontend/uno.config.ts +++ b/frontend/uno.config.ts @@ -1,15 +1 @@ -import { defineConfig, presetUno } from 'unocss'; - -export default defineConfig({ - presets: [ - presetUno({ - prefix: 'uno-', - preflight: false - }) - ], - theme: { - colors: { - background: 'rgb(var(--j-color-background))' - } - } -}); +export { defaultConfig as default } from '@jellyfin-vue/configs/unocss'; diff --git a/package-lock.json b/package-lock.json index 26041b78..7d15ab6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,10 +1,9 @@ { - "name": "@jellyfin-vue", + "name": "jellyfin-vue", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@jellyfin-vue", "license": "GPL-3.0-only", "workspaces": [ "frontend", @@ -12,8 +11,12 @@ "packages/*" ], "devDependencies": { - "eslint": "9.10.0", - "typescript": "5.6.2" + "@eslint/config-inspector": "0.5.4", + "@types/node": "22.5.4", + "eslint": "9.12.0", + "jiti": "2.3.3", + "typescript": "5.6.2", + "vue-tsc": "2.1.6" }, "engines": { "node": ">=20.8.1 <21.0.0", @@ -52,46 +55,23 @@ "vuetify": "3.7.1" }, "devDependencies": { - "@eslint/config-inspector": "0.5.4", - "@eslint/js": "9.10.0", "@iconify/json": "2.2.246", - "@intlify/eslint-plugin-vue-i18n": "3.0.0", "@intlify/unplugin-vue-i18n": "4.0.0", "@jellyfin-vue/configs": "*", "@jellyfin-vue/vite-plugins": "*", "@rollup/plugin-virtual": "3.0.2", - "@stylistic/eslint-plugin": "2.8.0", "@types/dompurify": "3.0.5", - "@types/node": "22.5.4", "@types/sortablejs": "1.15.8", "@types/uuid": "10.0.0", - "@unocss/eslint-config": "0.62.3", "@vitejs/plugin-vue": "5.1.3", "browserslist": "4.23.3", - "eslint-config-flat-gitignore": "0.3.0", - "eslint-plugin-css": "0.11.0", - "eslint-plugin-depend": "0.11.0", - "eslint-plugin-file-progress": "1.5.0", - "eslint-plugin-import-x": "4.2.1", - "eslint-plugin-jsdoc": "50.2.2", - "eslint-plugin-jsonc": "2.16.0", - "eslint-plugin-promise": "7.1.0", - "eslint-plugin-regexp": "2.6.0", - "eslint-plugin-sonarjs": "1.0.4", - "eslint-plugin-unicorn": "55.0.0", - "eslint-plugin-vue": "9.28.0", - "eslint-plugin-vue-scoped-css": "2.8.1", - "globals": "15.9.0", "lightningcss": "1.26.0", - "typescript-eslint": "8.5.0", "unocss": "0.62.3", "unplugin-icons": "0.19.3", "unplugin-vue-components": "0.27.4", "unplugin-vue-router": "0.10.8", "vite": "5.4.6", - "vite-plugin-vue-devtools": "7.4.4", - "vue-eslint-parser": "9.4.3", - "vue-tsc": "2.1.6" + "vite-plugin-vue-devtools": "7.4.4" } }, "node_modules/@ampproject/remapping": { @@ -729,9 +709,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", "dev": true, "license": "MIT", "dependencies": { @@ -1345,6 +1325,16 @@ "eslint": "^8.50.0 || ^9.0.0" } }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", @@ -1407,9 +1397,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", - "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", "dev": true, "license": "MIT", "engines": { @@ -1427,9 +1417,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", - "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1445,6 +1435,30 @@ "integrity": "sha512-p0fta2L9CMeTbC7rNYeKF0bcu+tUGb4NH5D/pHC0RUNW1I+rrBDXLoGYKaH4yNfxxMrsZ4k1fchgkCI4F9zIFQ==", "license": "OFL-1.1" }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1460,9 +1474,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1673,9 +1687,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -2371,6 +2385,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/node": { "version": "22.5.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", @@ -4472,29 +4493,32 @@ } }, "node_modules/eslint": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", - "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.10.0", - "@eslint/plugin-kit": "^0.1.0", + "@eslint/js": "9.12.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4504,13 +4528,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { @@ -4920,9 +4942,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4937,9 +4959,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4964,43 +4986,12 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/eslint/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==", + "node_modules/eslint/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/eslint/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/eslint/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" - } + "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", @@ -5128,15 +5119,15 @@ } }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5795,6 +5786,16 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/importx/node_modules/jiti": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.0.0-beta.2.tgz", + "integrity": "sha512-c+PHQZakiQuMKbnhvrjZUvrK6E/AfmTOf4P+E3Y4FNVHcNMX9e/XrnbEvO+m4wS6ZjsvhHh/POQTlfy8uXFc0A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5984,16 +5985,6 @@ "node": ">=0.12.0" } }, - "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-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -6069,9 +6060,9 @@ } }, "node_modules/jiti": { - "version": "2.0.0-beta.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.0.0-beta.2.tgz", - "integrity": "sha512-c+PHQZakiQuMKbnhvrjZUvrK6E/AfmTOf4P+E3Y4FNVHcNMX9e/XrnbEvO+m4wS6ZjsvhHh/POQTlfy8uXFc0A==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz", + "integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==", "dev": true, "license": "MIT", "bin": { @@ -7062,9 +7053,9 @@ } }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, @@ -7122,13 +7113,13 @@ } }, "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^4.4.0" + "entities": "^4.5.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -8095,9 +8086,9 @@ "license": "MIT" }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -9761,9 +9752,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -9968,74 +9959,39 @@ }, "packages/configs": { "name": "@jellyfin-vue/configs", - "version": "0.0.1" + "version": "0.0.1", + "devDependencies": { + "@eslint/js": "9.12.0", + "@intlify/eslint-plugin-vue-i18n": "3.0.0", + "@stylistic/eslint-plugin": "2.8.0", + "@unocss/eslint-config": "0.62.3", + "eslint-config-flat-gitignore": "0.3.0", + "eslint-plugin-css": "0.11.0", + "eslint-plugin-depend": "0.11.0", + "eslint-plugin-file-progress": "1.5.0", + "eslint-plugin-import-x": "4.2.1", + "eslint-plugin-jsdoc": "50.2.2", + "eslint-plugin-jsonc": "2.16.0", + "eslint-plugin-promise": "7.1.0", + "eslint-plugin-regexp": "2.6.0", + "eslint-plugin-sonarjs": "1.0.4", + "eslint-plugin-unicorn": "55.0.0", + "eslint-plugin-vue": "9.28.0", + "eslint-plugin-vue-scoped-css": "2.8.1", + "find-up-simple": "1.0.0", + "globals": "15.9.0", + "typescript-eslint": "8.5.0", + "unocss": "0.62.3", + "vue-eslint-parser": "9.4.3" + } }, "packages/vite-plugins": { "name": "@jellyfin-vue/vite-plugins", "version": "0.0.1", "devDependencies": { + "@jellyfin-vue/configs": "*", "rollup-plugin-visualizer": "5.12.0", - "vite": "5.4.3" - } - }, - "packages/vite-plugins/node_modules/vite": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz", - "integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "vite": "5.4.6" } }, "packaging/tauri": { diff --git a/package.json b/package.json index 9c020255..52c037a9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,4 @@ { - "name": "@jellyfin-vue", "private": true, "type": "module", "homepage": "https://jellyfin.org/", @@ -22,8 +21,17 @@ "npm": ">=10.1.0", "yarn": "Yarn is not supported. Please use NPM." }, + "scripts": { + "lint": "npm run lint --workspaces", + "lint:fix": "npm run lint:fix --workspaces", + "check:types": "npm run check:types --workspaces" + }, "devDependencies": { - "eslint": "9.10.0", - "typescript": "5.6.2" + "@eslint/config-inspector": "0.5.4", + "@types/node": "22.5.4", + "eslint": "9.12.0", + "jiti": "2.3.3", + "typescript": "5.6.2", + "vue-tsc": "2.1.6" } } diff --git a/packages/configs/eslint.config.ts b/packages/configs/eslint.config.ts new file mode 100644 index 00000000..a693e045 --- /dev/null +++ b/packages/configs/eslint.config.ts @@ -0,0 +1,8 @@ +import type { Linter } from 'eslint'; +import { getBaseConfig } from './eslint/rules/base'; +import { getTSVueConfig } from './eslint/rules/typescript-vue'; + +export default [ + ...getBaseConfig('@jellyfin-vue/configs'), + ...getTSVueConfig(false, import.meta.dirname) +] satisfies Linter.Config[]; diff --git a/packages/configs/eslint/index.ts b/packages/configs/eslint/index.ts new file mode 100644 index 00000000..6002a81a --- /dev/null +++ b/packages/configs/eslint/index.ts @@ -0,0 +1,6 @@ +export { getBaseConfig } from './rules/base'; +export { unocss } from './rules/unocss'; +export { getTSVueConfig } from './rules/typescript-vue'; +export { json } from './rules/json'; +export { getNodeFiles, getWorkerFiles } from './rules/env'; +export { i18n } from './rules/i18n'; diff --git a/packages/configs/eslint/rules/base.ts b/packages/configs/eslint/rules/base.ts new file mode 100644 index 00000000..b8a86b4b --- /dev/null +++ b/packages/configs/eslint/rules/base.ts @@ -0,0 +1,147 @@ +import type { Linter } from 'eslint'; +/* + * import { join } from 'node:path'; + * import { findUpSync } from 'find-up-simple'; + */ +import unicorn from 'eslint-plugin-unicorn'; +// @ts-expect-error - No types available +import js from '@eslint/js'; +import stylistic from '@stylistic/eslint-plugin'; +import { configs as dependConfigs } from 'eslint-plugin-depend'; +import gitignore from 'eslint-config-flat-gitignore'; +// @ts-expect-error - No types available +import fileProgress from 'eslint-plugin-file-progress'; +import { eqeqeqConfig } from '../shared'; + +const CI_environment = process.env.CI ? 0 : 1; + +/** + * Gets ESLint's minimal configuration for the monorepo + * + * @param packageName - The name of the current package + */ +export function getBaseConfig(packageName: string): Linter.Config[] { + return [ + /* + * TODO: Wait for https://github.com/eslint/eslint/issues/19015 + * { + * cache: true, + * cacheLocation: join(findUpSync('node_modules', { type: 'directory' }) ?? '', '.cache/eslint', packageName.replace('/', '_')), + * }, + */ + { ...js.configs.recommended, + name: '(@jellyfin-vue/configs/eslint/base - eslint) Extended config from plugin' + }, + { + ...unicorn.configs['flat/recommended'], + name: '(@jellyfin-vue/configs/eslint/base - unicorn) Extended config from plugin' + }, + { + ...dependConfigs['flat/recommended'], + name: '(@jellyfin-vue/configs/eslint/base - depend) Extended config from plugin' + }, + { + ...stylistic.configs.customize({ + quotes: 'single', + semi: true, + commaDangle: 'never', + braceStyle: '1tbs', + arrowParens: false, + blockSpacing: true + }), + name: '(@jellyfin-vue/configs/eslint/base - @stylistic) Extended config from plugin' + }, + { + name: '(@jellyfin-vue/configs/eslint/base) Common settings', + rules: { + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-extend-native': 'error', + 'curly': ['error', 'all'], + 'prefer-arrow-callback': 'error', + 'multiline-comment-style': 'error', + 'unicode-bom': ['error', 'never'], + 'eqeqeq': eqeqeqConfig, + '@stylistic/quotes': ['error', 'single', { avoidEscape: true }], + '@stylistic/linebreak-style': ['error', 'unix'], + 'unicorn/import-style': 'off', + 'unicorn/filename-case': 'off', + 'unicorn/consistent-function-scoping': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/no-await-expression-member': 'off', + /** + * See https://github.com/jellyfin/jellyfin-vue/pull/2361 + */ + 'unicorn/explicit-length-check': 'off', + '@stylistic/padding-line-between-statements': [ + 'error', + // Always require blank lines after import, except between imports + { blankLine: 'always', prev: 'import', next: '*' }, + { blankLine: 'never', prev: 'import', next: 'import' }, + // Always require blank lines before and after every sequence of variable declarations and export + { + blankLine: 'always', + prev: '*', + next: ['const', 'let', 'var', 'export'] + }, + { + blankLine: 'always', + prev: ['const', 'let', 'var', 'export'], + next: '*' + }, + { + blankLine: 'any', + prev: ['const', 'let', 'var', 'export'], + next: ['const', 'let', 'var', 'export'] + }, + { + blankLine: 'any', + prev: ['block-like'], + next: '*' + }, + // Always require blank lines before and after class declaration, if, do/while, switch, try + { + blankLine: 'always', + prev: '*', + next: ['if', 'class', 'for', 'do', 'while', 'switch', 'try'] + }, + { + blankLine: 'always', + prev: ['if', 'class', 'for', 'do', 'while', 'switch', 'try'], + next: '*' + }, + // Always require blank lines before return statements + { blankLine: 'always', prev: '*', next: 'return' } + ] + } + }, + { + ...stylistic.configs['disable-legacy'], + name: '(@jellyfin-vue/configs/eslint/base) Disable stylistic legacy rules' + }, + /** + * Extra files to include and ignores that should override all the others + */ + { + name: '(@jellyfin-vue/configs/eslint/base) Ignored files', + ignores: [ + '**/.git', + ...gitignore().ignores + ] + }, + /** File progress plugin */ + { + name: '(@jellyfin-vue/configs/eslint/base) Linting progress report', + settings: { + progress: { + successMessage: 'Linting done!' + } + }, + plugins: { + 'file-progress': fileProgress + }, + rules: { + 'file-progress/activate': CI_environment + } + } + ]; +}; diff --git a/packages/configs/eslint/rules/env.ts b/packages/configs/eslint/rules/env.ts new file mode 100644 index 00000000..204f0a0b --- /dev/null +++ b/packages/configs/eslint/rules/env.ts @@ -0,0 +1,45 @@ +import type { Linter } from 'eslint'; +import globals from 'globals'; + +/** + * Gets the ESLint config from Node.js and development related files + * @param files - Defaults to `*config.*` and files under `scripts` folder + */ +export function getNodeFiles(files = ['*.config.*', 'scripts/**/*.ts']): Linter.Config[] { + return [{ + name: '(@jellyfin-vue/configs/eslint/env) Node.js and development-related files', + files, + languageOptions: { + globals: { + ...globals.node, + ...globals.nodeBuiltin + } + }, + rules: { + 'import-x/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true + } + ], + 'import-x/no-nodejs-modules': 'off' + } + } + ]; +} + +/** + * Gets the rules config for WebWorker files + * @param files - Defaults to `*.worker.ts` files + */ +export function getWorkerFiles(files = ['**/*.worker.ts']): Linter.Config[] { + return [{ + name: '(@jellyfin-vue/configs/eslint/env) WebWorkers', + files, + languageOptions: { + globals: { + ...globals.worker + } + } + }]; +} diff --git a/packages/configs/eslint/rules/i18n.ts b/packages/configs/eslint/rules/i18n.ts new file mode 100644 index 00000000..70186936 --- /dev/null +++ b/packages/configs/eslint/rules/i18n.ts @@ -0,0 +1,48 @@ +import type { Linter } from 'eslint'; +import i18nPlugin from '@intlify/eslint-plugin-vue-i18n'; +import { vueAndTsFiles } from '../shared'; + +export const i18n = [ + { + /* First index is just the plugin definition */ + ...i18nPlugin.configs['flat/recommended'].at(0), + /* Last contains the rule definitions */ + ...i18nPlugin.configs['flat/recommended'].at(-1), + name: '(@intlify/vue-i18n) Extended config (plugin & settings)', + settings: { + 'vue-i18n': { + localeDir: 'locales/en.json', + messageSyntaxVersion: '^9.0.0' + } + }, + files: vueAndTsFiles + }, + { + name: '(@intlify/vue-i18n) Custom config', + files: vueAndTsFiles, + rules: { + '@intlify/vue-i18n/no-unused-keys': ['error', { + extensions: ['.ts', '.vue', '.json'], + enableFix: true + }], + '@intlify/vue-i18n/no-raw-text': ['error', { + ignorePattern: '^[-#:()&.]+$' + }], + '@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error', + '@intlify/vue-i18n/no-dynamic-keys': 'error', + '@intlify/vue-i18n/key-format-style': 'error' + } + }, + /** + * See the following: + * - https://en.wikipedia.org/wiki/History_of_sentence_spacing#French_and_English_spacing + * - https://docs.weblate.org/en/weblate-4.14.1/user/checks.html#check-punctuation-spacing + */ + { + name: '(@intlify/vue-i18n - fr) Config exceptions for linguistic rules', + files: ['locales/fr.json'], + rules: { + 'no-irregular-whitespace': 'off' + } + } +] satisfies Linter.Config[]; diff --git a/packages/configs/eslint/rules/json.ts b/packages/configs/eslint/rules/json.ts new file mode 100644 index 00000000..63c5b902 --- /dev/null +++ b/packages/configs/eslint/rules/json.ts @@ -0,0 +1,21 @@ +import type { Linter } from 'eslint'; +import jsonc from 'eslint-plugin-jsonc'; + +const jsoncRecommended = jsonc.configs['flat/recommended-with-json']; + +export const json = [ + { + /* First index is just the plugin definition */ + ...jsoncRecommended.at(0), + ...jsoncRecommended.at(1), + name: '(@jellyfin-vue/configs/eslint/json) - Custom config', + rules: { + ...jsoncRecommended.at(1).rules, + ...jsoncRecommended.at(2).rules, + 'jsonc/auto': 'error', + '@stylistic/quotes': ['error', 'double'], + '@stylistic/semi': 'off', + '@stylistic/quote-props': 'off' + } + } +] satisfies Linter.Config[]; diff --git a/packages/configs/eslint/rules/typescript-vue.ts b/packages/configs/eslint/rules/typescript-vue.ts new file mode 100644 index 00000000..5c6bd0ef --- /dev/null +++ b/packages/configs/eslint/rules/typescript-vue.ts @@ -0,0 +1,231 @@ +import type { Linter } from 'eslint'; +import tseslint from 'typescript-eslint'; +import sonarjs from 'eslint-plugin-sonarjs'; +import regexp from 'eslint-plugin-regexp'; +import jsdoc from 'eslint-plugin-jsdoc'; +import eslintImportX from 'eslint-plugin-import-x'; +// @ts-expect-error - No types available +import vueScopedCSS from 'eslint-plugin-vue-scoped-css'; +import css from 'eslint-plugin-css'; +// @ts-expect-error - No types available +import vue from 'eslint-plugin-vue'; +// @ts-expect-error - No types available +import promise from 'eslint-plugin-promise'; +import globals from 'globals'; +import vueParser from 'vue-eslint-parser'; +import { eqeqeqConfig, vueAndTsFiles, vueFiles, tsFiles } from '../shared'; + +/** + * Util functions + */ +const flatArrayOfObjects = (obj: unknown[]) => Object.assign({}, ...obj); + +/** Common TypeScript and Vue rules */ +const common = [ + { + ...flatArrayOfObjects(tseslint.configs.strictTypeChecked), + name: '(@jellyfin-vue/configs/eslint/typescript-vue - typescript-eslint) Extended config from plugin (strict type checking)' + }, + { + ...flatArrayOfObjects(tseslint.configs.stylisticTypeChecked), + name: '(@jellyfin-vue/configs/eslint/typescript-vue - typescript-eslint) Extended config from plugin (stylistic type checking)' + }, + { + ...tseslint.configs.eslintRecommended, + name: '(@jellyfin-vue/configs/eslint/typescript-vue - typescript-eslint) Extended config from plugin (ESLint rules with type checking)' + }, + { + ...regexp.configs['flat/recommended'], + name: '(@jellyfin-vue/configs/eslint/typescript-vue - regexp) Extended config from plugin' + }, + { + ...promise.configs['flat/recommended'], + name: '(@jellyfin-vue/configs/eslint/typescript-vue - promise) Extended config from plugin' + }, + { + name: '(@jellyfin-vue/configs/eslint/typescript-vue - promise) Custom config', + rules: { + 'promise/prefer-await-to-callbacks': 'error', + 'promise/prefer-await-to-then': 'error' + } + }, + { + name: '(@jellyfin-vue/configs/eslint/typescript-vue - import) Custom config', + plugins: { + 'import-x': eslintImportX + }, + rules: { + 'import-x/no-extraneous-dependencies': 'error', + 'import-x/order': 'error', + 'import-x/no-cycle': 'error', + 'import-x/no-nodejs-modules': 'error', + 'import-x/no-duplicates': ['error', { 'prefer-inline': true, 'considerQueryString': true }], + // From the recommended preset + 'import-x/named': 'error', + 'import-x/export': 'error' + } + }, + { + name: '(@jellyfin-vue/configs/eslint/typescript-vue - JSDoc) Custom config', + plugins: { + jsdoc + }, + rules: { + 'jsdoc/require-hyphen-before-param-description': 'error', + 'jsdoc/require-description': 'error', + 'jsdoc/no-types': 'error', + 'jsdoc/require-jsdoc': 'error', + 'jsdoc/informative-docs': 'error' + } + }, + { + name: '(@jellyfin-vue/configs/eslint/typescript-vue - TypeScript & Vue) Custom config', + rules: { + '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-dynamic-delete': 'off', + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/explicit-member-accessibility': 'error', + '@typescript-eslint/consistent-type-imports': ['error', { + prefer: 'type-imports', + fixStyle: 'inline-type-imports' + }], + '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], + '@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }], + 'vue/return-in-computed-property': 'off' + } + }, + { + ...sonarjs.configs.recommended, + name: '(@jellyfin-vue/configs/eslint/typescript-vue - sonarcloud) Extended config from plugin' + } +] satisfies Linter.Config[]; + +/** Vue SFC only rules */ +const vue_config = [ + ...vue.configs['flat/recommended'].map((config) => { + /** + * Specified above, unnecessary to overwrite + */ + delete config.languageOptions?.globals; + /** + * DEPRECATED: See https://eslint.vuejs.org/rules/component-tags-order.html#vue-component-tags-order + * TODO: Remove when it's removed from the recommended rules + */ + delete config.rules?.['vue/component-tags-order']; + config.name = `(@jellyfin-vue/configs/eslint/typescript-vue - ${config.name}) - Extended config from plugin`; + + return config; + }), + { + ...flatArrayOfObjects(vueScopedCSS.configs['flat/recommended']), + name: '(@jellyfin-vue/configs/eslint/typescript-vue - Vue Scoped CSS) Extended config from plugin', + files: vueFiles + }, + { + ...css.configs['flat/recommended'], + name: '(@jellyfin-vue/configs/eslint/typescript-vue - Vue CSS) Extended config from plugin', + files: vueFiles + }, + { + name: '(Vue) Custom config', + files: vueFiles, + rules: { + 'vue/component-name-in-template-casing': [ + 'error', + 'PascalCase', + { + registeredComponentsOnly: false + } + ], + 'vue/define-macros-order': ['error', { + order: ['defineOptions', 'defineProps', 'defineEmits', 'defineSlots'] + }], + 'vue/html-closing-bracket-newline': ['error', { multiline: 'never' }], + 'vue/block-order': ['error', { + order: ['template', 'script:not([setup])', 'script[setup]'] + }], + 'vue/multi-word-component-names': 'off', + 'vue/require-default-prop': 'off', + 'vue/eqeqeq': eqeqeqConfig, + 'vue/block-lang': ['error', + { + script: { + lang: 'ts' + } + } + ] + + } + } +] satisfies Linter.Config[]; + +/** + * Gets the base configuration for TypeScript files only or both Vue and TypeScript + * + * @param enableVue - Whether to apply the base config for Vue files + * @returns + */ +export function getTSVueConfig(enableVue = true, tsconfigRootDir = import.meta.dirname): Linter.Config[] { + const result = [ + ...(enableVue ? vue_config : []), + ...common.map(conf => ({ + ...conf, files: enableVue ? vueAndTsFiles : tsFiles + }))] satisfies Linter.Config[]; + + const langOptions = { + ecmaVersion: 2024, + sourceType: 'module', + globals: { + ...globals.browser + } + }; + + const sharedParserOptions = { + projectService: true, + tsconfigRootDir, + warnOnUnsupportedTypeScriptVersion: false + }; + + // Extracted from https://github.com/vuejs/eslint-config-typescript + const base_vue_parser = { + name: '(@jellyfin-vue/configs/eslint/typescript-vue - Vue) Extra parser configuration for typed linting', + files: vueFiles, + languageOptions: { + ...langOptions, + parser: vueParser, + parserOptions: { + parser: tseslint.parser, + ...sharedParserOptions + } + } + }; + + const base_ts_parser = { + name: enableVue + ? '(@jellyfin-vue/configs/eslint/typescript-vue - TypeScript & Vue) Parser extra config' + : '(@jellyfin-vue/configs/eslint/typescript-vue - TypeScript) Parser extra config', + files: enableVue ? vueAndTsFiles : tsFiles, + languageOptions: { + ...langOptions, + parserOptions: { + ...sharedParserOptions, + ...(enableVue + ? { + extraFileExtensions: ['.vue'] + } + : {}) + } + } + }; + + result.push(base_ts_parser); + + if (enableVue) { + result.push(base_vue_parser); + } + + return result; +} diff --git a/packages/configs/eslint/rules/unocss.ts b/packages/configs/eslint/rules/unocss.ts new file mode 100644 index 00000000..72b7fe99 --- /dev/null +++ b/packages/configs/eslint/rules/unocss.ts @@ -0,0 +1,9 @@ +import type { Linter } from 'eslint'; +import UnoPlugin from '@unocss/eslint-config/flat'; + +export const unocss = [ + { + ...UnoPlugin, + name: '(unocss) Extended config from plugin' + } +] satisfies Linter.Config[]; diff --git a/packages/configs/eslint/shared.ts b/packages/configs/eslint/shared.ts new file mode 100644 index 00000000..424b866a --- /dev/null +++ b/packages/configs/eslint/shared.ts @@ -0,0 +1,7 @@ +/** + * Shared variables between rules + */ +export const eqeqeqConfig = ['error', 'always', { null: 'ignore' }]; +export const tsFiles = ['*.ts', '**/*.ts']; +export const vueFiles = ['*.vue', '**/*.vue']; +export const vueAndTsFiles = [...vueFiles, ...tsFiles]; diff --git a/packages/configs/package.json b/packages/configs/package.json index 440f071d..410b86e1 100644 --- a/packages/configs/package.json +++ b/packages/configs/package.json @@ -1,9 +1,40 @@ { "name": "@jellyfin-vue/configs", "version": "0.0.1", + "type": "module", "exports": { - "./tsconfig.json": "./tsconfig.json", - "./index.js": "./index.js" + "./typescript/base.json": "./typescript/base.json", + "./eslint": "./eslint/index.ts", + "./unocss": "./unocss.ts" }, - "type": "module" + "scripts": { + "lint": "eslint . --max-warnings=0 --flag unstable_ts_config", + "lint:fix": "eslint . --fix --max-warnings=0 --flag unstable_ts_config", + "lint:inspect": "eslint-config-inspector", + "check:types": "vue-tsc" + }, + "devDependencies": { + "@eslint/js": "9.12.0", + "@intlify/eslint-plugin-vue-i18n": "3.0.0", + "@stylistic/eslint-plugin": "2.8.0", + "@unocss/eslint-config": "0.62.3", + "eslint-config-flat-gitignore": "0.3.0", + "eslint-plugin-css": "0.11.0", + "eslint-plugin-depend": "0.11.0", + "eslint-plugin-file-progress": "1.5.0", + "eslint-plugin-import-x": "4.2.1", + "eslint-plugin-jsdoc": "50.2.2", + "eslint-plugin-jsonc": "2.16.0", + "eslint-plugin-promise": "7.1.0", + "eslint-plugin-regexp": "2.6.0", + "eslint-plugin-sonarjs": "1.0.4", + "eslint-plugin-unicorn": "55.0.0", + "eslint-plugin-vue": "9.28.0", + "eslint-plugin-vue-scoped-css": "2.8.1", + "find-up-simple": "1.0.0", + "globals": "15.9.0", + "typescript-eslint": "8.5.0", + "unocss": "0.62.3", + "vue-eslint-parser": "9.4.3" + } } diff --git a/packages/configs/tsconfig.json b/packages/configs/tsconfig.json index a9678d54..7a0db7b5 100644 --- a/packages/configs/tsconfig.json +++ b/packages/configs/tsconfig.json @@ -1,33 +1,10 @@ { + "extends": "./typescript/base.json", "compilerOptions": { - "target": "ESNext", - "module": "Preserve", - "moduleResolution": "Bundler", - "esModuleInterop": true, - "resolveJsonModule": true, - "sourceMap": true, - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "isolatedModules": true, - "moduleDetection": "force", - "useDefineForClassFields": true, - "noImplicitThis": true, - "noImplicitAny": true, - "noErrorTruncation": true, - "experimentalDecorators": true, - "removeComments": true, - "verbatimModuleSyntax": true, - "incremental": true, - "tsBuildInfoFile": "../node_modules/.cache/tsconfig.tsbuildinfo", - "paths": { - "@/*": ["src/*"] - }, + "baseUrl": "." }, - "vueCompilerOptions": { - "strictTemplates": true, - "htmlAttributes": ["aria-*", "data-*"], - "fallthroughAttributes": true - }, - "exclude": ["node_modules"] + "include": [ + "eslint/**/*.ts", + "*.ts" + ] } diff --git a/packages/configs/typescript/base.json b/packages/configs/typescript/base.json new file mode 100644 index 00000000..f0aec441 --- /dev/null +++ b/packages/configs/typescript/base.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "Preserve", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "isolatedModules": true, + "moduleDetection": "force", + "useDefineForClassFields": true, + "noImplicitThis": true, + "noImplicitAny": true, + "noErrorTruncation": true, + "experimentalDecorators": true, + "removeComments": true, + "verbatimModuleSyntax": true, + "incremental": true, + "tsBuildInfoFile": "../../../node_modules/.cache/tsconfig.tsbuildinfo", + "paths": { + "@/*": ["./src/*"] + }, + }, + "vueCompilerOptions": { + "strictTemplates": true, + "htmlAttributes": ["aria-*", "data-*"], + "fallthroughAttributes": true + }, + "exclude": ["node_modules"] +} diff --git a/packages/configs/unocss.ts b/packages/configs/unocss.ts new file mode 100644 index 00000000..4804fd10 --- /dev/null +++ b/packages/configs/unocss.ts @@ -0,0 +1,15 @@ +import { defineConfig, presetUno } from 'unocss'; + +export const defaultConfig = defineConfig({ + presets: [ + presetUno({ + prefix: 'uno-', + preflight: false + }) + ], + theme: { + colors: { + background: 'rgb(var(--j-color-background))' + } + } +}); diff --git a/packages/vite-plugins/eslint.config.ts b/packages/vite-plugins/eslint.config.ts new file mode 100644 index 00000000..096918e6 --- /dev/null +++ b/packages/vite-plugins/eslint.config.ts @@ -0,0 +1,8 @@ +import type { Linter } from 'eslint'; +import { getBaseConfig, getTSVueConfig, getNodeFiles } from '@jellyfin-vue/configs/eslint'; + +export default [ + ...getBaseConfig('@jellyfin-vue/vite-plugins'), + ...getTSVueConfig(false, import.meta.dirname), + ...getNodeFiles() +] satisfies Linter.Config[]; diff --git a/packages/vite-plugins/package.json b/packages/vite-plugins/package.json index 157d431b..4ddb508a 100644 --- a/packages/vite-plugins/package.json +++ b/packages/vite-plugins/package.json @@ -3,8 +3,15 @@ "version": "0.0.1", "exports": "./index.ts", "type": "module", + "scripts": { + "lint": "eslint . --max-warnings=0 --flag unstable_ts_config", + "lint:fix": "eslint . --fix --max-warnings=0 --flag unstable_ts_config", + "lint:inspect": "eslint-config-inspector", + "check:types": "vue-tsc" + }, "devDependencies": { + "@jellyfin-vue/configs": "*", "rollup-plugin-visualizer": "5.12.0", - "vite": "5.4.3" + "vite": "5.4.6" } } diff --git a/packages/vite-plugins/tsconfig.json b/packages/vite-plugins/tsconfig.json new file mode 100644 index 00000000..78cab3c0 --- /dev/null +++ b/packages/vite-plugins/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@jellyfin-vue/configs/typescript/base.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": [ + "*.ts" + ] +} diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index cf1333ce..0a249396 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -12,6 +12,7 @@ ENV COMMIT_HASH=$COMMIT_HASH COPY package.json package-lock.json .npmrc /app/ COPY frontend /app/frontend +COPY packages /app/packages WORKDIR /app/frontend # Build client diff --git a/packaging/tauri/package.json b/packaging/tauri/package.json index ae4ba61c..e0c5c90f 100644 --- a/packaging/tauri/package.json +++ b/packaging/tauri/package.json @@ -6,7 +6,10 @@ "start": "npm run gen-icon && tauri dev", "gen-icon": "tauri icon ../../frontend/public/icon.svg", "build": "npm run gen-icon && tauri build", - "clean": "git clean -fxd" + "clean": "git clean -fxd", + "lint": "true", + "lint:fix": "true", + "check:types": "true" }, "dependencies": { "@jellyfin-vue/frontend": "*"