@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 +21 -11
- package/dist/configs/imports.js +3 -1
- package/dist/configs/vue.js +4 -0
- package/dist/plugins/import-extensions/index.d.ts +1 -0
- package/dist/plugins/import-extensions/rules/ban-inline-type-imports.d.ts +7 -0
- package/dist/plugins/import-extensions/rules/ban-inline-type-imports.js +176 -0
- package/dist/plugins/import-extensions/rules/index.d.ts +1 -0
- package/dist/plugins/import-extensions/rules/index.js +2 -0
- package/dist/plugins/nextcloud/rules/no-deprecated-library-exports.js +9 -1
- package/dist/plugins/nextcloud/rules/no-deprecated-library-props.js +117 -28
- package/dist/plugins/nextcloud/utils/lib-version-parser.d.ts +3 -4
- package/dist/plugins/nextcloud/utils/lib-version-parser.js +13 -12
- package/package.json +16 -16
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
-->
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
-
## [v9.0.
|
|
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.
|
|
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.
|
|
110
|
-
* Updated `eslint-plugin-jsdoc` to
|
|
111
|
-
* Updated `eslint-plugin-perfectionist` to 5.
|
|
112
|
-
* Updated `eslint-plugin-vue` to 10.
|
|
113
|
-
* Updated `fast-xml-parser` to 5.
|
|
114
|
-
* Updated `globals` to 17.
|
|
115
|
-
* Updated `semver` to 7.
|
|
116
|
-
* Updated `sort-package-json` to
|
|
117
|
-
* Updated `typescript-eslint` to 8.
|
|
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
|
package/dist/configs/imports.js
CHANGED
|
@@ -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:
|
|
101
|
+
newlinesBetween: 0,
|
|
100
102
|
partitionByNewLine: false,
|
|
101
103
|
groups: [
|
|
102
104
|
`type-${type}`,
|
package/dist/configs/vue.js
CHANGED
|
@@ -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
|
{
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
64
|
-
|
|
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.
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
13
|
-
physicalFilename:
|
|
14
|
-
}
|
|
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 {
|
|
7
|
-
import {
|
|
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 }
|
|
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
|
|
28
|
+
let packageJsonPath;
|
|
29
29
|
try {
|
|
30
|
-
|
|
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
|
|
38
|
-
const json = JSON.parse(readFileSync(
|
|
38
|
+
if (!cachedMap.has(packageJsonPath)) {
|
|
39
|
+
const json = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
39
40
|
const libVersion = json.version;
|
|
40
|
-
cachedMap
|
|
41
|
+
cachedMap.set(packageJsonPath, (version) => gte(libVersion, version));
|
|
41
42
|
}
|
|
42
|
-
return cachedMap
|
|
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.
|
|
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": "^
|
|
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.
|
|
51
|
-
"eslint-plugin-jsdoc": "^
|
|
52
|
-
"eslint-plugin-perfectionist": "^5.
|
|
53
|
-
"eslint-plugin-vue": "^10.
|
|
54
|
-
"fast-xml-parser": "^5.
|
|
55
|
-
"globals": "^17.
|
|
56
|
-
"semver": "^7.
|
|
57
|
-
"sort-package-json": "^
|
|
58
|
-
"typescript-eslint": "^8.
|
|
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": "^
|
|
61
|
+
"@types/node": "^26.0.1",
|
|
62
62
|
"@types/semver": "^7.7.1",
|
|
63
|
-
"eslint": "^10.
|
|
64
|
-
"memfs": "^4.57.
|
|
65
|
-
"vitest": "^4.1.
|
|
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": "^
|
|
71
|
+
"node": "^22.14 || ^24 || >=26"
|
|
72
72
|
},
|
|
73
73
|
"devEngines": {
|
|
74
74
|
"packageManager": [
|