@nextcloud/eslint-config 9.0.0-rc.9 → 9.0.0
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 +14 -11
- package/dist/configs/imports.js +3 -1
- 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/package.json +16 -16
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
-->
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
-
## [v9.0.0](https://github.com/nextcloud-libraries/eslint-config/tree/v9.0.0) (
|
|
7
|
+
## [v9.0.0](https://github.com/nextcloud-libraries/eslint-config/tree/v9.0.0) (2026-06-26)
|
|
8
8
|
|
|
9
9
|
### Breaking
|
|
10
10
|
This package now is using ESLint v10 and requires ESLint flat configurations.
|
|
@@ -44,6 +44,7 @@ either using the sledge hammer method (remove the lock file and run `npm i`) or
|
|
|
44
44
|
* feat(vue3): force camelCase for events in `<template>` [\#1263](https://github.com/nextcloud-libraries/eslint-config/pull/1263) \([ShGKme](https://github.com/ShGKme)\)
|
|
45
45
|
* feat(vue3): force camelCase for slot names [\#1264](https://github.com/nextcloud-libraries/eslint-config/pull/1264) \([ShGKme](https://github.com/ShGKme)\)
|
|
46
46
|
* feat(vue3): force camelCase for props in template [\#1266](https://github.com/nextcloud-libraries/eslint-config/pull/1266) \([ShGKme](https://github.com/ShGKme)\)
|
|
47
|
+
* 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
48
|
|
|
48
49
|
### Fixed
|
|
49
50
|
* fix(codestyle): do not require splitting chains [\#951](https://github.com/nextcloud-libraries/eslint-config/pull/951)
|
|
@@ -81,6 +82,7 @@ either using the sledge hammer method (remove the lock file and run `npm i`) or
|
|
|
81
82
|
* 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
83
|
* 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
84
|
* fix: use vue-eslint-parser directly [\#1277](https://github.com/nextcloud-libraries/eslint-config/pull/1277) \([susnux](https://github.com/susnux)\)
|
|
85
|
+
* 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
86
|
|
|
85
87
|
### Changed
|
|
86
88
|
* Add SPDX header [#802](https://github.com/nextcloud-libraries/eslint-config/pull/802)
|
|
@@ -89,6 +91,7 @@ either using the sledge hammer method (remove the lock file and run `npm i`) or
|
|
|
89
91
|
* refactor: use camelCase for internal variables [#971](https://github.com/nextcloud-libraries/eslint-config/pull/971) ([susnux](https://github.com/susnux))
|
|
90
92
|
* chore: do not bundle config but just use Typescript [#967](https://github.com/nextcloud-libraries/eslint-config/pull/967)
|
|
91
93
|
* chore: require node ^20.19 [\#982](https://github.com/nextcloud-libraries/eslint-config/pull/982)
|
|
94
|
+
* 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
95
|
* 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
96
|
* chore: prepare v9.0.0-rc.1 [#1010](https://github.com/nextcloud-libraries/eslint-config/pull/1010) ([susnux](https://github.com/susnux))
|
|
94
97
|
* 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 +106,18 @@ either using the sledge hammer method (remove the lock file and run `npm i`) or
|
|
|
103
106
|
* chore(gitignore): ignore IDE configs [\#1265](https://github.com/nextcloud-libraries/eslint-config/pull/1265) \([ShGKme](https://github.com/ShGKme)\)
|
|
104
107
|
* test(vue3): add some formatting tests [\#1267](https://github.com/nextcloud-libraries/eslint-config/pull/1267) \([ShGKme](https://github.com/ShGKme)\)
|
|
105
108
|
* 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.
|
|
109
|
+
* Updated `@eslint/json` to 2.0.0
|
|
107
110
|
* Updated `@stylistic/eslint-plugin` to 5.7.0
|
|
108
111
|
* 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.
|
|
112
|
+
* Updated `eslint-plugin-antfu` to 3.2.3
|
|
113
|
+
* Updated `eslint-plugin-jsdoc` to 63.0.4
|
|
114
|
+
* Updated `eslint-plugin-perfectionist` to 5.9.1
|
|
115
|
+
* Updated `eslint-plugin-vue` to 10.9.2
|
|
116
|
+
* Updated `fast-xml-parser` to 5.9.0
|
|
117
|
+
* Updated `globals` to 17.7.0
|
|
118
|
+
* Updated `semver` to 7.8.4
|
|
119
|
+
* Updated `sort-package-json` to 4.0.0
|
|
120
|
+
* Updated `typescript-eslint` to 8.62.0
|
|
118
121
|
|
|
119
122
|
## [v8.4.2](https://github.com/nextcloud-libraries/eslint-config/tree/v8.4.2) (2025-02-16)
|
|
120
123
|
### 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}`,
|
|
@@ -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
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextcloud/eslint-config",
|
|
3
|
-
"version": "9.0.0
|
|
3
|
+
"version": "9.0.0",
|
|
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.7",
|
|
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.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@types/node": "^
|
|
61
|
+
"@types/node": "^26.0.0",
|
|
62
62
|
"@types/semver": "^7.7.1",
|
|
63
|
-
"eslint": "^10.
|
|
64
|
-
"memfs": "^4.57.
|
|
65
|
-
"vitest": "^4.1.
|
|
63
|
+
"eslint": "^10.5.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.13 || ^24 || >=26"
|
|
72
72
|
},
|
|
73
73
|
"devEngines": {
|
|
74
74
|
"packageManager": [
|