Skip to content

Commit 280aa96

Browse files
Add rules - wip
1 parent c860192 commit 280aa96

4 files changed

+354
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict'
2+
3+
const {RuleTester} = require('eslint')
4+
const rule = require('../import-next-to-be-stable-from-root')
5+
6+
const ruleTester = new RuleTester({
7+
parserOptions: {
8+
ecmaVersion: 'latest',
9+
sourceType: 'module',
10+
ecmaFeatures: {
11+
jsx: true,
12+
},
13+
},
14+
})
15+
16+
ruleTester.run('import-next-to-be-stable-from-root', rule, {
17+
valid: [],
18+
invalid: [
19+
// // Single experimental import
20+
{
21+
code: `import {Dialog} from '@primer/react/experimental'`,
22+
output: `import {Dialog} from '@primer/react'`,
23+
errors: [{messageId: 'importToBeStableFromRoot', line: 1}],
24+
},
25+
26+
// Multiple experimental imports
27+
{
28+
code: `import {Dialog, Stack} from '@primer/react/experimental'`,
29+
output: `import {Dialog, Stack} from '@primer/react'`,
30+
errors: [{messageId: 'importToBeStableFromRoot', line: 1}],
31+
},
32+
33+
// // Mix stable and non-stable imports from experimental entrypoint
34+
{
35+
code: `import {SelectPanel, Dialog, Stack} from '@primer/react/experimental'`,
36+
output: `import {Dialog, Stack} from '@primer/react',
37+
import {SelectPanel} from '@primer/react/experimental'`,
38+
errors: [{messageId: 'importToBeStableFromRoot', line: 1}],
39+
},
40+
41+
// Mix stable and non-stable imports from experimental entrypoint with existing stable
42+
{
43+
code: `import {SelectPanel, Dialog, Stack} from '@primer/react/experimental'
44+
import {Button} from '@primer/react'`,
45+
output: `import {Button, Dialog, Stack} from '@primer/react'
46+
import {SelectPanel} from '@primer/react/experimental'`,
47+
errors: [{messageId: 'importToBeStableFromRoot', line: 1}],
48+
},
49+
],
50+
})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict'
2+
3+
const {RuleTester} = require('eslint')
4+
const rule = require('../import-next-to-be-stable-from-root')
5+
6+
const ruleTester = new RuleTester({
7+
parserOptions: {
8+
ecmaVersion: 'latest',
9+
sourceType: 'module',
10+
ecmaFeatures: {
11+
jsx: true,
12+
},
13+
},
14+
})
15+
16+
ruleTester.run('import-next-to-be-stable-from-root', rule, {
17+
valid: [],
18+
invalid: [
19+
// Single next import
20+
{
21+
code: `import {Tooltip} from '@primer/react/next'`,
22+
output: `import {Tooltip} from '@primer/react'`,
23+
errors: [{messageId: 'importToBeStableFromRoot', line: 1}],
24+
},
25+
26+
// // With existing stable entrypoint
27+
{
28+
code: `import {Tooltip} from '@primer/react/next'
29+
import {Button} from '@primer/react'`,
30+
output: `\nimport {Button, Tooltip} from '@primer/react'`,
31+
errors: [{messageId: 'importToBeStableFromRoot', line: 1}],
32+
},
33+
],
34+
})
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
'use strict'
2+
3+
const url = require('../url')
4+
5+
const components = [
6+
{
7+
identifier: 'Dialog',
8+
entrypoint: '@primer/react/experimental',
9+
},
10+
{
11+
identifier: 'DialogProps',
12+
entrypoint: '@primer/react/experimental',
13+
},
14+
{
15+
identifier: 'DialogButtonProps',
16+
entrypoint: '@primer/react/experimental',
17+
},
18+
{
19+
identifier: 'Stack',
20+
entrypoint: '@primer/react/experimental',
21+
},
22+
{
23+
identifier: 'StackProps',
24+
entrypoint: '@primer/react/experimental',
25+
},
26+
{
27+
identifier: 'StackItemProps',
28+
entrypoint: '@primer/react/experimental',
29+
},
30+
]
31+
32+
// Maps entrypoints to a set of component
33+
const entrypoints = new Map()
34+
35+
for (const component of components) {
36+
if (!entrypoints.has(component.entrypoint)) {
37+
entrypoints.set(component.entrypoint, new Set())
38+
}
39+
entrypoints.get(component.entrypoint).add(component.identifier)
40+
}
41+
42+
/**
43+
* @type {import('eslint').Rule.RuleModule}
44+
*/
45+
module.exports = {
46+
meta: {
47+
type: 'problem',
48+
docs: {
49+
description: 'Use stable components from the `@primer/react` entrypoint',
50+
recommended: true,
51+
url: url(module),
52+
},
53+
fixable: true,
54+
schema: [],
55+
messages: {
56+
importToBeStableFromRoot: "Import stable components from '@primer/react' entrypoint",
57+
},
58+
},
59+
create(context) {
60+
const sourceCode = context.getSourceCode()
61+
62+
return {
63+
ImportDeclaration(node) {
64+
if (!entrypoints.has(node.source.value)) {
65+
return
66+
}
67+
68+
const entrypoint = entrypoints.get(node.source.value)
69+
70+
const stableComponents = node.specifiers.filter(specifier => {
71+
return entrypoint.has(specifier.imported.name)
72+
})
73+
74+
if (stableComponents.length === 0) {
75+
return
76+
}
77+
78+
const stableEntrypoint = node.parent.body.find(node => {
79+
if (node.type !== 'ImportDeclaration') {
80+
return false
81+
}
82+
83+
return node.source.value === '@primer/react'
84+
})
85+
86+
// All imports are from stable
87+
if (stableComponents.length === node.specifiers.length) {
88+
context.report({
89+
node,
90+
messageId: 'importToBeStableFromRoot',
91+
*fix(fixer) {
92+
if (stableEntrypoint) {
93+
const lastSpecifier = stableEntrypoint.specifiers[stableEntrypoint.specifiers.length - 1]
94+
yield fixer.remove(node)
95+
yield fixer.insertTextAfter(
96+
lastSpecifier,
97+
`, ${node.specifiers.map(specifier => specifier.imported.name).join(', ')}`,
98+
)
99+
} else {
100+
yield fixer.replaceText(node.source, `'@primer/react'`)
101+
}
102+
},
103+
})
104+
} else {
105+
// There is a mix of stable and non-stable imports
106+
context.report({
107+
node,
108+
messageId: 'importToBeStableFromRoot',
109+
*fix(fixer) {
110+
for (const specifier of stableComponents) {
111+
yield fixer.remove(specifier)
112+
const comma = sourceCode.getTokenAfter(specifier)
113+
if (comma.value === ',') {
114+
yield fixer.remove(comma)
115+
}
116+
}
117+
if (stableEntrypoint) {
118+
const lastSpecifier = stableEntrypoint.specifiers[stableEntrypoint.specifiers.length - 1]
119+
yield fixer.insertTextAfter(
120+
lastSpecifier,
121+
`, ${stableComponents.map(specifier => specifier.imported.name).join(', ')}`,
122+
)
123+
} else {
124+
yield fixer.insertTextAfter(
125+
node,
126+
`\nimport {${stableComponents
127+
.map(specifier => specifier.imported.name)
128+
.join(', ')}} from '@primer/react'`,
129+
)
130+
}
131+
},
132+
})
133+
}
134+
},
135+
}
136+
},
137+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use strict'
2+
3+
const url = require('../url')
4+
5+
const components = [
6+
{
7+
identifier: 'Tooltip',
8+
entrypoint: '@primer/react/next',
9+
},
10+
{
11+
identifier: 'TooltipProps',
12+
entrypoint: '@primer/react/next',
13+
},
14+
{
15+
identifier: 'TooltipDirection',
16+
entrypoint: '@primer/react/next',
17+
},
18+
{
19+
identifier: 'TriggerPropsType',
20+
entrypoint: '@primer/react/next',
21+
},
22+
{
23+
identifier: 'TooltipContext',
24+
entrypoint: '@primer/react/next',
25+
},
26+
]
27+
28+
// Maps entrypoints to a set of component
29+
const entrypoints = new Map()
30+
31+
for (const component of components) {
32+
if (!entrypoints.has(component.entrypoint)) {
33+
entrypoints.set(component.entrypoint, new Set())
34+
}
35+
entrypoints.get(component.entrypoint).add(component.identifier)
36+
}
37+
38+
/**
39+
* @type {import('eslint').Rule.RuleModule}
40+
*/
41+
module.exports = {
42+
meta: {
43+
type: 'problem',
44+
docs: {
45+
description: 'Use stable components from the `@primer/react` entrypoint',
46+
recommended: true,
47+
url: url(module),
48+
},
49+
fixable: true,
50+
schema: [],
51+
messages: {
52+
importToBeStableFromRoot: "Import stable components from '@primer/react' entrypoint",
53+
},
54+
},
55+
create(context) {
56+
const sourceCode = context.getSourceCode()
57+
58+
return {
59+
ImportDeclaration(node) {
60+
if (!entrypoints.has(node.source.value)) {
61+
return
62+
}
63+
64+
const entrypointMapValue = entrypoints.get(node.source.value)
65+
66+
const componentsToPromote = node.specifiers.filter(specifier => {
67+
return entrypointMapValue.has(specifier.imported.name)
68+
})
69+
70+
if (componentsToPromote.length === 0) {
71+
return
72+
}
73+
74+
const stableEntrypoint = node.parent.body.find(node => {
75+
if (node.type !== 'ImportDeclaration') {
76+
return false
77+
}
78+
79+
return node.source.value === '@primer/react'
80+
})
81+
82+
// All imports are from stable
83+
if (componentsToPromote.length === node.specifiers.length) {
84+
context.report({
85+
node,
86+
messageId: 'importToBeStableFromRoot',
87+
*fix(fixer) {
88+
if (stableEntrypoint) {
89+
const lastSpecifier = stableEntrypoint.specifiers[stableEntrypoint.specifiers.length - 1]
90+
yield fixer.remove(node)
91+
yield fixer.insertTextAfter(
92+
lastSpecifier,
93+
`, ${node.specifiers.map(specifier => specifier.imported.name).join(', ')}`,
94+
)
95+
} else {
96+
yield fixer.replaceText(node.source, `'@primer/react'`)
97+
}
98+
},
99+
})
100+
} else {
101+
// There is a mix of deprecated and non-deprecated imports
102+
// context.report({
103+
// node,
104+
// message: 'Import deprecated components from @primer/react/deprecated',
105+
// *fix(fixer) {
106+
// for (const specifier of deprecated) {
107+
// yield fixer.remove(specifier)
108+
// const comma = sourceCode.getTokenAfter(specifier)
109+
// if (comma.value === ',') {
110+
// yield fixer.remove(comma)
111+
// }
112+
// }
113+
// if (deprecatedEntrypoint) {
114+
// const lastSpecifier = deprecatedEntrypoint.specifiers[deprecatedEntrypoint.specifiers.length - 1]
115+
// yield fixer.insertTextAfter(
116+
// lastSpecifier,
117+
// `, ${deprecated.map(specifier => specifier.imported.name).join(', ')}`,
118+
// )
119+
// } else {
120+
// yield fixer.insertTextAfter(
121+
// node,
122+
// `\nimport {${deprecated
123+
// .map(specifier => specifier.imported.name)
124+
// .join(', ')}} from '@primer/react/deprecated'`,
125+
// )
126+
// }
127+
// },
128+
// })
129+
}
130+
},
131+
}
132+
},
133+
}

0 commit comments

Comments
 (0)