build(lint): migrate to eslint 10 + @nextcloud/eslint-config 9 - #146
Merged
Conversation
Brings the template onto the stack Nextcloud's own apps run (nextcloud/forms is the reference): eslint 10, flat config, ESM. New apps scaffolded from this repo now start there instead of on eslint 8. Follows ConductionNL/larpingapp#325, which piloted this. `eslint.config.mjs` here is the fleet's canonical copy — the only parts an app should change are the app-specific globals and file-scoped exemptions at the end. FOUR THINGS THAT HAVE TO BE RIGHT TOGETHER ------------------------------------------ 1. Node 22. `@nextcloud/eslint-config@9` declares `engines.node: ^22.14 || ^24 || >=26` and imports `findPackageJSON` from `node:module`, first available in 22.14. On Node 20 eslint dies before linting a single file. The shared workflows now default to Node 22 (ConductionNL/.github#450); `.nvmrc` and `engines.node` say the same here. 2. The two peer dependencies. `vue-eslint-parser` is NOT a dependency of @nextcloud/eslint-config — it is a peer of the `eslint-plugin-vue@10` it bundles, so the app must supply it at ^10, and `@typescript-eslint/parser` must be resolvable at the top level because vue-eslint-parser requires it by name. Get this wrong and `typescript-eslint/base` — the only other config object claiming `**/*.vue` — parses every SFC as TypeScript, every `.vue` file fails with `Parsing error`, and since eslint reports a parse failure as ONE finding and lints nothing else in the file, the whole Vue layer goes unchecked while the problem count looks small. 3. An `overrides` entry for nc-vue's eslint peer. It declares `eslint: "^8.56.0 || ^9.0.0"` as an OPTIONAL peer — optional means npm will not install it, NOT that a mismatched version is accepted — so a fresh `npm install` fails with ERESOLVE against eslint 10. `"@conduction/nextcloud-vue": { "eslint": "$eslint" }` resolves it declaratively, with no --force and no --legacy-peer-deps. The constraint is inert for us: it exists because nc-vue ships an eslint preset this config no longer imports. Remove the override once nc-vue widens the range. 4. The jsdoc override must carry NC's own `ignores`. v9 registers the jsdoc plugin ONLY inside `nextcloud/documentation/*`, and every one of those blocks ignores `**/tests/**`, `**/*.spec.*` and friends — Nextcloud does not require JSDoc in tests. Naming a `jsdoc/*` rule for a test file therefore references a plugin that is not registered there and eslint refuses to run at all: with `lint: "eslint src tests"` this took out ALL 12 files under tests/ while src/ was fine. conductionVue3Fixes AND FlatCompat ARE GONE ------------------------------------------- Measured with --print-config against v9: 21 of 21 `vue/no-deprecated-*` are enabled, and `vue/no-v-model-argument` / `vue/no-v-for-template-key` — the two Vue-2 rules the preset had to switch off because they are inverted under Vue 3 — are not enabled at all. Nothing left for it to correct. CODE FIXED RATHER THAN RULES RELAXED ------------------------------------ NC sets `no-console: [2,{}]` with no allowances and `no-unused-vars` with `ignoreRestSiblings: false` / `caughtErrors: 'all'`. Fleet policy is that we may be STRICTER than Nextcloud, never merely different: - 5 console statements -> `src/logger.js` (@nextcloud/logger), the logger Nextcloud's own apps use; it tags each line with app id and acting user. - `let x = null` before a try/catch that always assigns -> no initialiser (`no-useless-assignment`). - `v == null` -> `v === null || v === undefined` (`eqeqeq`). - a rethrow inside catch now carries `{ cause: err }` (`preserve-caught-error`), so the original stack survives. - `./_app-url` -> `./_app-url.ts` (`import-extensions/extensions`), verified with `playwright test --list`: still 10 tests in 3 files. - ExampleWidget's `title` prop was declared, passed by src/exampleWidget.js and never read (`vue/no-unused-properties`). It is now bound to the table's `ariaLabel`, which gives the list an accessible name it lacked under `hideHeader` — the dashboard chrome already draws the visible heading, so rendering it again would duplicate it. - 9 template attributes kebab -> camelCase, by --fix. This is NC's v9 house style, verified with --print-config: `vue/attribute-hyphenation: [2,"never"]`. The only widening is in the tests block: `_`/`__` throwaway bindings. varsIgnorePattern is `^_+$` — underscores ONLY, so a real name that merely starts with `_` is still reported — while argsIgnorePattern is `^_`, which is exactly what NC's own TypeScript block uses.⚠️ `perfectionist/sort-imports` MOVED TWO SPDX HEADERS into the middle of an import block (it carries a leading comment with its import). Restored to the top of `src/store/modules/{object,settings}.js`. Worth checking on every app that runs --fix; the two other files whose SPDX sits below line 3 were verified as pre-existing and legitimate (inside a `<script>` block and a docblock). VERIFIED -------- npm run lint PASS (eslint src tests --max-warnings 0 — no warnings) npm run format PASS npm run stylelint PASS npm run check:specs PASS npm run build PASS 30 files linted, all 8 .vue parsed, 0 fatal positive control 7 planted violations in a .vue probe are caught
rubenvdlinde
requested review from
Rem-Dam,
SudoThijn,
WilcoLouwerse,
bbrands02,
remko48 and
rjzondervan
as code owners
August 14, 2026 09:11
Contributor
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 721/721 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-14 09:16 UTC
Download the full PDF report from the workflow artifacts.
This was referenced Aug 14, 2026
Merged
Merged
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the template onto the stack Nextcloud's own apps run: eslint 10 +
@nextcloud/eslint-config@9, flat config, ESM. New apps scaffolded from this repo now start there instead of on eslint 8.eslint.config.mjshere is the fleet's canonical copy.Follows ConductionNL/larpingapp#325 (the pilot) and ConductionNL/.github#450 (Node 22 default).
Four things that must be right together
1. Node 22.
@nextcloud/eslint-config@9declaresengines.node: ^22.14 || ^24 || >=26and importsfindPackageJSONfromnode:module(first available in 22.14). On Node 20 eslint dies before linting a single file.2. The two peer dependencies.
vue-eslint-parseris not a dependency of the NC config — it is a peer of theeslint-plugin-vue@10it bundles, so the app supplies it at^10;@typescript-eslint/parsermust be resolvable at top level because vue-eslint-parser requires it by name. Get this wrong andtypescript-eslint/base— the only other config object claiming**/*.vue— parses every SFC as TypeScript. Every.vuefile then fails withParsing error, and eslint reports a parse failure as one finding and lints nothing else in that file, so the entire Vue layer goes unchecked while the problem count looks small.3. An⚠️ An existing lockfile hides this, because
overridesentry for nc-vue's eslint peer. nc-vue declareseslint: "^8.56.0 || ^9.0.0"as an optional peer — optional means npm won't install it, not that a mismatched version is accepted — so a freshnpm installERESOLVEs against eslint 10."@conduction/nextcloud-vue": { "eslint": "$eslint" }fixes it declaratively: no--force, no--legacy-peer-deps. The constraint is inert for us — it exists only because nc-vue ships an eslint preset this config no longer imports.npm cidoesn't re-resolve peers.4. The jsdoc override must carry NC's own
ignores. v9 registers the jsdoc plugin only insidenextcloud/documentation/*, and every one of those blocks ignores**/tests/**,**/*.spec.*etc. — Nextcloud doesn't require JSDoc in tests. Naming ajsdoc/*rule for a test file references a plugin that isn't registered there, and eslint refuses to run at all: this took out all 12 files under tests/ while src/ was fine.conductionVue3FixesandFlatCompatare goneMeasured with
--print-configagainst v9: 21 of 21vue/no-deprecated-*enabled, andvue/no-v-model-argument/vue/no-v-for-template-key— the two inverted Vue-2 rules the preset had to switch off — aren't enabled at all. Nothing left to correct.Code fixed rather than rules relaxed
NC sets
no-console: [2,{}]with no allowances. Policy is stricter-than-Nextcloud is fine, different is not:src/logger.js(@nextcloud/logger), as NC's own apps dolet x = nullbefore a try/catch that always assigns → no initialiserv == null→v === null || v === undefined{ cause: err }, so the original stack survives./_app-url→./_app-url.ts, verified withplaywright test --list(still 10 tests in 3 files)titleprop was declared, passed bysrc/exampleWidget.js, and never read — now bound to the table'sariaLabel, giving the list an accessible name it lacked underhideHeadervue/attribute-hyphenation: [2,"never"])The only widening is
_/__throwaways in tests:varsIgnorePattern: '^_+$'is underscores only, so a real name starting with_is still reported;argsIgnorePattern: '^_'matches NC's own TypeScript block.perfectionist/sort-importsmoved two SPDX headersIt carries a leading comment along with its import, which pushed the SPDX header into the middle of the import block in
src/store/modules/{object,settings}.js. Restored. Worth checking on every app that runs--fix. The two other files whose SPDX sits below line 3 were verified as pre-existing and legitimate (inside a<script>block and a docblock).Verified
npm run linteslint src tests --max-warnings 0, so zero warnings toonpm run formatnpm run stylelintnpm run check:specsnpm run build.vueparsed, 0 fatal.vueprobe caught