-
Notifications
You must be signed in to change notification settings - Fork 129
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add component-lifecycle-imports ESLint rule (#2164)
- Loading branch information
1 parent
8cb91e3
commit e1db849
Showing
8 changed files
with
298 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@sumup/eslint-plugin-circuit-ui': major | ||
--- | ||
|
||
Added `circuit-ui/component-lifecycle-imports` rule to update component imports when they move to a different lifecycle stage. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
packages/eslint-plugin-circuit-ui/component-lifecycle-imports/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Import components from the entrypoint for their lifecycle stage (`component-lifecycle-imports`) | ||
|
||
Circuit UI components move through different stages throughout their [lifecycle](https://circuit.sumup.com/?path=/docs/introduction-component-lifecycle--docs). Each stage is associated with its own package entrypoint. | ||
|
||
## Rule Details | ||
|
||
This rule flags components that have moved to a different stage and can automatically update their imports. Setting the rule's error level to `error` (or `2`) is recommended. | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```tsx | ||
import { RangePicker } from '@sumup/circuit-ui'; | ||
import type { RangePickerProps } from '@sumup/circuit-ui'; | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```tsx | ||
import { RangePicker } from '@sumup/circuit-ui/legacy'; | ||
import type { RangePickerProps } from '@sumup/circuit-ui/legacy'; | ||
``` | ||
|
||
### Options | ||
|
||
n/a | ||
|
||
## When Not To Use It | ||
|
||
n/a | ||
|
||
## Further Reading | ||
|
||
- [Component lifecycle documentation](https://circuit.sumup.com/?path=/docs/introduction-component-lifecycle--docs) on the Circuit UI docs |
112 changes: 112 additions & 0 deletions
112
packages/eslint-plugin-circuit-ui/component-lifecycle-imports/index.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/** | ||
* Copyright 2023, SumUp Ltd. | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
// We disable the rule in this file because we explicitly test invalid cases | ||
/* eslint-disable @sumup/circuit-ui/no-invalid-custom-properties */ | ||
|
||
import { ESLintUtils } from '@typescript-eslint/utils'; | ||
|
||
import { componentLifecycleImports } from '.'; | ||
|
||
const ruleTester = new ESLintUtils.RuleTester({ | ||
parser: '@typescript-eslint/parser', | ||
parserOptions: { | ||
ecmaFeatures: { | ||
jsx: true, | ||
}, | ||
}, | ||
}); | ||
|
||
ruleTester.run('component-lifecycle-imports', componentLifecycleImports, { | ||
valid: [ | ||
{ | ||
name: 'matching component import from unrelated package', | ||
code: ` | ||
import { RangePicker } from 'material-ui'; | ||
`, | ||
}, | ||
{ | ||
name: 'matching import from correct package', | ||
code: ` | ||
import { RangePicker } from '@sumup/circuit-ui/legacy'; | ||
`, | ||
}, | ||
{ | ||
name: 'unrelated import from matching package', | ||
code: ` | ||
import { Button } from '@sumup/circuit-ui'; | ||
`, | ||
}, | ||
{ | ||
name: 'unrelated import with matching local name', | ||
code: ` | ||
import { Button as RangePicker } from '@sumup/circuit-ui'; | ||
`, | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
name: 'single import with single match', | ||
code: ` | ||
import { RangePicker } from '@sumup/circuit-ui'; | ||
`, | ||
output: ` | ||
import { RangePicker } from '@sumup/circuit-ui/legacy'; | ||
`, | ||
errors: [{ messageId: 'refactor' }], | ||
}, | ||
{ | ||
name: 'single import with single match with local name', | ||
code: ` | ||
import { RangePicker as RangeInput } from '@sumup/circuit-ui'; | ||
`, | ||
output: ` | ||
import { RangePicker as RangeInput } from '@sumup/circuit-ui/legacy'; | ||
`, | ||
errors: [{ messageId: 'refactor' }], | ||
}, | ||
{ | ||
name: 'multiple imports with single match', | ||
code: ` | ||
import { RangePicker, Button } from '@sumup/circuit-ui'; | ||
`, | ||
output: ` | ||
import { Button } from '@sumup/circuit-ui';import { RangePicker } from '@sumup/circuit-ui/legacy'; | ||
`, | ||
errors: [{ messageId: 'refactor' }], | ||
}, | ||
{ | ||
name: 'multiple imports with multiple matches', | ||
code: ` | ||
import { RangePicker, RangePickerController } from '@sumup/circuit-ui'; | ||
`, | ||
// The other component will be migrated on the second pass. | ||
output: ` | ||
import { RangePickerController } from '@sumup/circuit-ui';import { RangePicker } from '@sumup/circuit-ui/legacy'; | ||
`, | ||
errors: [{ messageId: 'refactor' }, { messageId: 'refactor' }], | ||
}, | ||
{ | ||
name: 'single type import with single match', | ||
code: ` | ||
import type { RangePickerProps } from '@sumup/circuit-ui'; | ||
`, | ||
output: ` | ||
import type { RangePickerProps } from '@sumup/circuit-ui/legacy'; | ||
`, | ||
errors: [{ messageId: 'refactor' }], | ||
}, | ||
], | ||
}); |
143 changes: 143 additions & 0 deletions
143
packages/eslint-plugin-circuit-ui/component-lifecycle-imports/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
/** | ||
* Copyright 2023, SumUp Ltd. | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; | ||
|
||
const createRule = ESLintUtils.RuleCreator( | ||
(name) => | ||
`https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui/${name}`, | ||
); | ||
|
||
const mappings = [ | ||
{ | ||
from: '@sumup/circuit-ui', | ||
to: '@sumup/circuit-ui/legacy', | ||
specifiers: [ | ||
'RangePicker', | ||
'RangePickerProps', | ||
'RangePickerController', | ||
'RangePickerControllerProps', | ||
'SingleDayPicker', | ||
'SingleDayPickerProps', | ||
'CalendarConstants', | ||
'CalendarTag', | ||
'CalendarTagProps', | ||
'CalendarTagTwoStep', | ||
'CalendarTagTwoStepProps', | ||
'Grid', | ||
'Row', | ||
'Col', | ||
'ColProps', | ||
'InlineElements', | ||
'InlineElementsProps', | ||
'Header', | ||
'HeaderProps', | ||
'Sidebar', | ||
'SidebarProps', | ||
'SidebarContextProvider', | ||
'SidebarContextConsumer', | ||
'Tooltip', | ||
'TooltipProps', | ||
'uniqueId', | ||
], | ||
}, | ||
]; | ||
|
||
export const componentLifecycleImports = createRule({ | ||
name: 'component-lifecycle-imports', | ||
meta: { | ||
type: 'suggestion', | ||
schema: [], | ||
fixable: 'code', | ||
docs: { | ||
description: | ||
'Components that have moved to a different stage in their lifecycle should be imported from the relevant path.', | ||
recommended: 'error', | ||
}, | ||
messages: { | ||
refactor: | ||
'`{{name}}` has moved to a different stage in its lifecycle. Import it from `{{source}}` instead.', | ||
}, | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
return mappings.reduce((visitors, config) => { | ||
const { from, to, specifiers } = config; | ||
|
||
return { | ||
...visitors, | ||
[`ImportDeclaration:has(Literal[value="${from}"])`]: ( | ||
node: TSESTree.ImportDeclaration, | ||
) => { | ||
node.specifiers.forEach((specifier) => { | ||
if (specifier.type !== 'ImportSpecifier') { | ||
return; | ||
} | ||
|
||
const importedName = specifier.imported.name; | ||
const localName = specifier.local.name; | ||
|
||
if (!specifiers.includes(importedName)) { | ||
return; | ||
} | ||
|
||
const importStatement = | ||
node.importKind === 'type' ? 'import type' : 'import'; | ||
const importSpecifier = | ||
importedName === localName | ||
? importedName | ||
: `${importedName} as ${localName}`; | ||
|
||
context.report({ | ||
node: specifier, | ||
messageId: 'refactor', | ||
data: { name: importedName, source: to }, | ||
fix(fixer) { | ||
const fixes = []; | ||
|
||
if (node.specifiers.length === 1) { | ||
// Remove the import entirely if there's only one specifier | ||
fixes.push(fixer.remove(node)); | ||
} else { | ||
// ...otherwise, only remove the specifier | ||
fixes.push( | ||
fixer.replaceText( | ||
node, | ||
context | ||
.getSourceCode() | ||
.getText(node) | ||
.replace(importSpecifier, '') | ||
.replace(' ,', ''), | ||
), | ||
); | ||
} | ||
|
||
// Insert the new import | ||
fixes.push( | ||
fixer.insertTextAfter( | ||
node, | ||
`${importStatement} { ${importSpecifier} } from '${to}';`, | ||
), | ||
); | ||
|
||
return fixes; | ||
}, | ||
}); | ||
}); | ||
}, | ||
}; | ||
}, {}); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters