@nextcloud/eslint-config 9.0.0-rc.9 → 9.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,8 +4,15 @@
4
4
  -->
5
5
  # Changelog
6
6
 
7
- ## [v9.0.0](https://github.com/nextcloud-libraries/eslint-config/tree/v9.0.0) (unreleased)
7
+ ## [v9.0.1](https://github.com/nextcloud-libraries/eslint-config/tree/v9.0.1) (2026-07-07)
8
+ ### Fixed
9
+ * fix(vue): error on `vue/attributes-order` and `vue/order-in-components` instead of warn [\#1445](https://github.com/nextcloud-libraries/eslint-config/pull/1445) \([susnux](https://github.com/susnux)\)
10
+ * fix: `no-deprecated-library-*` rules incorrectly behave for `nextcloud/vue` syntax [\#1452](https://github.com/nextcloud-libraries/eslint-config/pull/1452) \([Antreesy](https://github.com/Antreesy)\)
11
+ * fix(no-deprecated-library-props): support win32 filesystem and different `@nextcloud/vue` directory structures [\#1454](https://github.com/nextcloud-libraries/eslint-config/pull/1454) \([ShGKme](https://github.com/ShGKme)\)
12
+ * fix(no-deprecated-library-props): support camelCase attributes [\#1453](https://github.com/nextcloud-libraries/eslint-config/pull/1453) \([Antreesy](https://github.com/Antreesy)\)
13
+ * fix: increase min. Node version to 22.14 [\#1455](https://github.com/nextcloud-libraries/eslint-config/pull/1455) \([susnux](https://github.com/susnux)\)
8
14
 
15
+ ## [v9.0.0](https://github.com/nextcloud-libraries/eslint-config/tree/v9.0.0) (2026-06-26)
9
16
  ### Breaking
10
17
  This package now is using ESLint v10 and requires ESLint flat configurations.
11
18
  Please refer to the README on how to adjust your configuration for flat config.
@@ -44,6 +51,7 @@ either using the sledge hammer method (remove the lock file and run `npm i`) or
44
51
  * feat(vue3): force camelCase for events in `<template>` [\#1263](https://github.com/nextcloud-libraries/eslint-config/pull/1263) \([ShGKme](https://github.com/ShGKme)\)
45
52
  * feat(vue3): force camelCase for slot names [\#1264](https://github.com/nextcloud-libraries/eslint-config/pull/1264) \([ShGKme](https://github.com/ShGKme)\)
46
53
  * feat(vue3): force camelCase for props in template [\#1266](https://github.com/nextcloud-libraries/eslint-config/pull/1266) \([ShGKme](https://github.com/ShGKme)\)
54
+ * feat(import): enforce consistent types imports and ban usage of inline type specifiers [\#1382](https://github.com/nextcloud-libraries/eslint-config/pull/1382) \([susnux](https://github.com/susnux)\)
47
55
 
48
56
  ### Fixed
49
57
  * fix(codestyle): do not require splitting chains [\#951](https://github.com/nextcloud-libraries/eslint-config/pull/951)
@@ -81,6 +89,7 @@ either using the sledge hammer method (remove the lock file and run `npm i`) or
81
89
  * fix: switch to `@stylistic/exp-list-style` to resolve array edge-cases [\#1203](https://github.com/nextcloud-libraries/eslint-config/pull/1203) \([susnux](https://github.com/susnux)\)
82
90
  * fix(nextcloud-plugin): add more removed API in Nextcloud 33 [\#1269](https://github.com/nextcloud-libraries/eslint-config/pull/1269) \([susnux](https://github.com/susnux)\)
83
91
  * fix: use vue-eslint-parser directly [\#1277](https://github.com/nextcloud-libraries/eslint-config/pull/1277) \([susnux](https://github.com/susnux)\)
92
+ * fix(imports): no newline inside import statements [\#1379](https://github.com/nextcloud-libraries/eslint-config/pull/1379) \([max-nextcloud](https://github.com/max-nextcloud)\)
84
93
 
85
94
  ### Changed
86
95
  * Add SPDX header [#802](https://github.com/nextcloud-libraries/eslint-config/pull/802)
@@ -89,6 +98,7 @@ either using the sledge hammer method (remove the lock file and run `npm i`) or
89
98
  * refactor: use camelCase for internal variables [#971](https://github.com/nextcloud-libraries/eslint-config/pull/971) ([susnux](https://github.com/susnux))
90
99
  * chore: do not bundle config but just use Typescript [#967](https://github.com/nextcloud-libraries/eslint-config/pull/967)
91
100
  * chore: require node ^20.19 [\#982](https://github.com/nextcloud-libraries/eslint-config/pull/982)
101
+ * chore: remove EOL Node version and add new future LTS [\#1381](https://github.com/nextcloud-libraries/eslint-config/pull/1381) \([susnux](https://github.com/susnux)\)
92
102
  * chore: allow to run `npm lint` without compile step [#975](https://github.com/nextcloud-libraries/eslint-config/pull/975) ([susnux](https://github.com/susnux))
93
103
  * chore: prepare v9.0.0-rc.1 [#1010](https://github.com/nextcloud-libraries/eslint-config/pull/1010) ([susnux](https://github.com/susnux))
94
104
  * docs: consistent badge style with the reuse one [#1060](https://github.com/nextcloud-libraries/eslint-config/pull/1060) ([susnux](https://github.com/susnux))
@@ -103,18 +113,18 @@ either using the sledge hammer method (remove the lock file and run `npm i`) or
103
113
  * chore(gitignore): ignore IDE configs [\#1265](https://github.com/nextcloud-libraries/eslint-config/pull/1265) \([ShGKme](https://github.com/ShGKme)\)
104
114
  * test(vue3): add some formatting tests [\#1267](https://github.com/nextcloud-libraries/eslint-config/pull/1267) \([ShGKme](https://github.com/ShGKme)\)
105
115
  * refactor: merge Nextcloud specific rules into the `@nextcloud` plugin [\#1278](https://github.com/nextcloud-libraries/eslint-config/pull/1278) \([susnux](https://github.com/susnux)\)
106
- * Updated `@eslint/json` to 0.14.0
116
+ * Updated `@eslint/json` to 2.0.0
107
117
  * Updated `@stylistic/eslint-plugin` to 5.7.0
108
118
  * Updated `eslint-config-flat-gitignore` to 2.1.0
109
- * Updated `eslint-plugin-antfu` to 3.1.3
110
- * Updated `eslint-plugin-jsdoc` to 62.0.0
111
- * Updated `eslint-plugin-perfectionist` to 5.3.1
112
- * Updated `eslint-plugin-vue` to 10.6.2
113
- * Updated `fast-xml-parser` to 5.3.3
114
- * Updated `globals` to 17.0.0
115
- * Updated `semver` to 7.7.3
116
- * Updated `sort-package-json` to 3.6.0
117
- * Updated `typescript-eslint` to 8.53.0
119
+ * Updated `eslint-plugin-antfu` to 3.2.3
120
+ * Updated `eslint-plugin-jsdoc` to 63.0.4
121
+ * Updated `eslint-plugin-perfectionist` to 5.9.1
122
+ * Updated `eslint-plugin-vue` to 10.9.2
123
+ * Updated `fast-xml-parser` to 5.9.0
124
+ * Updated `globals` to 17.7.0
125
+ * Updated `semver` to 7.8.4
126
+ * Updated `sort-package-json` to 4.0.0
127
+ * Updated `typescript-eslint` to 8.62.0
118
128
 
119
129
  ## [v8.4.2](https://github.com/nextcloud-libraries/eslint-config/tree/v8.4.2) (2025-02-16)
120
130
  ### Fixed
@@ -30,6 +30,8 @@ export function imports(options) {
30
30
  rules: {
31
31
  // Require file extensions
32
32
  'import-extensions/extensions': 'error',
33
+ // enforce explicit type imports (no inline types)
34
+ 'import-extensions/ban-inline-type-imports': 'error',
33
35
  // Sorting of imports
34
36
  'sort-imports': 'off',
35
37
  'perfectionist/sort-imports': [
@@ -96,7 +98,7 @@ export function imports(options) {
96
98
  function createSortingConfig(type) {
97
99
  return {
98
100
  type: 'natural',
99
- newlinesBetween: 1,
101
+ newlinesBetween: 0,
100
102
  partitionByNewLine: false,
101
103
  groups: [
102
104
  `type-${type}`,
@@ -123,7 +123,11 @@ export function vue(options) {
123
123
  'vue/padding-line-between-blocks': 'error',
124
124
  // Prefer separated static and dynamic class attributes
125
125
  'vue/prefer-separate-static-class': 'error',
126
+ // For consistent layout of the template
127
+ 'vue/attributes-order': 'error',
126
128
  // For consistent layout of components
129
+ 'vue/order-in-components': 'error',
130
+ // For consistent layout of script-setup components
127
131
  'vue/define-macros-order': [
128
132
  'error',
129
133
  {
@@ -4,6 +4,7 @@
4
4
  */
5
5
  declare const _default: {
6
6
  rules: {
7
+ 'ban-inline-type-imports': import("eslint").Rule.RuleModule;
7
8
  extensions: import("eslint").Rule.RuleModule;
8
9
  };
9
10
  meta: {
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3
+ * SPDX-FileCopyrightText: 2015 Ben Mosher
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import type { Rule } from 'eslint';
7
+ export declare const rule: Rule.RuleModule;
@@ -0,0 +1,176 @@
1
+ /*!
2
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3
+ * SPDX-FileCopyrightText: 2015 Ben Mosher
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ export const rule = {
7
+ meta: {
8
+ fixable: 'code',
9
+ type: 'suggestion',
10
+ docs: {
11
+ dialects: ['typescript'],
12
+ description: 'Ban the use of inline type-only markers for named imports.',
13
+ },
14
+ messages: {
15
+ preferTopLevel: 'Prefer using a top-level type-only import instead of inline type specifiers.',
16
+ },
17
+ },
18
+ create(context) {
19
+ return {
20
+ ImportDeclaration(node) {
21
+ if (!('importKind' in node)
22
+ || node.importKind === 'type'
23
+ // no specifiers (import {} from '') cannot have inline - so is valid
24
+ || node.specifiers.length === 0
25
+ || (node.specifiers.length === 1
26
+ // default imports are both "inline" and "top-level"
27
+ && (node.specifiers[0].type === 'ImportDefaultSpecifier'
28
+ // namespace imports are both "inline" and "top-level"
29
+ || node.specifiers[0].type === 'ImportNamespaceSpecifier'))) {
30
+ return;
31
+ }
32
+ const typeSpecifiers = [];
33
+ const valueSpecifiers = [];
34
+ let defaultSpecifier;
35
+ for (const specifier of node.specifiers) {
36
+ if (specifier.type === 'ImportDefaultSpecifier') {
37
+ defaultSpecifier = specifier;
38
+ continue;
39
+ }
40
+ if (!('importKind' in specifier) || !specifier.importKind) {
41
+ valueSpecifiers.push(specifier);
42
+ continue;
43
+ }
44
+ if (specifier.importKind === 'type') {
45
+ typeSpecifiers.push(specifier);
46
+ }
47
+ else if (specifier.importKind === 'value') {
48
+ valueSpecifiers.push(specifier);
49
+ }
50
+ }
51
+ const typeImport = getImportText(node, context.sourceCode, typeSpecifiers);
52
+ const newImports = typeImport.trim();
53
+ if (typeSpecifiers.length === node.specifiers.length) {
54
+ // all specifiers have inline specifiers - so we replace the entire import
55
+ context.report({
56
+ node,
57
+ messageId: 'preferTopLevel',
58
+ fix(fixer) {
59
+ return fixer.replaceText(node, newImports);
60
+ },
61
+ });
62
+ }
63
+ else {
64
+ // remove specific specifiers and insert new imports for them
65
+ for (const specifier of typeSpecifiers) {
66
+ context.report({
67
+ node: specifier,
68
+ messageId: 'preferTopLevel',
69
+ fix(fixer) {
70
+ const fixes = [];
71
+ // if there are no value specifiers, then the other report fixer will be called, not this one
72
+ if (valueSpecifiers.length > 0) {
73
+ // import { Value, type Type } from 'mod';
74
+ // we can just remove the type specifiers
75
+ removeSpecifiers(fixes, fixer, context.sourceCode, typeSpecifiers);
76
+ // make the import nicely formatted by also removing the trailing comma after the last value import
77
+ // eg
78
+ // import { Value, type Type } from 'mod';
79
+ // to
80
+ // import { Value } from 'mod';
81
+ // not
82
+ // import { Value, } from 'mod';
83
+ removeCommaAfterNode(fixes, fixer, context.sourceCode, valueSpecifiers[valueSpecifiers.length - 1]);
84
+ }
85
+ else if (defaultSpecifier) {
86
+ // import Default, { type Type } from 'mod';
87
+ // remove the entire curly block so we don't leave an empty one behind
88
+ // NOTE - the default specifier *must* be the first specifier always!
89
+ // so a comma exists that we also have to clean up or else it's bad syntax
90
+ const comma = context.sourceCode.getTokenAfter(defaultSpecifier, isComma);
91
+ const closingBrace = context.sourceCode.getTokenAfter(node.specifiers[node.specifiers.length - 1], (token) => token.type === 'Punctuator' && token.value === '}');
92
+ if (comma && closingBrace) {
93
+ fixes.push(fixer.removeRange([
94
+ comma.range[0],
95
+ closingBrace.range[1],
96
+ ]));
97
+ }
98
+ }
99
+ // insert the new imports after the old declaration
100
+ return fixes.concat(fixer.insertTextAfter(node, `\n${newImports}`));
101
+ },
102
+ });
103
+ }
104
+ }
105
+ },
106
+ };
107
+ },
108
+ };
109
+ /**
110
+ * Check if the given token is a comma
111
+ *
112
+ * @param token - The token to check
113
+ */
114
+ function isComma(token) {
115
+ return token.type === 'Punctuator' && token.value === ',';
116
+ }
117
+ /**
118
+ * Remove the given specifiers from the import declaration, along with any trailing commas if necessary.
119
+ *
120
+ * @param fixes - The array to which the generated fixes will be added
121
+ * @param fixer - The fixer object used to create the fixes
122
+ * @param sourceCode - The source code object used to analyze the code and find tokens
123
+ * @param specifiers - The specifiers to remove from the import declaration
124
+ */
125
+ function removeSpecifiers(fixes, fixer, sourceCode, specifiers) {
126
+ for (const specifier of specifiers) {
127
+ removeCommaAfterNode(fixes, fixer, sourceCode, specifier);
128
+ fixes.push(fixer.remove(specifier));
129
+ }
130
+ }
131
+ /**
132
+ * Remove the trailing comma
133
+ *
134
+ * @param fixes - The array to which the generated fixes will be added
135
+ * @param fixer - The fixer object used to create the fixes
136
+ * @param sourceCode - The source code object used to analyze the code and find tokens
137
+ * @param node - The node after which to check for a comma and remove it if exists
138
+ */
139
+ function removeCommaAfterNode(fixes, fixer, sourceCode, node) {
140
+ const token = sourceCode.getTokenAfter(node);
141
+ if (token && isComma(token)) {
142
+ const nextToken = sourceCode.getTokenAfter(token);
143
+ // get the empty space to remove double whitespace after removing the comma
144
+ const emptySpace = sourceCode.text
145
+ .slice(token.range[1], nextToken?.range[0] ?? token.range[1])
146
+ .match(/^[ \t]*[\n\r]*/)?.[0] ?? '';
147
+ if (emptySpace) {
148
+ fixes.push(fixer.removeRange([token.range[0], token.range[1] + emptySpace.length]));
149
+ }
150
+ else {
151
+ fixes.push(fixer.remove(token));
152
+ }
153
+ }
154
+ }
155
+ /**
156
+ * Get the text for a new top-level type-only import based on the given inline type specifiers.
157
+ *
158
+ * @param node - The original import declaration node containing the inline type specifiers
159
+ * @param sourceCode - The source code object used to analyze the code and find tokens
160
+ * @param specifiers - The inline type specifiers for which to generate the new import text
161
+ */
162
+ function getImportText(node, sourceCode, specifiers) {
163
+ const sourceString = sourceCode.getText(node.source);
164
+ if (specifiers.length === 0) {
165
+ return '';
166
+ }
167
+ const names = specifiers.map((s) => {
168
+ const name = 'name' in s.imported ? s.imported.name : s.imported.value;
169
+ if (name === s.local.name) {
170
+ return name;
171
+ }
172
+ return `${name} as ${s.local.name}`;
173
+ });
174
+ // insert a fresh top-level import
175
+ return `import type {${names.join(', ')}} from ${sourceString};`;
176
+ }
@@ -3,5 +3,6 @@
3
3
  * SPDX-License-Identifier: AGPL-3.0-or-later
4
4
  */
5
5
  export declare const rules: {
6
+ 'ban-inline-type-imports': import("eslint").Rule.RuleModule;
6
7
  extensions: import("eslint").Rule.RuleModule;
7
8
  };
@@ -2,7 +2,9 @@
2
2
  * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3
3
  * SPDX-License-Identifier: AGPL-3.0-or-later
4
4
  */
5
+ import { rule as banInlineTypeImports } from "./ban-inline-type-imports.js";
5
6
  import { rule as extensions } from "./extensions.js";
6
7
  export const rules = {
8
+ 'ban-inline-type-imports': banInlineTypeImports,
7
9
  extensions,
8
10
  };
@@ -26,7 +26,11 @@ const rule = {
26
26
  const versionSatisfies = createLibVersionValidator(context);
27
27
  const isVersionValidForDist = versionSatisfies('8.23.0');
28
28
  const oldPattern = '@nextcloud/vue/dist/([^/]+)/([^/.]+)';
29
- const mixinPattern = '@nextcloud/vue/mixins/([^/.]+)';
29
+ // Matches both the legacy `dist/Mixins/*` path and a bare `mixins/*` path.
30
+ // There is no clean `@nextcloud/vue/mixins/*` subpath export (only components,
31
+ // composables, directives and functions), so mixins must be migrated to
32
+ // composables rather than have their import path rewritten.
33
+ const mixinPattern = '@nextcloud/vue/(?:dist/)?mixins/([^/.]+)';
30
34
  const isVersionValidForTooltip = versionSatisfies('8.25.0');
31
35
  const tooltipPattern = '@nextcloud/vue/directives/Tooltip';
32
36
  const isVersionValidForNcSettingsInputText = versionSatisfies('8.31.0');
@@ -73,6 +77,10 @@ const rule = {
73
77
  context.report({ node, messageId: 'outdatedVueLibrary' });
74
78
  return;
75
79
  }
80
+ // Mixins removed in v9 and already reported as `deprecatedMixin`
81
+ if (match[1].toLowerCase() === 'mixins') {
82
+ return;
83
+ }
76
84
  const newImportPath = `'@nextcloud/vue/${match[1].toLowerCase()}/${match[2]}'`;
77
85
  context.report({
78
86
  node,
@@ -4,6 +4,37 @@
4
4
  */
5
5
  import { createLibVersionValidator } from "../utils/lib-version-parser.js";
6
6
  import { defineTemplateBodyVisitor } from "../utils/vue-template-visitor.js";
7
+ /*
8
+ * Deprecated props can be authored in either kebab-case or camelCase:
9
+ * - camelCase is forced by `vue/attribute-hyphenation: never` shipped rule;
10
+ * - kebab-case might come from legacy code and other presets.
11
+ * A single `VAttribute` visitor normalizes the raw attribute name with `toCamelCase()`
12
+ * and dispatches to the handler registered under the canonical camelCase name.
13
+ */
14
+ /**
15
+ * Convert an attribute name to camelCase, matching Vue's own prop normalization,
16
+ * so a template attribute written in either casing maps to a single canonical name.
17
+ *
18
+ * @param name - the raw attribute name as written in the template
19
+ */
20
+ const toCamelCase = (name) => name.replace(/-(\w)/g, (_, char) => char.toUpperCase());
21
+ /**
22
+ * Resolve the raw (case-preserving) name of a template attribute: the argument
23
+ * for a directive binding (`:foo` / `v-model:foo`), or the key otherwise.
24
+ * Returns `undefined` when there is no static name (e.g. a dynamic argument).
25
+ *
26
+ * Must use `rawName`, not `name`: Vue lowercases `name` in the AST (`focusTrap`
27
+ * becomes `focustrap`), which `toCamelCase()` cannot restore, so it would no
28
+ * longer match the camelCase handler key. `rawName` keeps the original casing.
29
+ *
30
+ * @param node - the attribute or directive node
31
+ */
32
+ function getRawName(node) {
33
+ if (node.key.type === 'VDirectiveKey') {
34
+ return node.key.argument?.type === 'VIdentifier' ? node.key.argument.rawName : undefined;
35
+ }
36
+ return node.key.rawName;
37
+ }
7
38
  export default {
8
39
  meta: {
9
40
  docs: {
@@ -28,7 +59,7 @@ export default {
28
59
  useNoFocusTrapInstead: 'Using `focus-trap` is deprecated - use `no-focus-trap` instead',
29
60
  useKeepOpenInstead: 'Using `close-on-select` is deprecated - use `keep-open` instead',
30
61
  useNcSelectUsersInstead: 'Using `user-select` is deprecated - use `NcSelectUsers` component instead',
31
- useArrowEndInstead: 'Using `arrow-right` is deprecated - use `arrow-end` instead',
62
+ useArrowEndInstead: 'Using `arrowRight` as `trailing-button-icon` value is deprecated - use `arrowEnd` instead',
32
63
  removeAriaHidden: 'Using `aria-hidden` is deprecated - remove prop from components, otherwise root element will inherit incorrect attribute.',
33
64
  removeLimitWidth: 'Using `limit-width` is deprecated - remove prop from components, otherwise root element will inherit incorrect attribute.',
34
65
  removeExact: 'Using `exact` is deprecated - consult Vue Router documentation for alternatives.',
@@ -48,7 +79,7 @@ export default {
48
79
  const isDateTimePickerFormatValid = versionSatisfies('8.25.0'); // #6738
49
80
  const isNcSelectKeepOpenValid = versionSatisfies('8.25.0'); // #6791
50
81
  const isNcPopoverNoFocusTrapValid = versionSatisfies('8.26.0'); // #6808
51
- const isNcSelectUsersValid = versionSatisfies('8.27.1'); // #7032
82
+ const isNcSelectUsersValid = versionSatisfies('8.25.0'); // #6791
52
83
  const isNcTextFieldArrowEndValid = versionSatisfies('8.28.0'); // #7002
53
84
  const isCloseButtonOutsideValid = versionSatisfies('8.32.0'); // #7553
54
85
  const legacyTypes = [
@@ -60,8 +91,13 @@ export default {
60
91
  'tertiary',
61
92
  'tertiary-no-background',
62
93
  ];
63
- return defineTemplateBodyVisitor(context, {
64
- 'VElement VAttribute:has(VIdentifier[name="type"])': function (node) {
94
+ /*
95
+ * Deprecated prop handlers keyed by their canonical camelCase name. Each
96
+ * handler receives the matched attribute node and verifies the owning
97
+ * component and installed library version itself.
98
+ */
99
+ const handlers = {
100
+ type: function (node) {
65
101
  if (![
66
102
  'ncactions',
67
103
  'ncappnavigationnew',
@@ -75,8 +111,10 @@ export default {
75
111
  context.report({ node, messageId: 'outdatedVueLibrary' });
76
112
  return;
77
113
  }
78
- const hasNativeType = node.parent.attributes.find((attr) => (attr.key.name === 'native-type'
79
- || (attr.key.type === 'VDirectiveKey' && attr.key.argument && attr.key.argument.type === 'VIdentifier' && attr.key.argument.name === 'native-type')));
114
+ const hasNativeType = node.parent.attributes.some((attr) => {
115
+ const rawName = getRawName(attr);
116
+ return rawName !== undefined && toCamelCase(rawName) === 'nativeType';
117
+ });
80
118
  const isLiteral = node.value.type === 'VLiteral' && legacyTypes.includes(node.value.value);
81
119
  const isExpression = node.value.type === 'VExpressionContainer'
82
120
  && node.value.expression.type === 'ConditionalExpression'
@@ -102,7 +140,7 @@ export default {
102
140
  });
103
141
  }
104
142
  },
105
- 'VElement VAttribute:has(VIdentifier[name="native-type"])': function (node) {
143
+ nativeType: function (node) {
106
144
  if (![
107
145
  'ncbutton',
108
146
  'ncdialogbutton',
@@ -126,7 +164,7 @@ export default {
126
164
  },
127
165
  });
128
166
  },
129
- 'VElement VAttribute:has(VIdentifier[name="aria-hidden"])': function (node) {
167
+ ariaHidden: function (node) {
130
168
  if (node.parent.parent.name.startsWith('ncaction')
131
169
  || node.parent.parent.name === 'ncbutton') {
132
170
  if (!isAriaHiddenValid) {
@@ -139,7 +177,10 @@ export default {
139
177
  });
140
178
  }
141
179
  },
142
- 'VElement[name="ncappcontent"] VAttribute:has(VIdentifier[name="allow-swipe-navigation"])': function (node) {
180
+ allowSwipeNavigation: function (node) {
181
+ if (node.parent.parent.name !== 'ncappcontent') {
182
+ return;
183
+ }
143
184
  if (!isDisableSwipeValid) {
144
185
  context.report({ node, messageId: 'outdatedVueLibrary' });
145
186
  return;
@@ -149,7 +190,10 @@ export default {
149
190
  messageId: 'useDisableSwipeForNavInstead',
150
191
  });
151
192
  },
152
- 'VElement[name="ncavatar"] VAttribute:has(VIdentifier[name="show-user-status"])': function (node) {
193
+ showUserStatus: function (node) {
194
+ if (node.parent.parent.name !== 'ncavatar') {
195
+ return;
196
+ }
153
197
  if (!isDefaultBooleanFalseValid) {
154
198
  context.report({ node, messageId: 'outdatedVueLibrary' });
155
199
  return;
@@ -159,7 +203,10 @@ export default {
159
203
  messageId: 'useHideStatusInstead',
160
204
  });
161
205
  },
162
- 'VElement[name="ncavatar"] VAttribute:has(VIdentifier[name="show-user-status-compact"])': function (node) {
206
+ showUserStatusCompact: function (node) {
207
+ if (node.parent.parent.name !== 'ncavatar') {
208
+ return;
209
+ }
163
210
  if (!isDefaultBooleanFalseValid) {
164
211
  context.report({ node, messageId: 'outdatedVueLibrary' });
165
212
  return;
@@ -169,7 +216,10 @@ export default {
169
216
  messageId: 'useVerboseStatusInstead',
170
217
  });
171
218
  },
172
- 'VElement[name="ncavatar"] VAttribute:has(VIdentifier[name="allow-placeholder"])': function (node) {
219
+ allowPlaceholder: function (node) {
220
+ if (node.parent.parent.name !== 'ncavatar') {
221
+ return;
222
+ }
173
223
  if (!isDefaultBooleanFalseValid) {
174
224
  context.report({ node, messageId: 'outdatedVueLibrary' });
175
225
  return;
@@ -179,7 +229,10 @@ export default {
179
229
  messageId: 'useNoPlaceholderInstead',
180
230
  });
181
231
  },
182
- 'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="formatter"])': function (node) {
232
+ formatter: function (node) {
233
+ if (node.parent.parent.name !== 'ncdatetimepicker') {
234
+ return;
235
+ }
183
236
  if (!isDateTimePickerFormatValid) {
184
237
  context.report({ node, messageId: 'outdatedVueLibrary' });
185
238
  return;
@@ -189,7 +242,10 @@ export default {
189
242
  messageId: 'useFormatInstead',
190
243
  });
191
244
  },
192
- 'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="lang"])': function (node) {
245
+ lang: function (node) {
246
+ if (node.parent.parent.name !== 'ncdatetimepicker') {
247
+ return;
248
+ }
193
249
  if (!isVue3Valid) {
194
250
  // Do not throw for v8.X.X
195
251
  return;
@@ -199,7 +255,10 @@ export default {
199
255
  messageId: 'useLocaleInstead',
200
256
  });
201
257
  },
202
- 'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="range"])': function (node) {
258
+ range: function (node) {
259
+ if (node.parent.parent.name !== 'ncdatetimepicker') {
260
+ return;
261
+ }
203
262
  if (!isDateTimePickerFormatValid) {
204
263
  context.report({ node, messageId: 'outdatedVueLibrary' });
205
264
  return;
@@ -209,7 +268,7 @@ export default {
209
268
  messageId: 'useTypeDateRangeInstead',
210
269
  });
211
270
  },
212
- 'VElement VAttribute:has(VIdentifier[name="can-close"])': function (node) {
271
+ canClose: function (node) {
213
272
  if (![
214
273
  'ncdialog',
215
274
  'ncmodal',
@@ -225,7 +284,10 @@ export default {
225
284
  messageId: 'useNoCloseInstead',
226
285
  });
227
286
  },
228
- 'VElement[name="ncpopover"] VAttribute:has(VIdentifier[name="close-on-click-outside"])': function (node) {
287
+ closeOnClickOutside: function (node) {
288
+ if (node.parent.parent.name !== 'ncpopover') {
289
+ return;
290
+ }
229
291
  if (!isVue3Valid) {
230
292
  // Do not throw for v8.X.X
231
293
  return;
@@ -235,7 +297,10 @@ export default {
235
297
  messageId: 'useNoCloseOnClickOutsideInstead',
236
298
  });
237
299
  },
238
- 'VElement[name="ncmodal"] VAttribute:has(VIdentifier[name="enable-swipe"])': function (node) {
300
+ enableSwipe: function (node) {
301
+ if (node.parent.parent.name !== 'ncmodal') {
302
+ return;
303
+ }
239
304
  if (!isDisableSwipeValid) {
240
305
  context.report({ node, messageId: 'outdatedVueLibrary' });
241
306
  return;
@@ -245,7 +310,10 @@ export default {
245
310
  messageId: 'useDisableSwipeForModalInstead',
246
311
  });
247
312
  },
248
- 'VElement[name="ncmodal"] VAttribute:has(VIdentifier[name="close-button-contained"])': function (node) {
313
+ closeButtonContained: function (node) {
314
+ if (node.parent.parent.name !== 'ncmodal') {
315
+ return;
316
+ }
249
317
  if (!isCloseButtonOutsideValid) {
250
318
  context.report({ node, messageId: 'outdatedVueLibrary' });
251
319
  return;
@@ -255,7 +323,10 @@ export default {
255
323
  messageId: 'useCloseButtonOutsideInstead',
256
324
  });
257
325
  },
258
- 'VElement[name="ncpopover"] VAttribute:has(VIdentifier[name="focus-trap"])': function (node) {
326
+ focusTrap: function (node) {
327
+ if (node.parent.parent.name !== 'ncpopover') {
328
+ return;
329
+ }
259
330
  if (!isNcPopoverNoFocusTrapValid) {
260
331
  context.report({ node, messageId: 'outdatedVueLibrary' });
261
332
  return;
@@ -265,7 +336,10 @@ export default {
265
336
  messageId: 'useNoFocusTrapInstead',
266
337
  });
267
338
  },
268
- 'VElement[name="ncselect"] VAttribute:has(VIdentifier[name="close-on-select"])': function (node) {
339
+ closeOnSelect: function (node) {
340
+ if (node.parent.parent.name !== 'ncselect') {
341
+ return;
342
+ }
269
343
  if (!isNcSelectKeepOpenValid) {
270
344
  context.report({ node, messageId: 'outdatedVueLibrary' });
271
345
  return;
@@ -275,7 +349,10 @@ export default {
275
349
  messageId: 'useKeepOpenInstead',
276
350
  });
277
351
  },
278
- 'VElement[name="ncselect"] VAttribute:has(VIdentifier[name="user-select"])': function (node) {
352
+ userSelect: function (node) {
353
+ if (node.parent.parent.name !== 'ncselect') {
354
+ return;
355
+ }
279
356
  if (!isNcSelectUsersValid) {
280
357
  context.report({ node, messageId: 'outdatedVueLibrary' });
281
358
  return;
@@ -285,7 +362,7 @@ export default {
285
362
  messageId: 'useNcSelectUsersInstead',
286
363
  });
287
364
  },
288
- 'VElement VAttribute:has(VIdentifier[name="trailing-button-icon"])': function (node) {
365
+ trailingButtonIcon: function (node) {
289
366
  if (node.parent.parent.name !== 'nctextfield') {
290
367
  return;
291
368
  }
@@ -320,14 +397,17 @@ export default {
320
397
  });
321
398
  }
322
399
  },
323
- 'VElement[name="ncsettingssection"] VAttribute:has(VIdentifier[name="limit-width"])': function (node) {
324
- // This was deprecated in 8.13.0 (Nextcloud 30+), before first supported version by plugin
400
+ limitWidth: function (node) {
401
+ if (node.parent.parent.name !== 'ncsettingssection') {
402
+ return;
403
+ }
404
+ // This was deprecated in 8.12.0 (Nextcloud 30+), before first supported version by plugin
325
405
  context.report({
326
406
  node,
327
407
  messageId: 'removeLimitWidth',
328
408
  });
329
409
  },
330
- 'VElement VAttribute:has(VIdentifier[name="exact"])': function (node) {
410
+ exact: function (node) {
331
411
  if (![
332
412
  'ncactionrouter',
333
413
  'ncappnavigationitem',
@@ -346,7 +426,7 @@ export default {
346
426
  messageId: 'removeExact',
347
427
  });
348
428
  },
349
- 'VElement VAttribute:has(VIdentifier[name="checked"])': function (node) {
429
+ checked: function (node) {
350
430
  if (![
351
431
  'ncactioncheckbox',
352
432
  'ncactionradio',
@@ -379,7 +459,7 @@ export default {
379
459
  },
380
460
  });
381
461
  },
382
- 'VElement VAttribute:has(VIdentifier[name="value"])': function (node) {
462
+ value: function (node) {
383
463
  if (![
384
464
  'ncactioninput',
385
465
  'ncactiontexteditable',
@@ -424,6 +504,15 @@ export default {
424
504
  },
425
505
  });
426
506
  },
507
+ };
508
+ return defineTemplateBodyVisitor(context, {
509
+ VAttribute(node) {
510
+ const rawName = getRawName(node);
511
+ if (rawName === undefined) {
512
+ return;
513
+ }
514
+ handlers[toCamelCase(rawName)]?.(node);
515
+ },
427
516
  });
428
517
  },
429
518
  };
@@ -5,13 +5,12 @@
5
5
  * @param options Options
6
6
  * @param options.cwd The current working directory
7
7
  * @param options.physicalFilename The real filename where ESLint is linting currently
8
- * @param importResolve Optional custom import resolver function (for testing purposes)
9
8
  * @return Function validator, return a boolean whether current version satisfies minimal required for the rule
10
9
  */
11
10
  export declare function createLibVersionValidator({ cwd, physicalFilename }: {
12
- cwd: any;
13
- physicalFilename: any;
14
- }, importResolve?: (specifier: string, parent?: string | import("node:url").URL) => string): ((version: string) => boolean);
11
+ cwd: string;
12
+ physicalFilename: string;
13
+ }): ((version: string) => boolean);
15
14
  /**
16
15
  * Clear the module cache
17
16
  */
@@ -3,8 +3,9 @@
3
3
  * SPDX-License-Identifier: AGPL-3.0-or-later
4
4
  */
5
5
  import { readFileSync } from 'node:fs';
6
- import { isAbsolute, join, resolve } from 'node:path';
7
- import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import { findPackageJSON } from 'node:module';
7
+ import { isAbsolute, resolve } from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
8
9
  import { gte } from 'semver';
9
10
  /**
10
11
  * Cached map of paths: Reco <path_to_package.json, validator>
@@ -17,29 +18,29 @@ const cachedMap = new Map();
17
18
  * @param options Options
18
19
  * @param options.cwd The current working directory
19
20
  * @param options.physicalFilename The real filename where ESLint is linting currently
20
- * @param importResolve Optional custom import resolver function (for testing purposes)
21
21
  * @return Function validator, return a boolean whether current version satisfies minimal required for the rule
22
22
  */
23
- export function createLibVersionValidator({ cwd, physicalFilename }, importResolve = import.meta.resolve) {
23
+ export function createLibVersionValidator({ cwd, physicalFilename }) {
24
24
  // Try to find package.json of the nextcloud-vue package
25
25
  const sourceFile = isAbsolute(physicalFilename)
26
26
  ? resolve(physicalFilename)
27
27
  : resolve(cwd, physicalFilename);
28
- let packageJsonDir;
28
+ let packageJsonPath;
29
29
  try {
30
- const modulePath = fileURLToPath(importResolve('@nextcloud/vue', pathToFileURL(sourceFile)));
31
- const idx = modulePath.lastIndexOf('/dist/');
32
- packageJsonDir = modulePath.substring(0, idx);
30
+ packageJsonPath = findPackageJSON('@nextcloud/vue', pathToFileURL(sourceFile));
33
31
  }
34
32
  catch {
33
+ packageJsonPath = undefined;
34
+ }
35
+ if (!packageJsonPath) {
35
36
  return () => false;
36
37
  }
37
- if (!cachedMap[packageJsonDir]) {
38
- const json = JSON.parse(readFileSync(join(packageJsonDir, 'package.json'), 'utf-8'));
38
+ if (!cachedMap.has(packageJsonPath)) {
39
+ const json = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
39
40
  const libVersion = json.version;
40
- cachedMap[packageJsonDir] = (version) => gte(libVersion, version);
41
+ cachedMap.set(packageJsonPath, (version) => gte(libVersion, version));
41
42
  }
42
- return cachedMap[packageJsonDir];
43
+ return cachedMap.get(packageJsonPath);
43
44
  }
44
45
  /**
45
46
  * Clear the module cache
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcloud/eslint-config",
3
- "version": "9.0.0-rc.9",
3
+ "version": "9.0.1",
4
4
  "description": "Eslint shared config for nextcloud apps and libraries",
5
5
  "keywords": [
6
6
  "eslint",
@@ -44,31 +44,31 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@eslint/js": "^10.0.1",
47
- "@eslint/json": "^1.2.0",
47
+ "@eslint/json": "^2.0.0",
48
48
  "@stylistic/eslint-plugin": "^5.10.0",
49
49
  "eslint-config-flat-gitignore": "^2.3.0",
50
- "eslint-plugin-antfu": "^3.2.2",
51
- "eslint-plugin-jsdoc": "^62.9.0",
52
- "eslint-plugin-perfectionist": "^5.8.0",
53
- "eslint-plugin-vue": "^10.8.0",
54
- "fast-xml-parser": "^5.5.10",
55
- "globals": "^17.4.0",
56
- "semver": "^7.7.4",
57
- "sort-package-json": "^3.6.1",
58
- "typescript-eslint": "^8.58.0"
50
+ "eslint-plugin-antfu": "^3.2.3",
51
+ "eslint-plugin-jsdoc": "^63.0.10",
52
+ "eslint-plugin-perfectionist": "^5.9.1",
53
+ "eslint-plugin-vue": "^10.9.2",
54
+ "fast-xml-parser": "^5.9.3",
55
+ "globals": "^17.7.0",
56
+ "semver": "^7.8.5",
57
+ "sort-package-json": "^4.0.0",
58
+ "typescript-eslint": "^8.62.1"
59
59
  },
60
60
  "devDependencies": {
61
- "@types/node": "^25.5.2",
61
+ "@types/node": "^26.0.1",
62
62
  "@types/semver": "^7.7.1",
63
- "eslint": "^10.2.0",
64
- "memfs": "^4.57.1",
65
- "vitest": "^4.1.3"
63
+ "eslint": "^10.6.0",
64
+ "memfs": "^4.57.8",
65
+ "vitest": "^4.1.9"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "eslint": ">=10"
69
69
  },
70
70
  "engines": {
71
- "node": "^20.19 || ^22.13 || ^24"
71
+ "node": "^22.14 || ^24 || >=26"
72
72
  },
73
73
  "devEngines": {
74
74
  "packageManager": [