@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 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) (unreleased)
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.14.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.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
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
@@ -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}`,
@@ -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
  };
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.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": "^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.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": "^25.5.2",
61
+ "@types/node": "^26.0.0",
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.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": "^20.19 || ^22.13 || ^24"
71
+ "node": "^22.13 || ^24 || >=26"
72
72
  },
73
73
  "devEngines": {
74
74
  "packageManager": [