@wordpress/eslint-plugin 25.0.0 → 25.1.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/README.md CHANGED
@@ -14,7 +14,11 @@ npm install @wordpress/eslint-plugin --save-dev
14
14
 
15
15
  ## Usage
16
16
 
17
- ### Flat config (ESLint v10+, recommended)
17
+ **Minimum ESLint version:** `^9.0.0 || ^10.0.0`
18
+
19
+ > **Upgrading from an older version?** See the [ESLint v10 migration guide](https://github.com/WordPress/gutenberg/blob/HEAD/docs/how-to-guides/eslint-v10-migration.md) for a comprehensive walkthrough of the breaking changes, migration steps, and troubleshooting.
20
+
21
+ ### Flat config (ESLint v9+, recommended)
18
22
 
19
23
  Create an `eslint.config.mjs` file in your project root:
20
24
 
@@ -41,7 +45,7 @@ export default [
41
45
 
42
46
  Refer to the [ESLint flat config documentation](https://eslint.org/docs/latest/use/configure/configuration-files) for more information.
43
47
 
44
- ### Legacy eslintrc (ESLint v9, deprecated)
48
+ ### Legacy eslintrc (ESLint v9 only, deprecated)
45
49
 
46
50
  If you are still using ESLint v9 with the legacy `.eslintrc.*` format, a compatibility wrapper is available:
47
51
 
@@ -52,7 +56,9 @@ const wordpress = require( '@wordpress/eslint-plugin/eslintrc' );
52
56
  module.exports = wordpress.configs.recommended;
53
57
  ```
54
58
 
55
- > **Note:** The eslintrc wrapper is deprecated and will be removed in a future major version. Please migrate to flat config.
59
+ All config presets are available through the wrapper (e.g., `wordpress.configs.esnext`, `wordpress.configs[ 'recommended-with-formatting' ]`).
60
+
61
+ > **Note:** The eslintrc wrapper is deprecated and will be removed in the next major version. ESLint v10 does not support `.eslintrc.*` files at all. Please migrate to flat config.
56
62
 
57
63
  ### About the recommended preset
58
64
 
@@ -121,6 +127,7 @@ The granular rulesets will not define any environment globals. As such, if they
121
127
  | [no-dom-globals-in-react-fc](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-dom-globals-in-react-fc.md) | Disallow use of DOM globals in the render cycle of a React function component. | |
122
128
  | [components-no-missing-40px-size-prop](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/components-no-missing-40px-size-prop.md) | Disallow missing `__next40pxDefaultSize` prop on `@wordpress/components` components. | ✓ |
123
129
  | [components-no-unsafe-button-disabled](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/components-no-unsafe-button-disabled.md) | Disallow using `disabled` on Button without `accessibleWhenDisabled`. | ✓ |
130
+ | [no-unsafe-render-order](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unsafe-render-order.md) | Prevent unsafe `render` composition orders that silently remove semantics. | ✓ |
124
131
  | [no-i18n-in-save](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-i18n-in-save.md) | Disallow translation functions in block save methods. | |
125
132
  | [no-unmerged-classname](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unmerged-classname.md) | Disallow unmerged `className` in components that spread rest props. | |
126
133
  | [no-unguarded-get-range-at](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unguarded-get-range-at.md) | Disallow the usage of unguarded `getRangeAt` calls. | ✓ |
@@ -129,6 +136,7 @@ The granular rulesets will not define any environment globals. As such, if they
129
136
  | [no-unused-vars-before-return](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md) | Disallow assigning variable values if unused before a return. | ✓ |
130
137
  | [no-wp-process-env](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-wp-process-env.md) | Disallow legacy usage of WordPress variables via `process.env` like `process.env.SCRIPT_DEBUG`. | ✓ |
131
138
  | [react-no-unsafe-timeout](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md) | Disallow unsafe `setTimeout` in component. | |
139
+ | [use-import-as](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/use-import-as.md) | Enforce configured `as` names for specific named imports and unlocked private APIs. | |
132
140
  | [valid-sprintf](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/valid-sprintf.md) | Enforce valid sprintf usage. | ✓ |
133
141
  | [wp-global-usage](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/wp-global-usage.md) | Enforce correct usage of WordPress globals like `globalThis.SCRIPT_DEBUG`. | |
134
142
 
@@ -151,8 +159,14 @@ If you are upgrading from a previous version that used `.eslintrc.*` files:
151
159
  3. Convert `overrides` to separate config objects with `files` patterns.
152
160
  4. Replace `env` with `languageOptions.globals` using the [`globals`](https://www.npmjs.com/package/globals) package.
153
161
  5. Delete your `.eslintignore` file and move patterns into an `ignores` config object.
162
+ 6. Update rule prefixes in inline comments: `eslint-comments/*` has been renamed to `@eslint-community/eslint-comments/*`. For example:
163
+ ```diff
164
+ - /* eslint-disable eslint-comments/no-unlimited-disable */
165
+ + /* eslint-disable @eslint-community/eslint-comments/no-unlimited-disable */
166
+ ```
167
+ 7. Remove any `/* eslint-env */` comments — they are no longer supported in ESLint v10. Use `languageOptions.globals` in your config instead.
154
168
 
155
- See the [ESLint migration guide](https://eslint.org/docs/latest/use/configure/migration-guide) for full details.
169
+ For a comprehensive walkthrough with examples and troubleshooting, see the [Gutenberg ESLint v10 migration guide](https://github.com/WordPress/gutenberg/blob/HEAD/docs/how-to-guides/eslint-v10-migration.md). See also the [ESLint migration guide](https://eslint.org/docs/latest/use/configure/migration-guide) for general flat config details.
156
170
 
157
171
  ## Contributing to this package
158
172
 
@@ -1,16 +1,14 @@
1
- /* eslint-disable jsdoc/check-tag-names -- Package names containing @ are not JSDoc tags */
2
1
  /**
3
2
  * Wrapper around @babel/eslint-parser that ensures compatibility with
4
3
  * ESLint v10+. ESLint v10 expects `scopeManager.addGlobals()` which is
5
4
  * available in eslint-scope v9 bundled with ESLint v10, but
6
- * @babel/eslint-parser v7 uses an older eslint-scope that does not have
5
+ * `@babel/eslint-parser` v7 uses an older eslint-scope that does not have
7
6
  * this method. This wrapper patches the scope manager when needed.
8
7
  *
9
8
  * TODO: Remove this wrapper when upgrading to @babel/eslint-parser v8+,
10
9
  * which adds native ESLint v10 support (requires @babel/core v8).
11
10
  * See https://github.com/babel/babel/issues/17791
12
11
  */
13
- /* eslint-enable jsdoc/check-tag-names */
14
12
  const babelParser = require( '@babel/eslint-parser' );
15
13
 
16
14
  module.exports = {
package/configs/custom.js CHANGED
@@ -16,6 +16,7 @@ module.exports = [
16
16
  '@wordpress/no-unguarded-get-range-at': 'error',
17
17
  '@wordpress/no-global-active-element': 'error',
18
18
  '@wordpress/no-global-get-selection': 'error',
19
+ '@wordpress/no-unsafe-render-order': 'error',
19
20
  '@wordpress/no-setting-ds-tokens': 'error',
20
21
  '@wordpress/no-unknown-ds-tokens': 'error',
21
22
  '@wordpress/no-unsafe-wp-apis': 'error',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/eslint-plugin",
3
- "version": "25.0.0",
3
+ "version": "25.1.0",
4
4
  "description": "ESLint plugin for WordPress development.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -44,9 +44,9 @@
44
44
  "@babel/eslint-parser": "^7.28.6",
45
45
  "@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
46
46
  "@eslint/compat": "^2.0.0",
47
- "@wordpress/babel-preset-default": "^8.44.0",
48
- "@wordpress/prettier-config": "^4.44.0",
49
- "@wordpress/theme": "^0.11.0",
47
+ "@wordpress/babel-preset-default": "^8.45.0",
48
+ "@wordpress/prettier-config": "^4.45.0",
49
+ "@wordpress/theme": "^0.12.0",
50
50
  "cosmiconfig": "^7.0.0",
51
51
  "eslint-config-prettier": "^10.0.0",
52
52
  "eslint-import-resolver-typescript": "^4.4.4",
@@ -83,5 +83,5 @@
83
83
  "publishConfig": {
84
84
  "access": "public"
85
85
  },
86
- "gitHead": "b862d8c84121a47bbeff882f6c87e61681ce2e0d"
86
+ "gitHead": "8c229eaed0e88c9827e2da3d73a78f9ddd77714b"
87
87
  }
@@ -35,6 +35,18 @@ ruleTester.run( 'no-setting-ds-tokens', rule, {
35
35
  {
36
36
  code: `const { '--wpds-color-fg-content-neutral': neutralColor } = styles;`,
37
37
  },
38
+ {
39
+ code: `const css = '--my-custom-prop: red;';`,
40
+ },
41
+ {
42
+ code: 'const css = `--my-custom-prop-${ suffix }: red;`;',
43
+ },
44
+ {
45
+ code: 'const css = `--my-custom-prop: red;`;',
46
+ },
47
+ {
48
+ code: '<style>{ `--my-custom-prop-${ suffix }: red;` }</style>',
49
+ },
38
50
  ],
39
51
  invalid: [
40
52
  {
@@ -77,5 +89,45 @@ ruleTester.run( 'no-setting-ds-tokens', rule, {
77
89
  },
78
90
  ],
79
91
  },
92
+ {
93
+ code: `const css = '--wpds-color-fg-content-neutral: red;';`,
94
+ errors: [
95
+ {
96
+ messageId: 'disallowedSet',
97
+ },
98
+ ],
99
+ },
100
+ {
101
+ code: 'const css = `--wpds-color-fg-content-neutral: red;`;',
102
+ errors: [
103
+ {
104
+ messageId: 'disallowedSet',
105
+ },
106
+ ],
107
+ },
108
+ {
109
+ code: 'const css = `--wpds-color-fg-content-neutral: ${ value };`;',
110
+ errors: [
111
+ {
112
+ messageId: 'disallowedSet',
113
+ },
114
+ ],
115
+ },
116
+ {
117
+ code: 'const css = `--wpds-color-${ suffix }: red;`;',
118
+ errors: [
119
+ {
120
+ messageId: 'disallowedSet',
121
+ },
122
+ ],
123
+ },
124
+ {
125
+ code: '<style>{ `--wpds-color-${ suffix }: red;` }</style>',
126
+ errors: [
127
+ {
128
+ messageId: 'disallowedSet',
129
+ },
130
+ ],
131
+ },
80
132
  ],
81
133
  } );
@@ -44,6 +44,15 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
44
44
  {
45
45
  code: `const style = { '--wpds-color-fg-content-neutral': 'red' };`,
46
46
  },
47
+ {
48
+ code: `const css = '--wpds-color-fg-content-neutral: red;';`,
49
+ },
50
+ {
51
+ code: 'const css = `--wpds-color-fg-content-neutral: red;`;',
52
+ },
53
+ {
54
+ code: 'const css = `--wpds-color-fg-content-neutral: ${ value };`;',
55
+ },
47
56
  ],
48
57
  invalid: [
49
58
  {
@@ -147,6 +156,17 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
147
156
  },
148
157
  ],
149
158
  },
159
+ {
160
+ code: `const css = '--wpds-nonexistent-token: red;';`,
161
+ errors: [
162
+ {
163
+ messageId: 'onlyKnownTokens',
164
+ data: {
165
+ tokenNames: "'--wpds-nonexistent-token'",
166
+ },
167
+ },
168
+ ],
169
+ },
150
170
  {
151
171
  code: `const token = '--wpds-color-fg-content-neutral';`,
152
172
  errors: [
@@ -202,5 +222,39 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
202
222
  },
203
223
  ],
204
224
  },
225
+ {
226
+ code: `const css = '--wpds-color-fg-content-neutral: red; color: --wpds-color-bg-surface-neutral;';`,
227
+ errors: [
228
+ {
229
+ messageId: 'bareToken',
230
+ data: {
231
+ tokenNames: "'--wpds-color-bg-surface-neutral'",
232
+ },
233
+ },
234
+ ],
235
+ },
236
+ {
237
+ code: `const css = '--wpds-color-fg-content-neutral: red; background: --wpds-color-fg-content-neutral;';`,
238
+ errors: [
239
+ {
240
+ messageId: 'bareToken',
241
+ data: {
242
+ tokenNames: "'--wpds-color-fg-content-neutral'",
243
+ },
244
+ },
245
+ ],
246
+ },
247
+ {
248
+ code: `const css = '--wpds-other-nonexistent-token: red; color: var(--wpds-nonexistent-token);';`,
249
+ errors: [
250
+ {
251
+ messageId: 'onlyKnownTokens',
252
+ data: {
253
+ tokenNames:
254
+ "'--wpds-other-nonexistent-token', '--wpds-nonexistent-token'",
255
+ },
256
+ },
257
+ ],
258
+ },
205
259
  ],
206
260
  } );
@@ -0,0 +1,136 @@
1
+ import { RuleTester } from 'eslint';
2
+ import rule from '../no-unsafe-render-order';
3
+
4
+ const ruleTester = new RuleTester( {
5
+ languageOptions: {
6
+ sourceType: 'module',
7
+ ecmaVersion: 6,
8
+ parserOptions: {
9
+ ecmaFeatures: {
10
+ jsx: true,
11
+ },
12
+ },
13
+ },
14
+ } );
15
+
16
+ ruleTester.run( 'no-unsafe-render-order', rule, {
17
+ valid: [
18
+ {
19
+ code: `
20
+ import { Dialog, VisuallyHidden } from '@wordpress/ui';
21
+
22
+ <VisuallyHidden render={ <Dialog.Title /> }>
23
+ Title
24
+ </VisuallyHidden>;
25
+ `,
26
+ },
27
+ {
28
+ code: `
29
+ import { Text, Link } from '@wordpress/ui';
30
+
31
+ <Text render={ <Link href="#" /> }>Read more</Text>;
32
+ `,
33
+ },
34
+ {
35
+ code: `
36
+ import { Popover } from '@wordpress/ui';
37
+ import { VisuallyHidden } from 'some-other-package';
38
+
39
+ <Popover.Title render={ <VisuallyHidden /> }>
40
+ Title
41
+ </Popover.Title>;
42
+ `,
43
+ },
44
+ {
45
+ code: `
46
+ import { Link } from '@wordpress/ui';
47
+
48
+ <Link href="#">Read more</Link>;
49
+ `,
50
+ },
51
+ {
52
+ code: `
53
+ import { Link } from '@wordpress/ui';
54
+ import { VisuallyHidden } from 'some-other-package';
55
+
56
+ <Link href="#" render={ <VisuallyHidden /> }>
57
+ Read more
58
+ </Link>;
59
+ `,
60
+ },
61
+ ],
62
+ invalid: [
63
+ {
64
+ code: `
65
+ import { Dialog, VisuallyHidden } from '@wordpress/ui';
66
+
67
+ <Dialog.Title render={ <VisuallyHidden /> }>
68
+ Title
69
+ </Dialog.Title>;
70
+ `,
71
+ errors: [ { messageId: 'visuallyHiddenOrder' } ],
72
+ },
73
+ {
74
+ code: `
75
+ import { Dialog as UIDialog, VisuallyHidden as Hidden } from '@wordpress/ui';
76
+
77
+ <UIDialog.Title render={ <Hidden /> }>
78
+ Title
79
+ </UIDialog.Title>;
80
+ `,
81
+ errors: [ { messageId: 'visuallyHiddenOrder' } ],
82
+ },
83
+ {
84
+ code: `
85
+ import { VisuallyHidden } from '@wordpress/ui';
86
+
87
+ <CustomThing render={ <VisuallyHidden /> }>
88
+ Hidden content
89
+ </CustomThing>;
90
+ `,
91
+ errors: [ { messageId: 'visuallyHiddenOrder' } ],
92
+ },
93
+ {
94
+ code: `
95
+ import { Link, Text } from '@wordpress/ui';
96
+
97
+ <Link href="#" render={ <Text /> }>
98
+ Read more
99
+ </Link>;
100
+ `,
101
+ errors: [ { messageId: 'linkTextOrder' } ],
102
+ },
103
+ {
104
+ code: `
105
+ import { Link as UILink, Text as UIText } from '@wordpress/ui';
106
+
107
+ <UILink href="#" render={ <UIText /> }>
108
+ Read more
109
+ </UILink>;
110
+ `,
111
+ errors: [ { messageId: 'linkTextOrder' } ],
112
+ },
113
+ {
114
+ code: `
115
+ import * as Field from '../index';
116
+ import { VisuallyHidden } from '../../../visually-hidden';
117
+
118
+ <Field.Label render={ <VisuallyHidden /> }>Name</Field.Label>;
119
+ `,
120
+ options: [ { checkLocalImports: true } ],
121
+ errors: [ { messageId: 'visuallyHiddenOrder' } ],
122
+ },
123
+ {
124
+ code: `
125
+ import { Link } from '../index';
126
+ import { Text } from '../../text';
127
+
128
+ <Link href="#" render={ <Text /> }>
129
+ Read more
130
+ </Link>;
131
+ `,
132
+ options: [ { checkLocalImports: true } ],
133
+ errors: [ { messageId: 'linkTextOrder' } ],
134
+ },
135
+ ],
136
+ } );
@@ -0,0 +1,271 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { RuleTester } from 'eslint';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import rule from '../use-import-as';
10
+
11
+ const ruleTester = new RuleTester( {
12
+ languageOptions: {
13
+ sourceType: 'module',
14
+ ecmaVersion: 2022,
15
+ },
16
+ } );
17
+
18
+ const options = [
19
+ {
20
+ '@wordpress/components': {
21
+ Badge: 'WCBadge',
22
+ VisuallyHidden: 'WCVisuallyHidden',
23
+ },
24
+ },
25
+ ];
26
+
27
+ const withSuggestions = ( message, output, desc ) => ( {
28
+ message,
29
+ suggestions: [
30
+ {
31
+ desc,
32
+ output,
33
+ },
34
+ ],
35
+ } );
36
+
37
+ ruleTester.run( 'use-import-as', rule, {
38
+ valid: [
39
+ // With no config, the rule is a no-op.
40
+ {
41
+ code: "import { VisuallyHidden } from '@wordpress/components';",
42
+ },
43
+
44
+ // Unrelated packages are not affected.
45
+ {
46
+ code: "import { VisuallyHidden } from '@wordpress/ui';",
47
+ options,
48
+ },
49
+ {
50
+ code: "import { Button } from '@wordpress/components';",
51
+ options,
52
+ },
53
+
54
+ // Default and namespace imports are not affected.
55
+ {
56
+ code: "import Components from '@wordpress/components';",
57
+ options,
58
+ },
59
+ {
60
+ code: "import * as Components from '@wordpress/components';",
61
+ options,
62
+ },
63
+
64
+ // Configured `as` names are allowed.
65
+ {
66
+ code: "import { VisuallyHidden as WCVisuallyHidden } from '@wordpress/components';",
67
+ options,
68
+ },
69
+ {
70
+ code: 'import { "VisuallyHidden" as WCVisuallyHidden } from \'@wordpress/components\';',
71
+ options,
72
+ },
73
+ {
74
+ code: "import { Button, VisuallyHidden as WCVisuallyHidden } from '@wordpress/components';",
75
+ options,
76
+ },
77
+ {
78
+ code: `
79
+ import { privateApis as componentsPrivateApis } from '@wordpress/components';
80
+ import { unlock } from '../../lock-unlock';
81
+
82
+ const { Badge: WCBadge } = unlock( componentsPrivateApis );
83
+ `,
84
+ options,
85
+ },
86
+ {
87
+ code: `
88
+ import { privateApis } from '@wordpress/components';
89
+ import { unlock } from '../../lock-unlock';
90
+
91
+ const { Badge: WCBadge } = unlock( privateApis );
92
+ `,
93
+ options,
94
+ },
95
+ {
96
+ code: `
97
+ import { privateApis } from '@wordpress/components';
98
+ import { unlock as open } from '../../lock-unlock';
99
+
100
+ const { Badge: WCBadge = fallbackBadge } = open( privateApis );
101
+ `,
102
+ options,
103
+ },
104
+ {
105
+ code: `
106
+ import { privateApis as uiPrivateApis } from '@wordpress/ui';
107
+ import { unlock } from '../../lock-unlock';
108
+
109
+ const { Badge } = unlock( uiPrivateApis );
110
+ `,
111
+ options,
112
+ },
113
+ {
114
+ code: `
115
+ import { privateApis } from '@wordpress/components';
116
+ import { unlock } from '../../lock-unlock';
117
+
118
+ const { [ badgeKey ]: Badge } = unlock( privateApis );
119
+ `,
120
+ options,
121
+ },
122
+ {
123
+ code: `
124
+ import { privateApis } from '@wordpress/components';
125
+ import { unlock } from '../../lock-unlock';
126
+
127
+ function test() {
128
+ function unlock( value ) {
129
+ return value;
130
+ }
131
+
132
+ const { Badge } = unlock( privateApis );
133
+
134
+ return Badge;
135
+ }
136
+ `,
137
+ options,
138
+ },
139
+ ],
140
+
141
+ invalid: [
142
+ {
143
+ code: "import { VisuallyHidden } from '@wordpress/components';",
144
+ options,
145
+ errors: [
146
+ withSuggestions(
147
+ '`VisuallyHidden` from `@wordpress/components` must be imported as `WCVisuallyHidden`.',
148
+ "import { VisuallyHidden as WCVisuallyHidden } from '@wordpress/components';",
149
+ 'Import as `WCVisuallyHidden`.'
150
+ ),
151
+ ],
152
+ },
153
+ {
154
+ code: "import { VisuallyHidden as Hidden } from '@wordpress/components';",
155
+ options,
156
+ errors: [
157
+ withSuggestions(
158
+ '`VisuallyHidden` from `@wordpress/components` must be imported as `WCVisuallyHidden`.',
159
+ "import { VisuallyHidden as WCVisuallyHidden } from '@wordpress/components';",
160
+ 'Import as `WCVisuallyHidden`.'
161
+ ),
162
+ ],
163
+ },
164
+ {
165
+ code: 'import { "VisuallyHidden" as Hidden } from \'@wordpress/components\';',
166
+ options,
167
+ errors: [
168
+ withSuggestions(
169
+ '`VisuallyHidden` from `@wordpress/components` must be imported as `WCVisuallyHidden`.',
170
+ 'import { "VisuallyHidden" as WCVisuallyHidden } from \'@wordpress/components\';',
171
+ 'Import as `WCVisuallyHidden`.'
172
+ ),
173
+ ],
174
+ },
175
+ {
176
+ code: "import { Button, VisuallyHidden } from '@wordpress/components';",
177
+ options,
178
+ errors: [
179
+ withSuggestions(
180
+ '`VisuallyHidden` from `@wordpress/components` must be imported as `WCVisuallyHidden`.',
181
+ "import { Button, VisuallyHidden as WCVisuallyHidden } from '@wordpress/components';",
182
+ 'Import as `WCVisuallyHidden`.'
183
+ ),
184
+ ],
185
+ },
186
+ {
187
+ code: `
188
+ import { privateApis as componentsPrivateApis } from '@wordpress/components';
189
+ import { unlock } from '../../lock-unlock';
190
+
191
+ const { Badge } = unlock( componentsPrivateApis );
192
+ `,
193
+ options,
194
+ errors: [
195
+ withSuggestions(
196
+ '`Badge` from `@wordpress/components` must be imported as `WCBadge`.',
197
+ `
198
+ import { privateApis as componentsPrivateApis } from '@wordpress/components';
199
+ import { unlock } from '../../lock-unlock';
200
+
201
+ const { Badge: WCBadge } = unlock( componentsPrivateApis );
202
+ `,
203
+ 'Destructure as `WCBadge`.'
204
+ ),
205
+ ],
206
+ },
207
+ {
208
+ code: `
209
+ import { privateApis } from '@wordpress/components';
210
+ import { unlock } from '../../lock-unlock';
211
+
212
+ const { Badge: HiddenBadge } = unlock( privateApis );
213
+ `,
214
+ options,
215
+ errors: [
216
+ withSuggestions(
217
+ '`Badge` from `@wordpress/components` must be imported as `WCBadge`.',
218
+ `
219
+ import { privateApis } from '@wordpress/components';
220
+ import { unlock } from '../../lock-unlock';
221
+
222
+ const { Badge: WCBadge } = unlock( privateApis );
223
+ `,
224
+ 'Destructure as `WCBadge`.'
225
+ ),
226
+ ],
227
+ },
228
+ {
229
+ code: `
230
+ import { privateApis } from '@wordpress/components';
231
+ import { unlock } from '../../lock-unlock';
232
+
233
+ const { Badge: HiddenBadge = fallbackBadge } = unlock( privateApis );
234
+ `,
235
+ options,
236
+ errors: [
237
+ withSuggestions(
238
+ '`Badge` from `@wordpress/components` must be imported as `WCBadge`.',
239
+ `
240
+ import { privateApis } from '@wordpress/components';
241
+ import { unlock } from '../../lock-unlock';
242
+
243
+ const { Badge: WCBadge = fallbackBadge } = unlock( privateApis );
244
+ `,
245
+ 'Destructure as `WCBadge`.'
246
+ ),
247
+ ],
248
+ },
249
+ {
250
+ code: `
251
+ import { privateApis } from '@wordpress/components';
252
+ import { unlock } from '../../lock-unlock';
253
+
254
+ const { Badge = fallbackBadge } = unlock( privateApis );
255
+ `,
256
+ options,
257
+ errors: [
258
+ withSuggestions(
259
+ '`Badge` from `@wordpress/components` must be imported as `WCBadge`.',
260
+ `
261
+ import { privateApis } from '@wordpress/components';
262
+ import { unlock } from '../../lock-unlock';
263
+
264
+ const { Badge: WCBadge = fallbackBadge } = unlock( privateApis );
265
+ `,
266
+ 'Destructure as `WCBadge`.'
267
+ ),
268
+ ],
269
+ },
270
+ ],
271
+ } );
@@ -27,8 +27,10 @@ ruleTester.run( 'use-recommended-components', rule, {
27
27
 
28
28
  // Allowed @wordpress/ui components.
29
29
  "import { Badge } from '@wordpress/ui';",
30
+ "import { Link } from '@wordpress/ui';",
30
31
  "import { Stack } from '@wordpress/ui';",
31
- "import { Badge, Stack } from '@wordpress/ui';",
32
+ "import { Text } from '@wordpress/ui';",
33
+ "import { Badge, Link, Stack, Text } from '@wordpress/ui';",
32
34
  ],
33
35
 
34
36
  invalid: [
@@ -1,6 +1,4 @@
1
- const DS_TOKEN_PREFIX = 'wpds-';
2
-
3
- const wpdsTokensRegex = new RegExp( `(?:^|[^\\w])--${ DS_TOKEN_PREFIX }`, 'i' );
1
+ const { wpdsTokensRegex } = require( '../utils/ds-token-utils' );
4
2
 
5
3
  module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
6
4
  meta: {