|
| 1 | +// SPDX-License-Identifier: EUPL-1.2 |
| 2 | +// SPDX-FileCopyrightText: 2026 Conduction B.V. |
| 3 | +// |
| 4 | +// eslint 10 + @nextcloud/eslint-config 9 — the same stack Nextcloud's own apps |
| 5 | +// run (nextcloud/forms is the reference). Flat config, ESM. |
| 6 | +// |
| 7 | +// This file is the fleet's canonical shape. Copy it verbatim into an app; the |
| 8 | +// only parts that should ever differ are the last two blocks (app-specific |
| 9 | +// globals and file-scoped exemptions). |
| 10 | +// |
| 11 | +// WHY `.mjs` AND NOT `"type": "module"` IN package.json |
| 12 | +// ----------------------------------------------------- |
| 13 | +// `@nextcloud/eslint-config@9` is `"type": "module"`, so the config importing it |
| 14 | +// must be ESM. forms achieves that by making the whole package ESM; these apps |
| 15 | +// cannot — `webpack.config.js`, `vitest.config.js` and the `tests/**` CLI |
| 16 | +// checkers are CommonJS and would stop parsing. Naming the config `.mjs` scopes |
| 17 | +// the module system to the one file that needs it. |
| 18 | +// |
| 19 | +// 🔴 NODE 22 IS REQUIRED, NOT PREFERRED |
| 20 | +// ------------------------------------- |
| 21 | +// `@nextcloud/eslint-config@9` declares `engines.node: ^22.14 || ^24 || >=26` |
| 22 | +// and imports `findPackageJSON` from `node:module`, an API that first exists in |
| 23 | +// 22.14. On Node 20 eslint dies before linting a single file with |
| 24 | +// `SyntaxError: … does not provide an export named 'findPackageJSON'`, and npm |
| 25 | +// reports the mismatch only as an EBADENGINE warning it continues past. |
| 26 | +// |
| 27 | +// 🔴 THE PEER DEPENDENCIES ARE LOAD-BEARING |
| 28 | +// ----------------------------------------- |
| 29 | +// `vue-eslint-parser` is NOT a dependency of `@nextcloud/eslint-config` — it is |
| 30 | +// a peer of the `eslint-plugin-vue@10` it bundles, so the APP must supply it, |
| 31 | +// at `^10`. If a stale `eslint-plugin-vue@^9` / `vue-eslint-parser@^9` is left |
| 32 | +// in devDependencies it hoists over the bundled copy, `vue/base/setup-for-vue` |
| 33 | +// then supplies NO parser, and `typescript-eslint/base` — which also claims |
| 34 | +// `**/*.vue` — parses every SFC as TypeScript. Every `.vue` file fails with |
| 35 | +// `Parsing error: Expression expected`, and because eslint reports a parse |
| 36 | +// failure as ONE finding and lints nothing else in that file, the whole Vue |
| 37 | +// layer goes unchecked while the problem count looks small. |
| 38 | +// `@typescript-eslint/parser` must likewise be resolvable from the top level: |
| 39 | +// `vue-eslint-parser` requires it by name for `<script>` blocks. |
| 40 | +// |
| 41 | +// 🔴 AND package.json CARRIES AN `overrides` ENTRY FOR THIS (no comments are |
| 42 | +// possible there, so it is explained here): |
| 43 | +// |
| 44 | +// "overrides": { "@conduction/nextcloud-vue": { "eslint": "$eslint" } } |
| 45 | +// |
| 46 | +// `@conduction/nextcloud-vue` declares `eslint: "^8.56.0 || ^9.0.0"` as a peer. |
| 47 | +// It is marked OPTIONAL, which means npm will not INSTALL it — but if eslint is |
| 48 | +// present anyway, the version must still satisfy that range, so a fresh |
| 49 | +// `npm install` fails with ERESOLVE against eslint 10. The peer exists only |
| 50 | +// because nc-vue ships an eslint preset (`@conduction/nextcloud-vue/eslint`), |
| 51 | +// which this config no longer imports, so the constraint is inert for us. |
| 52 | +// `$eslint` pins it to whatever the root declares. Drop the override once |
| 53 | +// nc-vue widens the range to include ^10 (ConductionNL/nextcloud-vue). |
| 54 | +// |
| 55 | +// ⚠️ An EXISTING lockfile hides this: `npm ci` does not re-resolve peers, so an |
| 56 | +// app can look fine until someone adds a dependency. |
| 57 | +// |
| 58 | +// WHAT THIS FILE NO LONGER NEEDS |
| 59 | +// ------------------------------ |
| 60 | +// `conductionVue3Fixes` and the `FlatCompat` bridge to the eslintrc-era |
| 61 | +// `@nextcloud` config are GONE. That preset existed to patch a Vue-2 ruleset. |
| 62 | +// Measured against v9's `recommended` with `--print-config`, every one of its |
| 63 | +// jobs is already done: 21 of 21 `vue/no-deprecated-*` rules are enabled, and |
| 64 | +// `vue/no-v-model-argument` / `vue/no-v-for-template-key` — the two rules the |
| 65 | +// preset had to switch off because they are inverted under Vue 3 — are not |
| 66 | +// enabled at all, so there is nothing left to disable. |
| 67 | +import { recommended } from '@nextcloud/eslint-config' |
| 68 | +import eslintConfigPrettier from 'eslint-config-prettier' |
| 69 | + |
| 70 | +export default [ |
| 71 | + ...recommended, |
| 72 | + |
| 73 | + { |
| 74 | + // 🔴 SCOPED, and it must be. Flat config resolves a rule's plugin from the |
| 75 | + // config object the rule sits in, and v9 also lints `.json` (it ships |
| 76 | + // `@eslint/json`), where `jsdoc` is not registered. An unscoped `jsdoc/*` |
| 77 | + // override aborts the entire run with "The jsdoc plugin is not defined in |
| 78 | + // your configuration file" — a hard config error, not a lint finding. |
| 79 | + // |
| 80 | + // 🔴 THE `ignores` ARE PART OF THAT SCOPE, NOT AN OPINION. v9 registers the |
| 81 | + // jsdoc plugin ONLY inside `nextcloud/documentation/*`, and every one of |
| 82 | + // those blocks carries exactly this ignore list — Nextcloud does not |
| 83 | + // require JSDoc in tests. Referencing a `jsdoc/*` rule for a test file |
| 84 | + // therefore names a plugin that is not registered there, and eslint |
| 85 | + // refuses to run at all. Measured: with `lint: "eslint src tests"` this |
| 86 | + // took out ALL 12 files under tests/ while src/ was fine. |
| 87 | + files: ['**/*.js', '**/*.mjs', '**/*.ts', '**/*.tsx', '**/*.vue'], |
| 88 | + ignores: [ |
| 89 | + '**/*.test.*', |
| 90 | + '**/*.spec.*', |
| 91 | + '**/*.cy.*', |
| 92 | + '**/test/**', |
| 93 | + '**/tests/**', |
| 94 | + '**/__tests__/**', |
| 95 | + '**/__mocks__/**', |
| 96 | + ], |
| 97 | + rules: { |
| 98 | + // `@spec` (hydra gate-16 / gate-19 traceability) and `@visual` (the |
| 99 | + // visual-coverage gate) are this fleet's own JSDoc tags. v9 configures |
| 100 | + // `jsdoc/check-tag-names` with no `definedTags`, so without this every |
| 101 | + // annotation reports as an invalid tag name. |
| 102 | + // |
| 103 | + // It must be passed as RULE OPTIONS: once a preset has configured the |
| 104 | + // rule, it reads `definedTags` from its own options object and |
| 105 | + // `settings.jsdoc.definedTags` is ignored. |
| 106 | + 'jsdoc/check-tag-names': ['error', { definedTags: ['spec', 'visual'] }], |
| 107 | + }, |
| 108 | + }, |
| 109 | + |
| 110 | + { |
| 111 | + // `t` and `n` are imported for translation wiring that is not always called |
| 112 | + // yet. For `.ts`/`.vue` v9 turns the CORE `no-unused-vars` off and drives |
| 113 | + // `@typescript-eslint/no-unused-vars` instead, so the pattern belongs on |
| 114 | + // the TS rule. |
| 115 | + // |
| 116 | + // 🔴 SCOPED for the same reason as the block above: in an unscoped object |
| 117 | + // this applies to plain `.js` too, where v9 has NOT registered |
| 118 | + // `@typescript-eslint`, and eslint then refuses to run at all with |
| 119 | + // "could not find plugin @typescript-eslint". |
| 120 | + // |
| 121 | + // ⚠️ The swap is per-file-type, not global: `--print-config` on a `.js` |
| 122 | + // file reports `@typescript-eslint/no-unused-vars: undefined` and core |
| 123 | + // `no-unused-vars: [2, …]`. Plain `.js` therefore gets no ignore pattern |
| 124 | + // from here, which is deliberate — widening it would hide dead bindings. |
| 125 | + files: ['**/*.ts', '**/*.tsx', '**/*.vue'], |
| 126 | + rules: { |
| 127 | + '@typescript-eslint/no-unused-vars': [ |
| 128 | + 'error', |
| 129 | + { |
| 130 | + varsIgnorePattern: '^(t|n)$', |
| 131 | + argsIgnorePattern: '^_', |
| 132 | + ignoreRestSiblings: true, |
| 133 | + }, |
| 134 | + ], |
| 135 | + }, |
| 136 | + }, |
| 137 | + |
| 138 | + { |
| 139 | + // Node-side CLI checkers under tests/ legitimately use console and |
| 140 | + // process.exit, and ship as plain JS with no shebang. A GLOB, not a file |
| 141 | + // list: an explicit list silently stopped covering every new checker. |
| 142 | + files: ['tests/**/*.js', 'tests/**/*.mjs', 'tests/**/*.ts'], |
| 143 | + rules: { |
| 144 | + 'no-console': 'off', |
| 145 | + 'n/no-process-exit': 'off', |
| 146 | + 'n/hashbang': 'off', |
| 147 | + // `_` / `__` as a deliberate throwaway binding — `catch (_)`, a |
| 148 | + // discarded destructuring slot. Narrow on purpose: the pattern matches |
| 149 | + // UNDERSCORES ONLY, so a real name that happens to start with `_` is |
| 150 | + // still reported. v9 drives plain `.js` through the CORE rule (the |
| 151 | + // `@typescript-eslint` swap is per-file-type), so it is set here. |
| 152 | + 'no-unused-vars': [ |
| 153 | + 'error', |
| 154 | + { |
| 155 | + // vars/caught: UNDERSCORES ONLY, so a real name that merely |
| 156 | + // starts with `_` is still reported. |
| 157 | + varsIgnorePattern: '^_+$', |
| 158 | + caughtErrors: 'all', |
| 159 | + caughtErrorsIgnorePattern: '^_+$', |
| 160 | + // args: leading underscore, which is what NC's own TypeScript |
| 161 | + // block uses (`argsIgnorePattern: '^_'`) — a positional |
| 162 | + // parameter often has to keep a descriptive name to document |
| 163 | + // the signature even when the body ignores it. |
| 164 | + argsIgnorePattern: '^_', |
| 165 | + ignoreRestSiblings: true, |
| 166 | + }, |
| 167 | + ], |
| 168 | + // Tests import devDependencies by definition; this rule is about what |
| 169 | + // ships in the published package, which tests/ never does. |
| 170 | + 'n/no-unpublished-import': 'off', |
| 171 | + }, |
| 172 | + }, |
| 173 | + |
| 174 | + { |
| 175 | + // Test globals. Several apps keep their spec files INSIDE `src/`, which the |
| 176 | + // lint script scans, and neither `@nextcloud/eslint-config` nor the runner |
| 177 | + // declares the framework globals. Without this, `no-undef` reports every |
| 178 | + // `describe` / `it` / `expect` as undefined: 1203 findings in openregister |
| 179 | + // alone, all from 7 identifiers. |
| 180 | + // |
| 181 | + // This is describing the environment, not relaxing a rule — the same reason |
| 182 | + // a webpack `require.context` file declares `require`. It matters that they |
| 183 | + // are declared rather than suppressed: 1203 fake findings would bury any |
| 184 | + // REAL `no-undef` in the same app, and `no-undef` is the rule that catches |
| 185 | + // a genuine typo'd identifier. |
| 186 | + files: [ |
| 187 | + '**/*.{test,spec}.{js,mjs,ts,tsx,vue}', |
| 188 | + '**/{test,tests,__tests__,__mocks__}/**/*.{js,mjs,ts,tsx,vue}', |
| 189 | + ], |
| 190 | + languageOptions: { |
| 191 | + globals: { |
| 192 | + describe: 'readonly', |
| 193 | + it: 'readonly', |
| 194 | + test: 'readonly', |
| 195 | + expect: 'readonly', |
| 196 | + beforeEach: 'readonly', |
| 197 | + afterEach: 'readonly', |
| 198 | + beforeAll: 'readonly', |
| 199 | + afterAll: 'readonly', |
| 200 | + vi: 'readonly', |
| 201 | + jest: 'readonly', |
| 202 | + suite: 'readonly', |
| 203 | + }, |
| 204 | + }, |
| 205 | + }, |
| 206 | + |
| 207 | + // eslint-config-prettier LAST OF THE PRESETS, and it has to be: it only turns |
| 208 | + // rules OFF, and what it turns off is everything prettier owns — including the |
| 209 | + // `@stylistic/*` family v9 introduces (`indent`, `quotes`, `semi`). |
| 210 | + // |
| 211 | + // Those three AGREE with @nextcloud/prettier-config (tab / single / never), |
| 212 | + // which is why Nextcloud ships both packages. Agreement is not the point: two |
| 213 | + // tools formatting the same bytes is the unfixable state this fleet already |
| 214 | + // hit with php-cs-fixer and PHPCS, so exactly one of them is allowed an |
| 215 | + // opinion and prettier is it. `prettier --check` runs as its own CI job. |
| 216 | + // |
| 217 | + // NOTE: forms additionally uses `eslint-plugin-prettier/recommended`, which |
| 218 | + // reports prettier violations AS eslint errors. Deliberately not adopted — |
| 219 | + // this fleet already runs `prettier --check` separately, and doing both means |
| 220 | + // one defect reported twice in two places. |
| 221 | + eslintConfigPrettier, |
| 222 | + |
| 223 | + { |
| 224 | + // AFTER eslint-config-prettier, because prettier's config does NOT cover |
| 225 | + // this rule and it has to stay off. `@stylistic/exp-list-style` rewrites a |
| 226 | + // wrapped expression list to put a trailing comma before the closing paren |
| 227 | + // (`… : v,)`), which prettier immediately reformats back — the two tools |
| 228 | + // fight over the same bytes forever. `nextcloud/forms` disables exactly |
| 229 | + // this rule in its own flat config, so switching it off is matching |
| 230 | + // Nextcloud's resolution rather than diverging from it. |
| 231 | + name: 'conduction/prettier-jurisdiction', |
| 232 | + rules: { |
| 233 | + '@stylistic/exp-list-style': 'off', |
| 234 | + }, |
| 235 | + }, |
| 236 | + |
| 237 | + { |
| 238 | + // ── versioniq-specific, carried over from .eslintrc.cjs ────────────── |
| 239 | + // |
| 240 | + // This app is Vue 3 + TypeScript throughout (`<script setup lang="ts">`). |
| 241 | + name: 'versioniq/app-rules', |
| 242 | + rules: { |
| 243 | + 'vue/first-attribute-linebreak': 'off', |
| 244 | + |
| 245 | + // `void asyncFn()` is this codebase's established idiom for an |
| 246 | + // intentionally-not-awaited call (see App.vue / *Panel.vue) — it is |
| 247 | + // exactly what the `no-floating-promises`-style convention asks for, |
| 248 | + // so the generic `no-void` rule (which forbids the operator outright) |
| 249 | + // would fight every existing call site rather than catch a real bug. |
| 250 | + 'no-void': 'off', |
| 251 | + }, |
| 252 | + }, |
| 253 | + |
| 254 | + { |
| 255 | + // Test files legitimately import devDependencies (vitest, @vue/test-utils). |
| 256 | + name: 'versioniq/test-files', |
| 257 | + files: ['**/*.spec.ts'], |
| 258 | + rules: { |
| 259 | + 'n/no-unpublished-import': 'off', |
| 260 | + }, |
| 261 | + }, |
| 262 | +] |
0 commit comments