@nextcloud/eslint-config 9.0.0-rc.4 → 9.0.0-rc.6

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
@@ -25,7 +25,10 @@ Please refer to the README on how to adjust your configuration for flat config.
25
25
  * feat(no-deprecated-props): extend existing rules to support other components [\#1069](https://github.com/nextcloud-libraries/eslint-config/pull/1069) \([Antreesy](https://github.com/Antreesy)\)
26
26
  * feat(imports): add custom plugin to suggest file extensions [\#1110](https://github.com/nextcloud-libraries/eslint-config/pull/1110) \([susnux](https://github.com/susnux)\)
27
27
  * feat(filesystem): ignore all files within the `.gitignore` [\#1108](https://github.com/nextcloud-libraries/eslint-config/pull/1108) \([susnux](https://github.com/susnux)\)
28
- * feat(l10n-plugin): also handle vue templates by @susnux in https://github.com/nextcloud-libraries/eslint-config/pull/1113
28
+ * feat(l10n-plugin): also handle vue templates [\#1113](https://github.com/nextcloud-libraries/eslint-config/pull/1113) \([susnux](https://github.com/susnux)\)
29
+ * feat(nextcloud-vue-plugin): deprecate additional props [\#1163](https://github.com/nextcloud-libraries/eslint-config/pull/1163) \([Antreesy](https://github.com/Antreesy)\)
30
+ * feat(nextcloud-vue-plugin): deprecate additional exports [\#1162](https://github.com/nextcloud-libraries/eslint-config/pull/1162) \([Antreesy](https://github.com/Antreesy)\)
31
+ * feat(nextcloud-vue-plugin): deprecate NcPopover props [\#1165](https://github.com/nextcloud-libraries/eslint-config/pull/1165) \([Antreesy](https://github.com/Antreesy)\)
29
32
 
30
33
  ### Fixed
31
34
  * fix(codestyle): do not require splitting chains [\#951](https://github.com/nextcloud-libraries/eslint-config/pull/951)
@@ -54,8 +57,12 @@ Please refer to the README on how to adjust your configuration for flat config.
54
57
  * fix(vue): also ignore the global router link component [\#1097](https://github.com/nextcloud-libraries/eslint-config/pull/1097) \([susnux](https://github.com/susnux)\)
55
58
  * fix(no-deprecated-props): respect nextcloud/vue library version for the rule [\#1084](https://github.com/nextcloud-libraries/eslint-config/pull/1084) \([Antreesy](https://github.com/Antreesy)\)
56
59
  * fix(codestyle): replace deprecated config in `@stylistic/quotes` rule [\#1109](https://github.com/nextcloud-libraries/eslint-config/pull/1109) \([susnux](https://github.com/susnux)\)
57
- * fix(l10n-plugin): also check translation strings in `n` method by @susnux in https://github.com/nextcloud-libraries/eslint-config/pull/1112
58
- * fix(filesystem): relax ignored files by @susnux in https://github.com/nextcloud-libraries/eslint-config/pull/1114
60
+ * fix(l10n-plugin): also check translation strings in `n` method [\#1112](https://github.com/nextcloud-libraries/eslint-config/pull/1112) \([susnux](https://github.com/susnux)\)
61
+ * fix(filesystem): relax ignored files [\#1114](https://github.com/nextcloud-libraries/eslint-config/pull/1114) \([susnux](https://github.com/susnux)\)
62
+ * fix(globs): adjust globs for test related files [\#1128](https://github.com/nextcloud-libraries/eslint-config/pull/1128) \([susnux](https://github.com/susnux)\)
63
+ * fix(vue): use vue variant of `no-irregular-whitespace` [\#1129](https://github.com/nextcloud-libraries/eslint-config/pull/1129) \([susnux](https://github.com/susnux)\)
64
+ * fix(nextcloud): add missing deprecations and removals [\#1206](https://github.com/nextcloud-libraries/eslint-config/pull/1206) \([susnux](https://github.com/susnux)\)
65
+ * fix(plugin:nextcloud-vue): use resolved dependency for detecting nextcloud-vue version [\#1220](https://github.com/nextcloud-libraries/eslint-config/pull/1220) \([susnux](https://github.com/susnux)\)
59
66
 
60
67
  ### Changed
61
68
  * Add SPDX header [#802](https://github.com/nextcloud-libraries/eslint-config/pull/802)
@@ -72,6 +79,9 @@ Please refer to the README on how to adjust your configuration for flat config.
72
79
  * ci: update workflows from organization [#1047](https://github.com/nextcloud-libraries/eslint-config/pull/1047) ([susnux](https://github.com/susnux))
73
80
  * chore: add active node version (24) to supported engines [#1066](https://github.com/nextcloud-libraries/eslint-config/pull/1066) ([susnux](https://github.com/susnux))
74
81
  * build: add common changelog formatting and documentation for it [#1067](https://github.com/nextcloud-libraries/eslint-config/pull/1067) ([susnux](https://github.com/susnux))
82
+ * chore: lint this project as a library [\#1130](https://github.com/nextcloud-libraries/eslint-config/pull/1130) \([susnux](https://github.com/susnux)\)
83
+ * chore: update devEngines to align with apps [\#1204](https://github.com/nextcloud-libraries/eslint-config/pull/1204) \([susnux](https://github.com/susnux)\)
84
+ * build: disable libcheck for Typescript [\#1205](https://github.com/nextcloud-libraries/eslint-config/pull/1205) \([susnux](https://github.com/susnux)\)
75
85
  * Updated `@eslint/json` to 0.12.0
76
86
  * Updated `@stylistic/eslint-plugin` 4.2.0
77
87
  * Updated `eslint-plugin-jsdoc` to 50.6.11
@@ -10,4 +10,4 @@ import type { ConfigOptions } from '../types.d.ts';
10
10
  *
11
11
  * @param options options defining the config preset flavor
12
12
  */
13
- export declare function codeStyle(options: ConfigOptions): (Linter.Config | Linter.BaseConfig)[];
13
+ export declare function codeStyle(options: ConfigOptions): Linter.Config[];
@@ -1,4 +1,4 @@
1
- import jsdocPlugin from 'eslint-plugin-jsdoc';
1
+ import { jsdoc } from 'eslint-plugin-jsdoc';
2
2
  import { GLOB_FILES_JAVASCRIPT, GLOB_FILES_TESTING, GLOB_FILES_TYPESCRIPT, GLOB_FILES_VUE, } from "../globs.js";
3
3
  const TS_FUNCTION_CONTEXTS = [
4
4
  'FunctionDeclaration:has(TSTypeAnnotation)',
@@ -12,6 +12,12 @@ const JS_FUNCTION_CONTEXTS = [
12
12
  'ArrowFunctionExpression:not(:has(TSTypeAnnotation))',
13
13
  'MethodDefinition:not(:has(TSTypeAnnotation))',
14
14
  ];
15
+ const SHARED_JSDOC_SETTINGS = {
16
+ // We use the alias for legacy reasons to prevent unnecessary noise
17
+ tagNamePreference: {
18
+ returns: 'return',
19
+ },
20
+ };
15
21
  /**
16
22
  * Config factory for code documentation related rules (JSDoc)
17
23
  *
@@ -20,30 +26,26 @@ const JS_FUNCTION_CONTEXTS = [
20
26
  export function documentation(options) {
21
27
  return [
22
28
  {
23
- files: [
24
- ...GLOB_FILES_JAVASCRIPT,
25
- ...GLOB_FILES_TYPESCRIPT,
26
- ...GLOB_FILES_VUE,
27
- ],
28
- plugins: {
29
- jsdoc: jsdocPlugin,
30
- },
31
- },
32
- {
33
- ...jsdocPlugin.configs['flat/recommended-typescript-flavor'],
29
+ ...jsdoc({
30
+ config: 'flat/recommended-typescript-flavor',
31
+ settings: {
32
+ ...SHARED_JSDOC_SETTINGS,
33
+ mode: 'permissive',
34
+ },
35
+ }),
36
+ name: 'nextcloud/documentation/javascript',
34
37
  files: [
35
38
  ...GLOB_FILES_JAVASCRIPT,
36
39
  ...(options.vueIsTypescript ? [] : GLOB_FILES_VUE),
37
40
  ],
38
- settings: {
39
- jsdoc: {
40
- mode: 'permissive',
41
- },
42
- },
43
41
  ignores: GLOB_FILES_TESTING,
44
42
  },
45
43
  {
46
- ...jsdocPlugin.configs['flat/recommended-typescript'],
44
+ ...jsdoc({
45
+ config: 'flat/recommended-typescript',
46
+ settings: SHARED_JSDOC_SETTINGS,
47
+ }),
48
+ name: 'nextcloud/documentation/typescript',
47
49
  files: [
48
50
  ...GLOB_FILES_TYPESCRIPT,
49
51
  ...(options.vueIsTypescript ? GLOB_FILES_VUE : []),
@@ -71,14 +73,6 @@ export function documentation(options) {
71
73
  { startLines: 1 },
72
74
  ],
73
75
  },
74
- settings: {
75
- jsdoc: {
76
- // We use the alias for legacy reasons to prevent unnecessary noise
77
- tagNamePreference: {
78
- returns: 'return',
79
- },
80
- },
81
- },
82
76
  },
83
77
  {
84
78
  name: 'nextcloud/documentation/rules-typescript',
@@ -99,6 +99,9 @@ export function vue(options) {
99
99
  'vue/no-unused-refs': 'warn',
100
100
  // Warn on unused props
101
101
  'vue/no-unused-properties': 'warn',
102
+ // This rule does not work in vue files, we must use the vue one
103
+ 'no-irregular-whitespace': 'off',
104
+ 'vue/no-irregular-whitespace': 'error',
102
105
  },
103
106
  },
104
107
  {
package/dist/globs.js CHANGED
@@ -7,8 +7,10 @@ export const GLOB_FILES_TESTING = [
7
7
  '**/*.test.*',
8
8
  '**/*.spec.*',
9
9
  '**/*.cy.*',
10
- '**/test',
11
- '**/tests',
10
+ '**/test/**',
11
+ '**/tests/**',
12
+ '**/__tests__/**',
13
+ '**/__mocks__/**',
12
14
  ];
13
15
  /** Glob pattern for Typescript files */
14
16
  export const GLOB_FILES_TYPESCRIPT = [
package/dist/index.d.ts CHANGED
@@ -1,27 +1,32 @@
1
+ /*!
2
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3
+ * SPDX-License-Identifier: AGPL-3.0-or-later
4
+ */
5
+ import type { Linter } from 'eslint';
1
6
  /**
2
7
  * Nextcloud shared configuration for projects using Vue 2 with Javascript <script> blocks
3
8
  */
4
- export declare const recommendedVue2Javascript: (import("eslint").Linter.Config<import("eslint").Linter.RulesRecord> | import("eslint").Linter.BaseConfig<import("eslint").Linter.RulesRecord, import("eslint").Linter.RulesRecord>)[];
9
+ export declare const recommendedVue2Javascript: Linter.Config[];
5
10
  /**
6
11
  * Nextcloud shared configuration for projects using Vue 2 with Typescript <script> blocks
7
12
  */
8
- export declare const recommendedVue2: (import("eslint").Linter.Config<import("eslint").Linter.RulesRecord> | import("eslint").Linter.BaseConfig<import("eslint").Linter.RulesRecord, import("eslint").Linter.RulesRecord>)[];
13
+ export declare const recommendedVue2: Linter.Config[];
9
14
  /**
10
15
  * Nextcloud shared configuration for projects using Vue 3 with Javascript <script> blocks
11
16
  */
12
- export declare const recommendedJavascript: (import("eslint").Linter.Config<import("eslint").Linter.RulesRecord> | import("eslint").Linter.BaseConfig<import("eslint").Linter.RulesRecord, import("eslint").Linter.RulesRecord>)[];
17
+ export declare const recommendedJavascript: Linter.Config[];
13
18
  /**
14
19
  * Nextcloud shared configuration for projects using Vue 3 with Typescript <script> blocks
15
20
  */
16
- export declare const recommended: (import("eslint").Linter.Config<import("eslint").Linter.RulesRecord> | import("eslint").Linter.BaseConfig<import("eslint").Linter.RulesRecord, import("eslint").Linter.RulesRecord>)[];
21
+ export declare const recommended: Linter.Config[];
17
22
  /**
18
23
  * Nextcloud shared configuration for projects using Vue 3 with Typescript <script> blocks
19
24
  */
20
- export declare const recommendedLibrary: (import("eslint").Linter.Config<import("eslint").Linter.RulesRecord> | import("eslint").Linter.BaseConfig<import("eslint").Linter.RulesRecord, import("eslint").Linter.RulesRecord>)[];
25
+ export declare const recommendedLibrary: Linter.Config[];
21
26
  /**
22
27
  * Nextcloud shared configuration for projects using Vue 3 with Typescript <script> blocks
23
28
  */
24
- export declare const recommendedVue2Library: (import("eslint").Linter.Config<import("eslint").Linter.RulesRecord> | import("eslint").Linter.BaseConfig<import("eslint").Linter.RulesRecord, import("eslint").Linter.RulesRecord>)[];
29
+ export declare const recommendedVue2Library: Linter.Config[];
25
30
  export { default as packageJsonPlugin } from './plugins/packageJson.ts';
26
31
  export { default as nextcloudPlugin } from './plugins/nextcloud/index.ts';
27
32
  export { default as l10nPlugin } from './plugins/l10n/index.ts';
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ /*!
2
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3
+ * SPDX-License-Identifier: AGPL-3.0-or-later
4
+ */
1
5
  import { codeStyle } from "./configs/codeStyle.js";
2
6
  import { documentation } from "./configs/documentation.js";
3
7
  import { filesystem } from "./configs/filesystem.js";
@@ -31,15 +31,18 @@ const global = {
31
31
  relative_modified_date: '16.0.0',
32
32
  };
33
33
  const oc = {
34
+ AppConfig: '16.0.0',
34
35
  L10n: '26.0.0',
36
+ SystemTags: '32.0.0',
35
37
  _capabilities: '17.0.0',
36
38
  addTranslations: '17.0.0',
37
39
  basename: '18.0.0',
38
40
  coreApps: '17.0.0',
39
41
  currentUser: '19.0.0',
42
+ dialogs: '30.0.0',
40
43
  dirname: '18.0.0',
41
44
  encodePath: '18.0.0',
42
- fileIsBlacklisted: '17.0.0',
45
+ fileIsBlacklisted: '18.0.0',
43
46
  filePath: '19.0.0',
44
47
  generateUrl: '19.0.0',
45
48
  get: '19.0.0',
@@ -71,6 +74,13 @@ const ocNested = {
71
74
  humanFileSize: '20.0.0',
72
75
  relativeModifiedDate: '20.0.0',
73
76
  },
77
+ dialogs: {
78
+ fileexists: '29.0.0',
79
+ },
80
+ config: {
81
+ blacklist_files_regex: '30.0.0',
82
+ forbidden_filename_characters: '30.0.0',
83
+ },
74
84
  };
75
85
  const rule = {
76
86
  meta: {
@@ -11,14 +11,26 @@ const global = {
11
11
  getURLParameter: '19.0.0',
12
12
  humanFileSize: '19.0.0',
13
13
  marked: '19.0.0',
14
+ md5: '32.0.0',
14
15
  relative_modified_date: '19.0.0',
15
16
  };
16
17
  const oc = {
18
+ AppConfig: '33.0.0',
19
+ SystemTags: '33.0.0',
17
20
  getScrollBarWidth: '15.0.0',
18
21
  addTranslations: '26.0.0',
19
22
  appSettings: '28.0.0',
23
+ fileIsBlacklisted: '33.0.0',
24
+ get: '33.0.0',
25
+ getHost: '33.0.0',
26
+ getHostName: '33.0.0',
27
+ getPort: '33.0.0',
28
+ getProtocol: '33.0.0',
20
29
  loadScript: '28.0.0',
21
30
  loadStyle: '28.0.0',
31
+ redirect: '33.0.0',
32
+ reload: '33.0.0',
33
+ set: '33.0.0',
22
34
  };
23
35
  const ocNested = {
24
36
  AppConfig: {
@@ -36,6 +48,12 @@ const ocNested = {
36
48
  const oca = {
37
49
  // ref: https://github.com/nextcloud/server/commit/6eced42b7a40f5b0ea0489244583219d0ee2e7af
38
50
  Search: '20.0.0',
51
+ FilesSharingDrop: '31.0.0',
52
+ };
53
+ const ocaNested = {
54
+ Sharing: {
55
+ ExternalLinkActions: '33.0.0',
56
+ },
39
57
  };
40
58
  // TODO: handle OC.x.y.z like OC.Share.ShareConfigModel.areAvatarsEnabled()
41
59
  // ref https://github.com/nextcloud/server/issues/11045
@@ -83,6 +101,27 @@ const rule = {
83
101
  message: `The property or function OCA.${node.property.name} was removed in Nextcloud ${oca[node.property.name]}`,
84
102
  });
85
103
  }
104
+ // OCA.x.y
105
+ if (node.object.type === 'MemberExpression'
106
+ && 'name' in node.object.object
107
+ && node.object.object.name === 'OCA'
108
+ && 'name' in node.object.property
109
+ && ocaNested[node.object.property.name]
110
+ && 'name' in node.property
111
+ && ocaNested[node.object.property.name][node.property.name]) {
112
+ const version = ocaNested[node.object.property.name][node.property.name];
113
+ if (checkTargetVersion(version)) {
114
+ const prop = [
115
+ 'OC',
116
+ node.object.property.name,
117
+ node.property.name,
118
+ ].join('.');
119
+ context.report({
120
+ node,
121
+ message: `The property or function ${prop} was removed in Nextcloud ${version}`,
122
+ });
123
+ }
124
+ }
86
125
  // OC.x
87
126
  if ('name' in node.object
88
127
  && 'name' in node.property
@@ -21,13 +21,21 @@ declare const _default: {
21
21
  useVerboseStatusInstead: string;
22
22
  useNoPlaceholderInstead: string;
23
23
  useFormatInstead: string;
24
+ useLocaleInstead: string;
24
25
  useTypeDateRangeInstead: string;
25
26
  useNoCloseInstead: string;
27
+ useNoCloseOnClickOutsideInstead: string;
26
28
  useDisableSwipeForModalInstead: string;
27
29
  useNoFocusTrapInstead: string;
28
30
  useKeepOpenInstead: string;
29
31
  useNcSelectUsersInstead: string;
32
+ useArrowEndInstead: string;
30
33
  removeAriaHidden: string;
34
+ removeLimitWidth: string;
35
+ removeExact: string;
36
+ useCloseButtonOutsideInstead: string;
37
+ useModelValueInsteadChecked: string;
38
+ useModelValueInsteadValue: string;
31
39
  };
32
40
  };
33
41
  create(context: import("eslint").Rule.RuleContext): any;
@@ -16,13 +16,21 @@ export declare const rules: {
16
16
  useVerboseStatusInstead: string;
17
17
  useNoPlaceholderInstead: string;
18
18
  useFormatInstead: string;
19
+ useLocaleInstead: string;
19
20
  useTypeDateRangeInstead: string;
20
21
  useNoCloseInstead: string;
22
+ useNoCloseOnClickOutsideInstead: string;
21
23
  useDisableSwipeForModalInstead: string;
22
24
  useNoFocusTrapInstead: string;
23
25
  useKeepOpenInstead: string;
24
26
  useNcSelectUsersInstead: string;
27
+ useArrowEndInstead: string;
25
28
  removeAriaHidden: string;
29
+ removeLimitWidth: string;
30
+ removeExact: string;
31
+ useCloseButtonOutsideInstead: string;
32
+ useModelValueInsteadChecked: string;
33
+ useModelValueInsteadValue: string;
26
34
  };
27
35
  };
28
36
  create(context: import("eslint").Rule.RuleContext): any;
@@ -16,18 +16,59 @@ const rule = {
16
16
  messages: {
17
17
  outdatedVueLibrary: 'Installed @nextcloud/vue library is outdated and does not support all reported errors. Install latest compatible version',
18
18
  deprecatedDist: 'Import from "@nextcloud/vue/dist" is deprecated',
19
+ deprecatedMixin: 'Mixins are no longer recommended by Vue. Consider using available alternatives',
20
+ deprecatedNcSettingsInputText: 'NcSettingsInputText is deprecated. Consider using available alternatives',
21
+ deprecatedTooltip: 'Tooltip directive is deprecated. use native title attribute or NcPopover instead',
19
22
  },
20
23
  },
21
24
  create(context) {
22
25
  const versionSatisfies = createLibVersionValidator(context);
23
- const isVersionValid = versionSatisfies('8.23.0');
26
+ const isVersionValidForDist = versionSatisfies('8.23.0');
24
27
  const oldPattern = '@nextcloud/vue/dist/([^/]+)/([^/.]+)';
28
+ const mixinPattern = '@nextcloud/vue/mixins/([^/.]+)';
29
+ const isVersionValidForTooltip = versionSatisfies('8.25.0');
30
+ const tooltipPattern = '@nextcloud/vue/directives/Tooltip';
31
+ const isVersionValidForNcSettingsInputText = versionSatisfies('8.31.0');
32
+ const patternForNcSettingsInputText = '@nextcloud/vue/components/NcSettingsInputText';
25
33
  return {
26
34
  ImportDeclaration: function (node) {
27
35
  const importPath = node.source.value;
36
+ const matchForNcSettingsInputText = importPath.match(new RegExp(patternForNcSettingsInputText));
37
+ if (matchForNcSettingsInputText) {
38
+ if (!isVersionValidForNcSettingsInputText) {
39
+ context.report({ node, messageId: 'outdatedVueLibrary' });
40
+ return;
41
+ }
42
+ context.report({
43
+ node,
44
+ messageId: 'deprecatedNcSettingsInputText',
45
+ });
46
+ }
47
+ const mixinMatch = importPath.match(new RegExp(mixinPattern, 'i'));
48
+ if (mixinMatch) {
49
+ if (!isVersionValidForDist) {
50
+ context.report({ node, messageId: 'outdatedVueLibrary' });
51
+ return;
52
+ }
53
+ context.report({
54
+ node,
55
+ messageId: 'deprecatedMixin',
56
+ });
57
+ }
58
+ const tooltipMatch = importPath.match(new RegExp(tooltipPattern));
59
+ if (tooltipMatch) {
60
+ if (!isVersionValidForTooltip) {
61
+ context.report({ node, messageId: 'outdatedVueLibrary' });
62
+ return;
63
+ }
64
+ context.report({
65
+ node,
66
+ messageId: 'deprecatedTooltip',
67
+ });
68
+ }
28
69
  const match = importPath.match(new RegExp(oldPattern));
29
70
  if (match) {
30
- if (!isVersionValid) {
71
+ if (!isVersionValidForDist) {
31
72
  context.report({ node, messageId: 'outdatedVueLibrary' });
32
73
  return;
33
74
  }
@@ -19,13 +19,21 @@ declare const _default: {
19
19
  useVerboseStatusInstead: string;
20
20
  useNoPlaceholderInstead: string;
21
21
  useFormatInstead: string;
22
+ useLocaleInstead: string;
22
23
  useTypeDateRangeInstead: string;
23
24
  useNoCloseInstead: string;
25
+ useNoCloseOnClickOutsideInstead: string;
24
26
  useDisableSwipeForModalInstead: string;
25
27
  useNoFocusTrapInstead: string;
26
28
  useKeepOpenInstead: string;
27
29
  useNcSelectUsersInstead: string;
30
+ useArrowEndInstead: string;
28
31
  removeAriaHidden: string;
32
+ removeLimitWidth: string;
33
+ removeExact: string;
34
+ useCloseButtonOutsideInstead: string;
35
+ useModelValueInsteadChecked: string;
36
+ useModelValueInsteadValue: string;
29
37
  };
30
38
  };
31
39
  create(context: Rule.RuleContext): any;
@@ -16,18 +16,28 @@ export default {
16
16
  useVerboseStatusInstead: 'Using `show-user-status-compact` is deprecated - use `verbose-status` instead',
17
17
  useNoPlaceholderInstead: 'Using `allow-placeholder` is deprecated - use `no-placeholder` instead',
18
18
  useFormatInstead: 'Using `formatter` is deprecated - use `format` instead',
19
+ useLocaleInstead: 'Using `lang` is deprecated - use `locale` instead',
19
20
  useTypeDateRangeInstead: 'Using `range` is deprecated - use `type` with `date-range` or `datetime-range` instead',
20
21
  useNoCloseInstead: 'Using `can-close` is deprecated - use `no-close` instead',
22
+ useNoCloseOnClickOutsideInstead: 'Using `close-on-click-outside` is deprecated - use `no-close-on-click-outside` instead',
21
23
  useDisableSwipeForModalInstead: 'Using `enable-swipe` is deprecated - use `disable-swipe` instead',
22
24
  useNoFocusTrapInstead: 'Using `focus-trap` is deprecated - use `no-focus-trap` instead',
23
25
  useKeepOpenInstead: 'Using `close-on-select` is deprecated - use `keep-open` instead',
24
26
  useNcSelectUsersInstead: 'Using `user-select` is deprecated - use `NcSelectUsers` component instead',
27
+ useArrowEndInstead: 'Using `arrow-right` is deprecated - use `arrow-end` instead',
25
28
  removeAriaHidden: 'Using `aria-hidden` is deprecated - remove prop from components, otherwise root element will inherit incorrect attribute.',
29
+ removeLimitWidth: 'Using `limit-width` is deprecated - remove prop from components, otherwise root element will inherit incorrect attribute.',
30
+ removeExact: 'Using `exact` is deprecated - consult Vue Router documentation for alternatives.',
31
+ useCloseButtonOutsideInstead: 'Using `close-button-contained` is deprecated - use `close-button-outside` instead',
32
+ useModelValueInsteadChecked: 'Using `checked` is deprecated - use `model-value` or `v-model` instead',
33
+ useModelValueInsteadValue: 'Using `value` is deprecated - use `model-value` or `v-model` instead',
26
34
  },
27
35
  },
28
36
  create(context) {
29
37
  const versionSatisfies = createLibVersionValidator(context);
38
+ const isVue3Valid = versionSatisfies('9.0.0'); // #6651
30
39
  const isAriaHiddenValid = versionSatisfies('8.2.0'); // #4835
40
+ const isModelValueValid = versionSatisfies('8.20.0'); // #6172
31
41
  const isDisableSwipeValid = versionSatisfies('8.23.0'); // #6452
32
42
  const isVariantTypeValid = versionSatisfies('8.24.0'); // #6472
33
43
  const isDefaultBooleanFalseValid = versionSatisfies('8.24.0'); // #6656
@@ -35,6 +45,8 @@ export default {
35
45
  const isNcSelectKeepOpenValid = versionSatisfies('8.25.0'); // #6791
36
46
  const isNcPopoverNoFocusTrapValid = versionSatisfies('8.26.0'); // #6808
37
47
  const isNcSelectUsersValid = versionSatisfies('8.27.1'); // #7032
48
+ const isNcTextFieldArrowEndValid = versionSatisfies('8.28.0'); // #7002
49
+ const isCloseButtonOutsideValid = versionSatisfies('8.32.0'); // #7553
38
50
  const legacyTypes = ['primary', 'error', 'warning', 'success', 'secondary', 'tertiary', 'tertiary-no-background'];
39
51
  return vueUtils.defineTemplateBodyVisitor(context, {
40
52
  'VElement VAttribute:has(VIdentifier[name="type"])': function (node) {
@@ -165,6 +177,16 @@ export default {
165
177
  messageId: 'useFormatInstead',
166
178
  });
167
179
  },
180
+ 'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="lang"])': function (node) {
181
+ if (!isVue3Valid) {
182
+ // Do not throw for v8.X.X
183
+ return;
184
+ }
185
+ context.report({
186
+ node,
187
+ messageId: 'useLocaleInstead',
188
+ });
189
+ },
168
190
  'VElement[name="ncdatetimepicker"] VAttribute:has(VIdentifier[name="range"])': function (node) {
169
191
  if (!isDateTimePickerFormatValid) {
170
192
  context.report({ node, messageId: 'outdatedVueLibrary' });
@@ -191,6 +213,16 @@ export default {
191
213
  messageId: 'useNoCloseInstead',
192
214
  });
193
215
  },
216
+ 'VElement[name="ncpopover"] VAttribute:has(VIdentifier[name="close-on-click-outside"])': function (node) {
217
+ if (!isVue3Valid) {
218
+ // Do not throw for v8.X.X
219
+ return;
220
+ }
221
+ context.report({
222
+ node,
223
+ messageId: 'useNoCloseOnClickOutsideInstead',
224
+ });
225
+ },
194
226
  'VElement[name="ncmodal"] VAttribute:has(VIdentifier[name="enable-swipe"])': function (node) {
195
227
  if (!isDisableSwipeValid) {
196
228
  context.report({ node, messageId: 'outdatedVueLibrary' });
@@ -201,6 +233,16 @@ export default {
201
233
  messageId: 'useDisableSwipeForModalInstead',
202
234
  });
203
235
  },
236
+ 'VElement[name="ncmodal"] VAttribute:has(VIdentifier[name="close-button-contained"])': function (node) {
237
+ if (!isCloseButtonOutsideValid) {
238
+ context.report({ node, messageId: 'outdatedVueLibrary' });
239
+ return;
240
+ }
241
+ context.report({
242
+ node,
243
+ messageId: 'useCloseButtonOutsideInstead',
244
+ });
245
+ },
204
246
  'VElement[name="ncpopover"] VAttribute:has(VIdentifier[name="focus-trap"])': function (node) {
205
247
  if (!isNcPopoverNoFocusTrapValid) {
206
248
  context.report({ node, messageId: 'outdatedVueLibrary' });
@@ -231,6 +273,142 @@ export default {
231
273
  messageId: 'useNcSelectUsersInstead',
232
274
  });
233
275
  },
276
+ 'VElement VAttribute:has(VIdentifier[name="trailing-button-icon"])': function (node) {
277
+ if (node.parent.parent.name !== 'nctextfield') {
278
+ return;
279
+ }
280
+ if (!isNcTextFieldArrowEndValid) {
281
+ context.report({ node, messageId: 'outdatedVueLibrary' });
282
+ return;
283
+ }
284
+ const isLiteral = node.value.type === 'VLiteral' && node.value.value === 'arrowRight';
285
+ const isExpression = node.value.type === 'VExpressionContainer' && node.value.expression?.type === 'ConditionalExpression'
286
+ && (node.value.expression.consequent.value === 'arrowRight' || node.value.expression.alternate.value === 'arrowRight');
287
+ /**
288
+ * if it is a literal with a deprecated value -> we migrate
289
+ * if it is an expression with a defined deprecated value -> we migrate
290
+ */
291
+ if (isLiteral || isExpression) {
292
+ context.report({
293
+ node,
294
+ messageId: 'useArrowEndInstead',
295
+ fix: (fixer) => {
296
+ if (node.key.type === 'VIdentifier') {
297
+ return fixer.replaceTextRange(node.value.range, '"arrowEnd"');
298
+ }
299
+ else if (node.key.type === 'VDirectiveKey') {
300
+ return (node.value.expression.consequent.value === 'arrowRight')
301
+ ? fixer.replaceTextRange(node.value.expression.consequent.range, '\'arrowEnd\'')
302
+ : fixer.replaceTextRange(node.value.expression.alternate.range, '\'arrowEnd\'');
303
+ }
304
+ },
305
+ });
306
+ }
307
+ },
308
+ 'VElement[name="ncsettingssection"] VAttribute:has(VIdentifier[name="limit-width"])': function (node) {
309
+ // This was deprecated in 8.13.0 (Nextcloud 30+), before first supported version by plugin
310
+ context.report({
311
+ node,
312
+ messageId: 'removeLimitWidth',
313
+ });
314
+ },
315
+ 'VElement VAttribute:has(VIdentifier[name="exact"])': function (node) {
316
+ if (![
317
+ 'ncactionrouter',
318
+ 'ncappnavigationitem',
319
+ 'ncbreadcrumb',
320
+ 'ncbutton',
321
+ 'nclistitem',
322
+ ].includes(node.parent.parent.name)) {
323
+ return;
324
+ }
325
+ if (!isVue3Valid) {
326
+ // Do not throw for v8.X.X
327
+ return;
328
+ }
329
+ context.report({
330
+ node,
331
+ messageId: 'removeExact',
332
+ });
333
+ },
334
+ 'VElement VAttribute:has(VIdentifier[name="checked"])': function (node) {
335
+ if (![
336
+ 'ncactioncheckbox',
337
+ 'ncactionradio',
338
+ 'nccheckboxradioswitch',
339
+ ].includes(node.parent.parent.name)) {
340
+ return;
341
+ }
342
+ if (!isModelValueValid) {
343
+ context.report({ node, messageId: 'outdatedVueLibrary' });
344
+ return;
345
+ }
346
+ context.report({
347
+ node,
348
+ messageId: 'useModelValueInsteadChecked',
349
+ fix: (fixer) => {
350
+ if (node.key.type === 'VIdentifier') {
351
+ return fixer.replaceTextRange(node.key.range, 'model-value');
352
+ }
353
+ else if (node.key.type === 'VDirectiveKey') {
354
+ if (node.key.name.name === 'model') {
355
+ return fixer.replaceTextRange(node.key.range, 'v-model');
356
+ }
357
+ else if (node.key.modifiers.some((m) => m.name === 'sync')) {
358
+ return fixer.replaceTextRange(node.key.range, 'v-model');
359
+ }
360
+ else {
361
+ return fixer.replaceTextRange(node.key.argument.range, 'model-value');
362
+ }
363
+ }
364
+ },
365
+ });
366
+ },
367
+ 'VElement VAttribute:has(VIdentifier[name="value"])': function (node) {
368
+ if (![
369
+ 'ncactioninput',
370
+ 'ncactiontexteditable',
371
+ 'nccolorpicker',
372
+ 'ncdatetimepicker',
373
+ 'ncdatetimepickernative',
374
+ 'ncinputfield',
375
+ 'nctextfield',
376
+ 'ncpasswordfield',
377
+ 'ncrichcontenteditable',
378
+ 'ncselecttags',
379
+ 'ncselect',
380
+ 'ncsettingsinputtext',
381
+ 'ncsettingsselectgroup',
382
+ 'nctextarea',
383
+ 'nctimezonepicker',
384
+ ].includes(node.parent.parent.name)) {
385
+ return;
386
+ }
387
+ if (!isModelValueValid) {
388
+ context.report({ node, messageId: 'outdatedVueLibrary' });
389
+ return;
390
+ }
391
+ context.report({
392
+ node,
393
+ messageId: 'useModelValueInsteadValue',
394
+ fix: (fixer) => {
395
+ if (node.key.type === 'VIdentifier') {
396
+ return fixer.replaceTextRange(node.key.range, 'model-value');
397
+ }
398
+ else if (node.key.type === 'VDirectiveKey') {
399
+ if (node.key.name.name === 'model') {
400
+ return fixer.replaceTextRange(node.key.range, 'v-model');
401
+ }
402
+ else if (node.key.modifiers.some((m) => m.name === 'sync')) {
403
+ return fixer.replaceTextRange(node.key.range, 'v-model');
404
+ }
405
+ else {
406
+ return fixer.replaceTextRange(node.key.argument.range, 'model-value');
407
+ }
408
+ }
409
+ },
410
+ });
411
+ },
234
412
  });
235
413
  },
236
414
  };
@@ -1,23 +1,3 @@
1
- /**
2
- * Check if a given path exists and is a file
3
- *
4
- * @param filePath The path
5
- */
6
- export declare function isFile(filePath: string): boolean;
7
- /**
8
- * Find the path of nearest `package.json` relative to given path
9
- *
10
- * @param currentPath Path to lookup
11
- * @return Either the full path where `package.json` is located or `undefined` if no found
12
- */
13
- export declare function findPackageJsonDir(currentPath: string): string | undefined;
14
- /**
15
- * Make sure that versions like '25' can be handled by semver
16
- *
17
- * @param version The pure version string
18
- * @return Sanitized version string
19
- */
20
- export declare function sanitizeTargetVersion(version: string): string;
21
1
  /**
22
2
  * Create a callback that takes a version number and checks if the version
23
3
  * is valid compared to configured version / detected version.
@@ -25,9 +5,14 @@ export declare function sanitizeTargetVersion(version: string): string;
25
5
  * @param options Options
26
6
  * @param options.cwd The current working directory
27
7
  * @param options.physicalFilename The real filename where ESLint is linting currently
8
+ * @param importResolve Optional custom import resolver function (for testing purposes)
28
9
  * @return Function validator, return a boolean whether current version satisfies minimal required for the rule
29
10
  */
30
11
  export declare function createLibVersionValidator({ cwd, physicalFilename }: {
31
12
  cwd: any;
32
13
  physicalFilename: any;
33
- }): ((version: string) => boolean);
14
+ }, importResolve?: (specifier: string, parent?: string | import("url").URL) => string): ((version: string) => boolean);
15
+ /**
16
+ * Clear the module cache
17
+ */
18
+ export declare function clearCache(): void;
@@ -2,56 +2,14 @@
2
2
  * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3
3
  * SPDX-License-Identifier: AGPL-3.0-or-later
4
4
  */
5
- import { lstatSync, readFileSync } from 'node:fs';
6
- import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
7
- import { gte, minVersion, valid } from 'semver';
5
+ import { readFileSync } from 'node:fs';
6
+ import { isAbsolute, join, resolve } from 'node:path';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+ import { gte } from 'semver';
8
9
  /**
9
10
  * Cached map of paths: Reco <path_to_package.json, validator>
10
11
  */
11
- const cachedMap = {};
12
- /**
13
- * Check if a given path exists and is a file
14
- *
15
- * @param filePath The path
16
- */
17
- export function isFile(filePath) {
18
- const stats = lstatSync(filePath, { throwIfNoEntry: false });
19
- return stats !== undefined && stats.isFile();
20
- }
21
- /**
22
- * Find the path of nearest `package.json` relative to given path
23
- *
24
- * @param currentPath Path to lookup
25
- * @return Either the full path where `package.json` is located or `undefined` if no found
26
- */
27
- export function findPackageJsonDir(currentPath) {
28
- for (const cachedDirPath of Object.keys(cachedMap)) {
29
- if (currentPath.startsWith(cachedDirPath)) {
30
- return cachedDirPath;
31
- }
32
- }
33
- while (currentPath && currentPath !== sep) {
34
- if (isFile(join(currentPath, 'package.json'))) {
35
- return currentPath;
36
- }
37
- currentPath = resolve(currentPath, '..');
38
- }
39
- return undefined;
40
- }
41
- /**
42
- * Make sure that versions like '25' can be handled by semver
43
- *
44
- * @param version The pure version string
45
- * @return Sanitized version string
46
- */
47
- export function sanitizeTargetVersion(version) {
48
- const sanitizedVersion = minVersion(version)?.version;
49
- // now version should look like '23.0.0'
50
- if (!valid(sanitizedVersion)) {
51
- throw Error(`[@nextcloud/eslint-plugin] Invalid target version ${version} found`);
52
- }
53
- return sanitizedVersion;
54
- }
12
+ const cachedMap = new Map();
55
13
  /**
56
14
  * Create a callback that takes a version number and checks if the version
57
15
  * is valid compared to configured version / detected version.
@@ -59,36 +17,33 @@ export function sanitizeTargetVersion(version) {
59
17
  * @param options Options
60
18
  * @param options.cwd The current working directory
61
19
  * @param options.physicalFilename The real filename where ESLint is linting currently
20
+ * @param importResolve Optional custom import resolver function (for testing purposes)
62
21
  * @return Function validator, return a boolean whether current version satisfies minimal required for the rule
63
22
  */
64
- export function createLibVersionValidator({ cwd, physicalFilename }) {
65
- // Try to find package.json and parse the supported version
66
- // Current working directory, either the filename (can be empty) or the cwd property
67
- const currentDirectory = isAbsolute(physicalFilename)
68
- ? resolve(dirname(physicalFilename))
69
- : dirname(resolve(cwd, physicalFilename));
70
- // The nearest package.json
71
- const packageJsonDir = findPackageJsonDir(currentDirectory);
72
- if (!packageJsonDir) {
73
- // Skip the rule
74
- return () => false;
23
+ export function createLibVersionValidator({ cwd, physicalFilename }, importResolve = import.meta.resolve) {
24
+ // Try to find package.json of the nextcloud-vue package
25
+ const sourceFile = isAbsolute(physicalFilename)
26
+ ? resolve(physicalFilename)
27
+ : resolve(cwd, physicalFilename);
28
+ let packageJsonDir;
29
+ try {
30
+ const modulePath = fileURLToPath(importResolve('@nextcloud/vue', pathToFileURL(sourceFile)));
31
+ const idx = modulePath.lastIndexOf('/dist/');
32
+ packageJsonDir = modulePath.substring(0, idx);
75
33
  }
76
- else if (cachedMap[packageJsonDir]) {
77
- return cachedMap[packageJsonDir];
78
- }
79
- const json = JSON.parse(readFileSync(join(packageJsonDir, 'package.json'), 'utf-8'));
80
- const libVersions = [
81
- json?.dependencies?.['@nextcloud/vue'],
82
- json?.devDependencies?.['@nextcloud/vue'],
83
- json?.peerDependencies?.['@nextcloud/vue'],
84
- ]
85
- .filter((version) => typeof version === 'string' && !!version)
86
- .map(sanitizeTargetVersion)
87
- .sort((a, b) => gte(a, b) ? 1 : -1);
88
- if (!libVersions.length) {
89
- // Skip the rule
34
+ catch {
90
35
  return () => false;
91
36
  }
92
- // Return, whether given version satisfies minimal version from dependencies
93
- return (version) => gte(libVersions[0], version);
37
+ if (!cachedMap[packageJsonDir]) {
38
+ const json = JSON.parse(readFileSync(join(packageJsonDir, 'package.json'), 'utf-8'));
39
+ const libVersion = json.version;
40
+ cachedMap[packageJsonDir] = (version) => gte(libVersion, version);
41
+ }
42
+ return cachedMap[packageJsonDir];
43
+ }
44
+ /**
45
+ * Clear the module cache
46
+ */
47
+ export function clearCache() {
48
+ cachedMap.clear();
94
49
  }
@@ -43,6 +43,7 @@ const SortPackageJsonRule = {
43
43
  node: body,
44
44
  message: 'package.json is not sorted correctly',
45
45
  fix(fixer) {
46
+ // @ts-expect-error its always an object node
46
47
  return fixer.replaceText(body, sortedPackageJsonText);
47
48
  },
48
49
  });
package/dist/utils.d.ts CHANGED
@@ -9,16 +9,4 @@ import type { Linter } from 'eslint';
9
9
  * @param configs The configs to restrict
10
10
  * @param files The glob pattern to assign
11
11
  */
12
- export declare function restrictConfigFiles(configs: Linter.Config[], files: string[]): {
13
- files: (string | string[])[];
14
- name?: string;
15
- basePath?: string;
16
- ignores?: string[];
17
- language?: string;
18
- languageOptions?: Linter.LanguageOptions;
19
- linterOptions?: Linter.LinterOptions;
20
- processor?: string | Linter.Processor;
21
- plugins?: Record<string, import("eslint").ESLint.Plugin>;
22
- rules?: Partial<Linter.RulesRecord>;
23
- settings?: Record<string, unknown>;
24
- }[];
12
+ export declare function restrictConfigFiles(configs: Linter.Config[], files: string[]): Linter.Config[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcloud/eslint-config",
3
- "version": "9.0.0-rc.4",
3
+ "version": "9.0.0-rc.6",
4
4
  "description": "Eslint shared config for nextcloud apps and libraries",
5
5
  "keywords": [
6
6
  "eslint",
@@ -37,31 +37,31 @@
37
37
  "build": "npm run build:cleanup && npm run build:source",
38
38
  "build:cleanup": "tsc --build --clean",
39
39
  "build:source": "tsc",
40
- "lint": "npx --node-options='--experimental-strip-types' eslint --flag unstable_native_nodejs_ts_config",
41
- "lint:fix": "npx --node-options='--experimental-strip-types' eslint --flag unstable_native_nodejs_ts_config --fix",
40
+ "lint": "eslint --flag unstable_native_nodejs_ts_config",
41
+ "lint:fix": "eslint --flag unstable_native_nodejs_ts_config --fix",
42
42
  "prerelease:format-changelog": "node build/format-changelog.mjs",
43
43
  "test": "vitest run"
44
44
  },
45
45
  "dependencies": {
46
- "@eslint/json": "^0.13.1",
47
- "@stylistic/eslint-plugin": "^5.2.2",
46
+ "@eslint/json": "^0.14.0",
47
+ "@stylistic/eslint-plugin": "^5.5.0",
48
48
  "eslint-config-flat-gitignore": "^2.1.0",
49
49
  "eslint-plugin-antfu": "^3.1.1",
50
- "eslint-plugin-jsdoc": "^51.2.3",
51
- "eslint-plugin-perfectionist": "^4.15.0",
52
- "eslint-plugin-vue": "^10.3.0",
53
- "fast-xml-parser": "^5.2.5",
54
- "globals": "^16.3.0",
55
- "semver": "^7.7.2",
50
+ "eslint-plugin-jsdoc": "^61.2.1",
51
+ "eslint-plugin-perfectionist": "^4.15.1",
52
+ "eslint-plugin-vue": "^10.5.1",
53
+ "fast-xml-parser": "^5.3.2",
54
+ "globals": "^16.5.0",
55
+ "semver": "^7.7.3",
56
56
  "sort-package-json": "^3.4.0",
57
- "typescript-eslint": "^8.38.0"
57
+ "typescript-eslint": "^8.46.4"
58
58
  },
59
59
  "devDependencies": {
60
- "@types/node": "^24.1.0",
61
- "@types/semver": "^7.7.0",
62
- "eslint": "^9.32.0",
63
- "memfs": "^4.24.0",
64
- "vitest": "^3.2.4"
60
+ "@types/node": "^24.10.1",
61
+ "@types/semver": "^7.7.1",
62
+ "eslint": "^9.39.1",
63
+ "memfs": "^4.51.0",
64
+ "vitest": "^4.0.13"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "eslint": ">=9"
@@ -73,12 +73,12 @@
73
73
  "packageManager": [
74
74
  {
75
75
  "name": "npm",
76
- "version": "^10"
76
+ "version": "^11.3.0"
77
77
  }
78
78
  ],
79
79
  "runtime": {
80
80
  "name": "node",
81
- "version": "^22.10"
81
+ "version": "^24.3.0"
82
82
  }
83
83
  }
84
84
  }