Skip to content

Commit b576ded

Browse files
rubenvdlindeConduction Release Bot
andauthored
fix(lint): migrate to flat eslint config, and give stylelint a config at all (#304)
* fix(eslint): migrate to flat config, the fleet's standard eslint could not run here at all once its version moved: Oops! Something went wrong! :( ESLint: 10.9.1 ESLint couldn't find an eslint.config.* file. versioniq was the LAST app in the fleet still on .eslintrc.cjs; every other one already ships eslint.config.mjs. That was invisible while the lockfile held eslint 8.57.1, and surfaced the moment a Dependabot group bump pulled eslint 10 -- which requires flat config. development branch eslint 8.57.1 .eslintrc.cjs works dependabot #276 eslint 10.9.1 crashes before linting a file This takes hermiq's eslint.config.mjs, which documents itself as the fleet's canonical shape ('copy it verbatim; only the last two blocks should differ'), and keeps versioniq's own rules in those two blocks with their original rationales. The jsdoc rules from .eslintrc.cjs are NOT carried over: the canonical config already scopes them and already declares the tag that gate-16 needs. Re-declaring them unscoped aborts the whole run -- which it did, once, before this was corrected. Fixing the config surfaced 247 findings it had been unable to report. 194 were auto-fixable. Of the rest: 24x import-extensions/extensions relative imports now carry .ts, checked against the file on disk rather than appended blindly 4x no-use-before-define two refs declared 90 lines below their first use, moved above it Verified: eslint exits 0 (113 warnings, 0 errors), stylelint exits 0, webpack build compiles, vitest 11 files / 58 tests pass. * fix(stylelint): give stylelint a configuration at all Aligning the lockfile for the eslint migration moved stylelint 16 -> 17, and 17 refused to start: ConfigurationError: No rules found within configuration. Have you provided a "rules" property? The cause was not the upgrade. This app declared `@nextcloud/stylelint-config` as a devDependency and then never extended it -- no config file of any kind, and no `stylelint` key in package.json. stylelint 16 tolerated that silently. A declared linter with no rules is not a lenient linter, it is an absent one: the package was installed on every CI run and judged nothing. The 16 -> 17 move did not break stylelint here, it revealed that stylelint had never been running. It now has a config pointing at the package it already depended on, written as `export default` rather than `module.exports` because this package is `"type": "module"` and a `.js` config file is therefore an ES module -- CommonJS there throws `ReferenceError: module is not defined in ES module scope`. With stylelint finally running it reported 70 findings; 59 were auto-fixable and the remainder were logical-property warnings (padding-left -> padding-inline-start), which are warnings and do not fail the run. The v3 stylelint stack is now declared explicitly rather than inherited: stylelint ^17.14.1, @nextcloud/stylelint-config ^3.2.2, stylelint-config-recommended-scss ^17.0.1, stylelint-config-recommended-vue ^1.6.1 and stylelint-config-html ^1.1.0 -- the same set shillinq and zaakafhandelapp run green. Note on the previous commit: its message lost the word `spec` from one sentence. Backticks in a double-quoted `-m` string were expanded by the shell before git saw them. It should read "already declares the `spec` tag that gate-16 requires". Left uncorrected because amending would need a force-push onto a shared branch. Verified: eslint exits 0 (113 warnings, 0 errors), stylelint exits 0, webpack build compiles, vitest 11 files / 58 tests pass. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
1 parent eba8e8c commit b576ded

35 files changed

Lines changed: 2511 additions & 1863 deletions

.eslintrc.cjs

Lines changed: 0 additions & 35 deletions
This file was deleted.

eslint.config.mjs

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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

Comments
 (0)