-
Notifications
You must be signed in to change notification settings - Fork 880
Lint: use recommended components instead of deprecated Jetpack components #49284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
88031e5
846e88f
df916df
b960781
9fd747f
770e765
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| module.exports = { | ||
| rules: { | ||
| 'use-recommended-jetpack-components': require( './rules/use-recommended-jetpack-components.cjs' ), | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "components": {}, | ||
| "subpaths": {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| const { createRequire } = require( 'node:module' ); | ||
| const path = require( 'node:path' ); | ||
| const requireFromJsTools = createRequire( | ||
| path.join( __dirname, '..', '..', '..', '..', 'js-tools', 'package.json' ) | ||
| ); | ||
| const { RuleTester } = requireFromJsTools( 'eslint' ); | ||
| const rule = require( '../use-recommended-jetpack-components.cjs' ); | ||
|
|
||
| const denylistPath = path.resolve( | ||
| __dirname, | ||
| '..', | ||
| '..', | ||
| '..', | ||
| 'jetpack-components-denylist.json' | ||
| ); | ||
|
|
||
| const ruleTester = new RuleTester( { | ||
| languageOptions: { | ||
| sourceType: 'module', | ||
| ecmaVersion: 'latest', | ||
| }, | ||
| } ); | ||
|
|
||
| ruleTester.run( 'use-recommended-jetpack-components', rule, { | ||
| valid: [ | ||
| "import { getRedirectUrl } from '@automattic/jetpack-api';", | ||
| { | ||
| code: "import { JetpackLogo } from '@automattic/jetpack-components';", | ||
| options: [ { denylistPath: path.join( __dirname, 'fixtures', 'empty-denylist.json' ) } ], | ||
| }, | ||
| ], | ||
|
|
||
| invalid: [ | ||
| { | ||
| code: "import { Button } from '@automattic/jetpack-components';", | ||
| options: [ { denylistPath } ], | ||
| errors: [ | ||
| { | ||
| message: 'Use `Button` from `@wordpress/ui` instead.', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| code: "import Button from '@automattic/jetpack-components/button';", | ||
| options: [ { denylistPath } ], | ||
| errors: [ | ||
| { | ||
| message: 'Use `Button` from `@wordpress/ui` instead.', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| } ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| const fs = require( 'node:fs' ); | ||
| const path = require( 'node:path' ); | ||
|
|
||
| const PACKAGE_NAME = '@automattic/jetpack-components'; | ||
| const DEFAULT_DENYLIST_PATH = path.resolve( | ||
| __dirname, | ||
| '..', | ||
| '..', | ||
| 'jetpack-components-denylist.json' | ||
| ); | ||
|
|
||
| /** | ||
| * Interpolate `{{ name }}` and `{{ source }}` placeholders in a lint message. | ||
| * | ||
| * @param {string|undefined} template - Message template. | ||
| * @param {string} name - Component name. | ||
| * @param {string} source - Import source. | ||
| * @return {string} Resolved message string. | ||
| */ | ||
| function resolveMessage( template, name, source ) { | ||
| if ( ! template ) { | ||
| return `\`${ name }\` from \`${ source }\` is not recommended. Prefer \`@wordpress/components\` or \`@wordpress/ui\` when an equivalent exists.`; | ||
| } | ||
|
|
||
| return template.replace( /\{\{\s*name\s*\}\}/g, name ).replace( /\{\{\s*source\s*\}\}/g, source ); | ||
| } | ||
|
|
||
| /** | ||
| * Load the Jetpack components denylist from disk. | ||
| * | ||
| * @param {string} denylistPath - Path to the denylist JSON file. | ||
| * @return {{ components: Record<string, string>, subpaths: Record<string, string> }} Denylist data. | ||
| */ | ||
| function loadDenylist( denylistPath ) { | ||
| const raw = JSON.parse( fs.readFileSync( denylistPath, 'utf8' ) ); | ||
|
|
||
| return { | ||
| components: normalizeEntries( raw.components ), | ||
| subpaths: normalizeEntries( raw.subpaths ), | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Normalize denylist entries to a flat component-name → message map. | ||
| * | ||
| * @param {Record<string, string | { message: string }>|undefined} entries - Denylist entries. | ||
| * @return {Record<string, string>} Normalized message map. | ||
| */ | ||
| function normalizeEntries( entries ) { | ||
| if ( ! entries ) { | ||
| return {}; | ||
| } | ||
|
|
||
| return Object.fromEntries( | ||
| Object.entries( entries ).map( ( [ key, value ] ) => { | ||
| if ( typeof value === 'string' ) { | ||
| return [ key, value ]; | ||
| } | ||
|
|
||
| return [ key, value.message ]; | ||
| } ) | ||
| ); | ||
| } | ||
|
|
||
| /** @type {import('eslint').Rule.RuleModule} */ | ||
| const rule = { | ||
| meta: { | ||
| type: 'suggestion', | ||
| docs: { | ||
| description: | ||
| 'Encourage the use of WordPress components instead of discouraged Jetpack components.', | ||
| }, | ||
| schema: [ | ||
| { | ||
| type: 'object', | ||
| properties: { | ||
| denylistPath: { | ||
| type: 'string', | ||
| }, | ||
| }, | ||
| additionalProperties: false, | ||
| }, | ||
| ], | ||
| }, | ||
| create( context ) { | ||
| const options = context.options[ 0 ] ?? {}; | ||
| const configuredPath = options.denylistPath ?? DEFAULT_DENYLIST_PATH; | ||
| const denylistPath = path.isAbsolute( configuredPath ) | ||
| ? configuredPath | ||
| : path.resolve( context.cwd ?? process.cwd(), configuredPath ); | ||
| const { components, subpaths } = loadDenylist( denylistPath ); | ||
|
|
||
| return { | ||
| /** | ||
| * Lint import declarations from Jetpack components. | ||
| * | ||
| * @param {import('estree').ImportDeclaration} node - Import declaration AST node. | ||
| */ | ||
| ImportDeclaration( node ) { | ||
| if ( typeof node.source.value !== 'string' ) { | ||
| return; | ||
| } | ||
|
|
||
| const source = node.source.value; | ||
|
|
||
| if ( source === PACKAGE_NAME ) { | ||
| reportMainPackageImports( context, node, source, components ); | ||
| return; | ||
| } | ||
|
|
||
| if ( source.startsWith( `${ PACKAGE_NAME }/` ) ) { | ||
| reportSubpathImports( | ||
| context, | ||
| node, | ||
| source, | ||
| source.slice( PACKAGE_NAME.length + 1 ), | ||
| components, | ||
| subpaths | ||
| ); | ||
| } | ||
| }, | ||
| }; | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Report discouraged imports from `@automattic/jetpack-components`. | ||
| * | ||
| * @param {import('eslint').Rule.RuleContext} context - ESLint context. | ||
| * @param {import('estree').ImportDeclaration} node - Import declaration node. | ||
| * @param {string} source - Import source. | ||
| * @param {Record<string, string>} components - Component denylist. | ||
| */ | ||
| function reportMainPackageImports( context, node, source, components ) { | ||
| node.specifiers.forEach( specifier => { | ||
| if ( specifier.type !== 'ImportSpecifier' ) { | ||
| return; | ||
| } | ||
|
|
||
| const name = specifier.imported.name; | ||
| const message = components[ name ]; | ||
|
|
||
| if ( message ) { | ||
| context.report( { | ||
| node: specifier, | ||
| message: resolveMessage( message, name, source ), | ||
| } ); | ||
| } | ||
| } ); | ||
| } | ||
|
|
||
| /** | ||
| * Report discouraged imports from `@automattic/jetpack-components/*` subpaths. | ||
| * | ||
| * @param {import('eslint').Rule.RuleContext} context - ESLint context. | ||
| * @param {import('estree').ImportDeclaration} node - Import declaration node. | ||
| * @param {string} source - Import source. | ||
| * @param {string} subpath - Subpath without leading slash. | ||
| * @param {Record<string, string>} components - Component denylist. | ||
| * @param {Record<string, string>} subpaths - Subpath denylist. | ||
| */ | ||
| function reportSubpathImports( context, node, source, subpath, components, subpaths ) { | ||
| const subpathMessage = subpaths[ subpath ]; | ||
|
|
||
| node.specifiers.forEach( specifier => { | ||
| if ( specifier.type === 'ImportDefaultSpecifier' && subpathMessage ) { | ||
| context.report( { | ||
| node: specifier, | ||
| message: resolveMessage( subpathMessage, subpath, source ), | ||
| } ); | ||
| return; | ||
| } | ||
|
|
||
| if ( specifier.type !== 'ImportSpecifier' ) { | ||
| return; | ||
| } | ||
|
|
||
| const name = specifier.imported.name; | ||
| const message = components[ name ] ?? subpathMessage; | ||
|
|
||
| if ( message ) { | ||
| context.report( { | ||
| node: specifier, | ||
| message: resolveMessage( message, name, source ), | ||
| } ); | ||
| } | ||
| } ); | ||
| } | ||
|
|
||
| module.exports = rule; |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,80 @@ | ||||||||||
| { | ||||||||||
| "components": { | ||||||||||
| "ActionButton": "Use `Button` from `@wordpress/ui` instead.", | ||||||||||
| "ActionPopover": "Use `Popover` from `@wordpress/ui` instead.", | ||||||||||
| "AdminSection": "Jetpack-specific layout component — remove from denylist to allow.", | ||||||||||
| "AdminSectionHero": "Jetpack-specific layout component — remove from denylist to allow.", | ||||||||||
| "Alert": "Use `Notice` from `@wordpress/ui` instead.", | ||||||||||
| "AutomatticBylineLogo": "Jetpack branding component — remove from denylist to allow.", | ||||||||||
| "AutomatticForAgenciesLogo": "Jetpack branding component — remove from denylist to allow.", | ||||||||||
| "AutomatticIconLogo": "Jetpack branding component — remove from denylist to allow.", | ||||||||||
| "BoostScoreBar": "Jetpack-specific visualization — remove from denylist to allow.", | ||||||||||
| "BoostScoreGraph": "Jetpack-specific visualization — remove from denylist to allow.", | ||||||||||
| "Button": "Use `Button` from `@wordpress/ui` instead.", | ||||||||||
| "Chip": "Use `Badge` from `@wordpress/ui` instead.", | ||||||||||
| "cleanLocale": "Jetpack utility — remove from denylist to allow.", | ||||||||||
| "Col": "Jetpack layout component — remove from denylist to allow or use `Stack` from `@wordpress/ui`.", | ||||||||||
| "Container": "Jetpack layout component — remove from denylist to allow or use `Stack` from `@wordpress/ui`.", | ||||||||||
| "ContextualUpgradeTrigger": "Jetpack-specific upsell component — remove from denylist to allow.", | ||||||||||
| "CopyToClipboard": "Use `ClipboardButton` from `@wordpress/components` (no `@wordpress/ui` equivalent yet).", | ||||||||||
| "DecorativeCard": "Use `Card` from `@wordpress/ui` instead.", | ||||||||||
| "DetailsViewer": "Jetpack-specific component — remove from denylist to allow.", | ||||||||||
| "Dialog": "Use `Dialog` from `@wordpress/ui` instead.", | ||||||||||
| "DiffViewer": "Jetpack-specific component — remove from denylist to allow.", | ||||||||||
| "DonutMeter": "Jetpack-specific visualization — remove from denylist to allow.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @keoshi noted:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
See package |
||||||||||
| "DotPager": "Jetpack-specific component — remove from denylist to allow.", | ||||||||||
| "getProductCheckoutUrl": "Jetpack utility — remove from denylist to allow.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Remove utility. |
||||||||||
| "GlobalNotices": "Use `@wordpress/notices` instead.", | ||||||||||
| "globalNoticesStore": "Use `@wordpress/notices` instead.", | ||||||||||
| "Gravatar": "Use `@gravatar-com/hovercards` or `@wordpress/components` when appropriate.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Gravatar component is fine until we have something in UI and/or Gravatar package. |
||||||||||
| "H2": "Use `Text` with `variant=\"heading-*\"` from `@wordpress/ui` instead.", | ||||||||||
| "H3": "Use `Text` with `variant=\"heading-*\"` from `@wordpress/ui` instead.", | ||||||||||
|
Comment on lines
+30
to
+31
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to map to a specific variants. |
||||||||||
| "IconTooltip": "Use `Tooltip` from `@wordpress/ui` instead.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| "IconsCard": "Jetpack-specific component — remove from denylist to allow.", | ||||||||||
| "IndeterminateProgressBar": "Use `Spinner` from `@wordpress/components` (no `@wordpress/ui` equivalent yet).", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommend Spinner and/or ProgressBar? |
||||||||||
| "isFirstMonthTrial": "Jetpack utility — remove from denylist to allow.", | ||||||||||
| "JetpackFooter": "Jetpack branding component — remove from denylist to allow.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Allow |
||||||||||
| "JetpackProtectLogo": "Jetpack branding component — remove from denylist to allow.", | ||||||||||
| "JetpackSearchLogo": "Jetpack branding component — remove from denylist to allow.", | ||||||||||
| "JetpackVaultPressBackupLogo": "Jetpack branding component — remove from denylist to allow.", | ||||||||||
| "JetpackVideoPressLogo": "Jetpack branding component — remove from denylist to allow.", | ||||||||||
|
Comment on lines
+37
to
+40
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allow logos?
Suggested change
|
||||||||||
| "LoadingPlaceholder": "Use `Placeholder` from `@wordpress/components` (no `@wordpress/ui` equivalent yet).", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect, Placeholder has a different purpose. Keep allowing or migrate to Spinner/ProgressBar for now? |
||||||||||
| "MarkedLines": "Jetpack-specific component — remove from denylist to allow.", | ||||||||||
| "NavigatorModal": "Use `Dialog` from `@wordpress/ui` and `Navigator` from `@wordpress/components` instead.", | ||||||||||
| "Notice": "Use `Notice` from `@wordpress/ui` instead.", | ||||||||||
| "NumberControl": "Use `__experimentalNumberControl` from `@wordpress/components` (no `@wordpress/ui` equivalent yet).", | ||||||||||
| "NumberSlider": "Use `RangeControl` from `@wordpress/components` (no `@wordpress/ui` equivalent yet).", | ||||||||||
| "Popover": "Use `Popover` from `@wordpress/ui` instead.", | ||||||||||
| "PricingTable": "Jetpack-specific pricing component — remove from denylist to allow.", | ||||||||||
| "PricingTableColumn": "Jetpack-specific pricing component — remove from denylist to allow.", | ||||||||||
| "PricingTableHeader": "Jetpack-specific pricing component — remove from denylist to allow.", | ||||||||||
| "PricingTableItem": "Jetpack-specific pricing component — remove from denylist to allow.", | ||||||||||
| "ProductOffer": "Jetpack-specific pricing component — remove from denylist to allow.", | ||||||||||
| "ProductPrice": "Jetpack-specific pricing component — remove from denylist to allow.", | ||||||||||
| "ProgressBar": "Use `ProgressBar` from `@wordpress/components` (no `@wordpress/ui` equivalent yet).", | ||||||||||
| "QRCode": "Jetpack-specific component — remove from denylist to allow.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Used twice in the repo. |
||||||||||
| "RadioControl": "Use `RadioControl` from `@wordpress/components` (no `@wordpress/ui` equivalent yet).", | ||||||||||
| "RecordMeterBar": "Jetpack-specific visualization — remove from denylist to allow.", | ||||||||||
| "Spinner": "Use `Spinner` from `@wordpress/components` (no `@wordpress/ui` equivalent yet).", | ||||||||||
| "SplitButton": "Use `Button` from `@wordpress/ui` (and `Dropdown` from `@wordpress/components` if needed) instead.", | ||||||||||
| "StatCard": "Jetpack-specific component — remove from denylist to allow.", | ||||||||||
| "Status": "Use `Badge` from `@wordpress/ui` instead.", | ||||||||||
| "TermsOfService": "Jetpack-specific component — remove from denylist to allow.", | ||||||||||
| "Testimonials": "Jetpack-specific component — remove from denylist to allow.", | ||||||||||
| "Text": "Use `Text` from `@wordpress/ui` instead.", | ||||||||||
| "ThemeProvider": "Use `@wordpress/theme` instead.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might still continue provide Jetpack brand color but that said, it's a bit heavy-handed way to supply it. |
||||||||||
| "Title": "Use `Text` from `@wordpress/ui` instead.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to pin a specific variant. |
||||||||||
| "ToggleControl": "Use `ToggleControl` from `@wordpress/components` (no `@wordpress/ui` equivalent yet).", | ||||||||||
| "useBreakpointMatch": "Jetpack layout hook — remove from denylist to allow.", | ||||||||||
| "useGlobalNotices": "Use `@wordpress/notices` instead.", | ||||||||||
| "ZendeskChat": "Jetpack-specific integration — remove from denylist to allow." | ||||||||||
| }, | ||||||||||
| "subpaths": { | ||||||||||
| "button": "Use `Button` from `@wordpress/ui` instead.", | ||||||||||
| "global-notices": "Use `@wordpress/notices` instead.", | ||||||||||
| "gravatar": "Use `@gravatar-com/hovercards` or `@wordpress/components` when appropriate.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Allow. |
||||||||||
| "jetpack-footer": "Jetpack branding component — remove from denylist to allow.", | ||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Allow |
||||||||||
| "pricing-table": "Jetpack-specific pricing component — remove from denylist to allow.", | ||||||||||
| "product-price": "Jetpack-specific pricing component — remove from denylist to allow." | ||||||||||
| } | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Jetpack components denylist | ||
|
|
||
| Curate discouraged `@automattic/jetpack-components` imports in | ||
| [`jetpack-components-denylist.json`](./jetpack-components-denylist.json). | ||
|
|
||
| The ESLint rule `@automattic/jetpack/use-recommended-jetpack-components` reads this | ||
| file and reports imports that match entries in `components` or `subpaths`. | ||
|
|
||
| ## How to curate | ||
|
|
||
| 1. Open `jetpack-components-denylist.json`. | ||
| 2. **Allow a component:** delete its key from `components` (or `subpaths` for subpath imports). | ||
| 3. **Change the lint message:** edit the string value or `{ "message": "..." }` object. | ||
| 4. **Add a component:** add a key under `components` for named imports from the main | ||
| package, or under `subpaths` for `@automattic/jetpack-components/<path>` imports. | ||
|
|
||
| When writing messages, prefer **`@wordpress/ui`** over **`@wordpress/components`** | ||
| when both packages ship the same primitive (e.g. `Button`, `Text`, `Popover`, | ||
| `Notice`, `Dialog`, `Badge`, `Card`). Use `@wordpress/components` only when there | ||
| is no `@wordpress/ui` equivalent yet (e.g. `Spinner`, `ToggleControl`). | ||
|
|
||
| Message placeholders: | ||
|
|
||
| - `{{ name }}` — imported component or subpath name | ||
| - `{{ source }}` — full import source (e.g. `@automattic/jetpack-components`) | ||
|
|
||
| ## Suppressions | ||
|
|
||
| Existing violations are tracked in [`eslint-excludelist.json`](../eslint-excludelist.json). | ||
|
|
||
| ## Scope | ||
|
|
||
| The rule is disabled inside `projects/js-packages/components/` so the package can | ||
| import its own modules. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Via @keoshi: