Skip to content

Commit 2efcb37

Browse files
committed
feat(lint): add no-styled-core rule
1 parent 67be111 commit 2efcb37

4 files changed

Lines changed: 205 additions & 0 deletions

File tree

eslint.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ export default typescript.config([
462462
plugins: {'@sentry/scraps': sentryScrapsPlugin},
463463
rules: {
464464
'@sentry/scraps/no-core-import': 'error',
465+
'@sentry/scraps/no-styled-core': 'error',
465466
'@sentry/scraps/no-token-import': 'error',
466467
'@sentry/scraps/use-semantic-token': [
467468
'error',

static/eslint/eslintPluginScraps/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import {noCoreImport} from './no-core-import';
2+
import {noStyledCore} from './no-styled-core';
23
import {noTokenImport} from './no-token-import';
34
import {restrictJsxSlotChildren} from './restrict-jsx-slot-children';
45
import {useSemanticToken} from './use-semantic-token';
56

67
export const rules = {
78
'no-core-import': noCoreImport,
9+
'no-styled-core': noStyledCore,
810
'no-token-import': noTokenImport,
911
'restrict-jsx-slot-children': restrictJsxSlotChildren,
1012
'use-semantic-token': useSemanticToken,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import {RuleTester} from '@typescript-eslint/rule-tester';
2+
3+
import {noStyledCore} from './no-styled-core';
4+
5+
const ruleTester = new RuleTester();
6+
7+
ruleTester.run('no-styled-core', noStyledCore, {
8+
valid: [
9+
{
10+
name: 'styled HTML element is allowed',
11+
code: `
12+
import styled from '@emotion/styled';
13+
const Wrapper = styled('div')\`color: red;\`;
14+
`,
15+
},
16+
{
17+
name: 'styled.element shorthand is allowed',
18+
code: `
19+
import styled from '@emotion/styled';
20+
const Wrapper = styled.div\`color: red;\`;
21+
`,
22+
},
23+
{
24+
name: 'styled with non-scraps component is allowed',
25+
code: `
26+
import styled from '@emotion/styled';
27+
import {SomeComponent} from 'other-lib';
28+
const Wrapper = styled(SomeComponent)\`color: red;\`;
29+
`,
30+
},
31+
{
32+
name: 'styled with local component is allowed',
33+
code: `
34+
import styled from '@emotion/styled';
35+
function MyComponent() { return null; }
36+
const Wrapper = styled(MyComponent)\`color: red;\`;
37+
`,
38+
},
39+
{
40+
name: 'components option filters enforcement',
41+
code: `
42+
import styled from '@emotion/styled';
43+
import {Button} from '@sentry/scraps/button';
44+
const StyledButton = styled(Button)\`color: red;\`;
45+
`,
46+
options: [{components: ['Flex']}],
47+
},
48+
],
49+
invalid: [
50+
{
51+
name: 'styled(Component)`` with scraps import',
52+
code: `
53+
import styled from '@emotion/styled';
54+
import {Button} from '@sentry/scraps/button';
55+
const StyledButton = styled(Button)\`color: red;\`;
56+
`,
57+
errors: [
58+
{
59+
messageId: 'forbidden',
60+
data: {name: 'Button', source: '@sentry/scraps/button'},
61+
},
62+
],
63+
},
64+
{
65+
name: 'styled(Component)({}) object syntax',
66+
code: `
67+
import styled from '@emotion/styled';
68+
import {Flex} from '@sentry/scraps/layout';
69+
const StyledFlex = styled(Flex)({display: 'block'});
70+
`,
71+
errors: [
72+
{
73+
messageId: 'forbidden',
74+
data: {name: 'Flex', source: '@sentry/scraps/layout'},
75+
},
76+
],
77+
},
78+
{
79+
name: 'aliased import is still caught',
80+
code: `
81+
import styled from '@emotion/styled';
82+
import {Button as Btn} from '@sentry/scraps/button';
83+
const StyledBtn = styled(Btn)\`color: red;\`;
84+
`,
85+
errors: [
86+
{
87+
messageId: 'forbidden',
88+
data: {name: 'Btn', source: '@sentry/scraps/button'},
89+
},
90+
],
91+
},
92+
{
93+
name: 'components option matches listed component',
94+
code: `
95+
import styled from '@emotion/styled';
96+
import {Button} from '@sentry/scraps/button';
97+
const StyledButton = styled(Button)\`color: red;\`;
98+
`,
99+
options: [{components: ['Button']}],
100+
errors: [
101+
{
102+
messageId: 'forbidden',
103+
data: {name: 'Button', source: '@sentry/scraps/button'},
104+
},
105+
],
106+
},
107+
{
108+
name: 'multiple scraps components in one file',
109+
code: `
110+
import styled from '@emotion/styled';
111+
import {Text} from '@sentry/scraps/text';
112+
import {Container} from '@sentry/scraps/layout';
113+
const StyledText = styled(Text)\`color: red;\`;
114+
const StyledContainer = styled(Container)\`padding: 8px;\`;
115+
`,
116+
errors: [
117+
{
118+
messageId: 'forbidden',
119+
data: {name: 'Text', source: '@sentry/scraps/text'},
120+
},
121+
{
122+
messageId: 'forbidden',
123+
data: {name: 'Container', source: '@sentry/scraps/layout'},
124+
},
125+
],
126+
},
127+
],
128+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* ESLint rule: no-styled-core
3+
*
4+
* Disallows wrapping @sentry/scraps components with styled().
5+
* Use the component's props API instead.
6+
*/
7+
import type {TSESTree} from '@typescript-eslint/utils';
8+
import {ESLintUtils} from '@typescript-eslint/utils';
9+
10+
import {createImportTracker} from '../ast/tracker/imports';
11+
import {getStyledCallInfo} from '../ast/utils/styled';
12+
13+
interface Options {
14+
components?: string[];
15+
}
16+
17+
export const noStyledCore = ESLintUtils.RuleCreator.withoutDocs<[Options], 'forbidden'>({
18+
meta: {
19+
type: 'problem',
20+
docs: {
21+
description:
22+
'Disallow wrapping @sentry/scraps components with styled(). Use the component props API instead.',
23+
},
24+
schema: [
25+
{
26+
type: 'object',
27+
properties: {
28+
components: {
29+
type: 'array',
30+
items: {type: 'string'},
31+
},
32+
},
33+
additionalProperties: false,
34+
},
35+
],
36+
messages: {
37+
forbidden:
38+
"Do not use `styled()` with core component `{{name}}` from `{{source}}`. Use the component's props API instead.",
39+
},
40+
},
41+
defaultOptions: [{}],
42+
create(context, [options = {}]) {
43+
const importTracker = createImportTracker();
44+
const componentSet = options.components ? new Set(options.components) : null;
45+
46+
function check(node: TSESTree.TaggedTemplateExpression | TSESTree.CallExpression) {
47+
const info = getStyledCallInfo(node);
48+
if (info?.kind !== 'component') {
49+
return;
50+
}
51+
52+
const resolved = importTracker.resolve(info.name);
53+
if (!resolved?.source.startsWith('@sentry/scraps/')) {
54+
return;
55+
}
56+
57+
if (componentSet && !componentSet.has(resolved.imported)) {
58+
return;
59+
}
60+
61+
context.report({
62+
node: info.tag,
63+
messageId: 'forbidden',
64+
data: {name: info.name, source: resolved.source},
65+
});
66+
}
67+
68+
return {
69+
...importTracker.visitors,
70+
TaggedTemplateExpression: check,
71+
CallExpression: check,
72+
};
73+
},
74+
});

0 commit comments

Comments
 (0)