Skip to content

Commit 5d8f4f3

Browse files
committed
feat: add no-wildcard-imports rule
1 parent e181679 commit 5d8f4f3

File tree

4 files changed

+371
-0
lines changed

4 files changed

+371
-0
lines changed

docs/rules/no-wildcard-imports.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# No Wildcard Imports
2+
3+
## Rule Details
4+
5+
This rule enforces that no wildcard imports are used from `@primer/react`.
6+
7+
👎 Examples of **incorrect** code for this rule
8+
9+
```jsx
10+
import {Dialog} from '@primer/react/lib-esm/Dialog/Dialog'
11+
12+
function ExampleComponent() {
13+
return <Dialog>{/* ... */}</Dialog>
14+
}
15+
```
16+
17+
👍 Examples of **correct** code for this rule:
18+
19+
```jsx
20+
import {Dialog} from '@primer/react/experimental'
21+
22+
function ExampleComponent() {
23+
return <Dialog>{/* ... */}</Dialog>
24+
}
25+
```

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
'a11y-remove-disable-tooltip': require('./rules/a11y-remove-disable-tooltip'),
1212
'a11y-use-next-tooltip': require('./rules/a11y-use-next-tooltip'),
1313
'use-deprecated-from-deprecated': require('./rules/use-deprecated-from-deprecated'),
14+
'no-wildcard-imports': require('./rules/no-wildcard-imports'),
1415
'no-unnecessary-components': require('./rules/no-unnecessary-components'),
1516
'prefer-action-list-item-onselect': require('./rules/prefer-action-list-item-onselect'),
1617
},
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use strict'
2+
3+
const {RuleTester} = require('eslint')
4+
const rule = require('../no-wildcard-imports')
5+
6+
const ruleTester = new RuleTester({
7+
parser: require.resolve('@typescript-eslint/parser'),
8+
parserOptions: {
9+
ecmaVersion: 'latest',
10+
sourceType: 'module',
11+
ecmaFeatures: {
12+
jsx: true,
13+
},
14+
},
15+
})
16+
17+
ruleTester.run('no-wildcard-imports', rule, {
18+
valid: [`import {Button} from '@primer/react'`],
19+
invalid: [
20+
// Test unknown path from wildcard import
21+
{
22+
code: `import type {UnknownImport} from '@primer/react/lib-esm/unknown-path'`,
23+
errors: [
24+
{
25+
message: 'Wildcard imports from @primer/react are not allowed. Import from an entrypoint instead',
26+
},
27+
],
28+
},
29+
30+
// Test type import
31+
{
32+
code: `import type {SxProp} from '@primer/react/lib-esm/sx'`,
33+
output: `import type {SxProp} from '@primer/react'`,
34+
errors: [
35+
{
36+
message: 'Wildcard imports from @primer/react/lib-esm/sx are not allowed. Import from an entrypoint instead',
37+
},
38+
],
39+
},
40+
41+
// Test multiple type imports
42+
{
43+
code: `import type {BetterSystemStyleObject, SxProp, BetterCssProperties} from '@primer/react/lib-esm/sx'`,
44+
output: `import type {BetterSystemStyleObject, SxProp, BetterCssProperties} from '@primer/react'`,
45+
errors: [
46+
{
47+
message: 'Wildcard imports from @primer/react/lib-esm/sx are not allowed. Import from an entrypoint instead',
48+
},
49+
],
50+
},
51+
52+
// Test import alias
53+
{
54+
code: `import type {SxProp as RenamedSxProp} from '@primer/react/lib-esm/sx'`,
55+
output: `import type {SxProp as RenamedSxProp} from '@primer/react'`,
56+
errors: [
57+
{
58+
message: 'Wildcard imports from @primer/react/lib-esm/sx are not allowed. Import from an entrypoint instead',
59+
},
60+
],
61+
},
62+
63+
// Test default import
64+
{
65+
code: `import useIsomorphicLayoutEffect from '@primer/react/lib-esm/useIsomorphicLayoutEffect'`,
66+
output: `import {useIsomorphicLayoutEffect} from '@primer/react'`,
67+
errors: [
68+
{
69+
message:
70+
'Wildcard imports from @primer/react/lib-esm/useIsomorphicLayoutEffect are not allowed. Import from an entrypoint instead',
71+
},
72+
],
73+
},
74+
75+
// Test multiple wildcard imports into single entrypoint
76+
{
77+
code: `import useResizeObserver from '@primer/react/lib-esm/hooks/useResizeObserver'
78+
import useIsomorphicLayoutEffect from '@primer/react/lib-esm/useIsomorphicLayoutEffect'`,
79+
output: `import {useResizeObserver} from '@primer/react'
80+
import {useIsomorphicLayoutEffect} from '@primer/react'`,
81+
errors: [
82+
{
83+
message:
84+
'Wildcard imports from @primer/react/lib-esm/hooks/useResizeObserver are not allowed. Import from an entrypoint instead',
85+
},
86+
{
87+
message:
88+
'Wildcard imports from @primer/react/lib-esm/useIsomorphicLayoutEffect are not allowed. Import from an entrypoint instead',
89+
},
90+
],
91+
},
92+
],
93+
})

src/rules/no-wildcard-imports.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
'use strict'
2+
3+
const url = require('../url')
4+
5+
const wildcardImports = new Map([
6+
[
7+
'@primer/react/lib-esm/sx',
8+
[
9+
{
10+
type: 'type',
11+
name: 'BetterSystemStyleObject',
12+
from: '@primer/react',
13+
},
14+
{
15+
type: 'type',
16+
name: 'SxProp',
17+
from: '@primer/react',
18+
},
19+
{
20+
type: 'type',
21+
name: 'BetterCssProperties',
22+
from: '@primer/react',
23+
},
24+
],
25+
],
26+
[
27+
'@primer/react/lib-esm/useIsomorphicLayoutEffect',
28+
[
29+
{
30+
name: 'default',
31+
from: '@primer/react',
32+
as: 'useIsomorphicLayoutEffect',
33+
},
34+
],
35+
],
36+
[
37+
'@primer/react/lib-esm/Token/IssueLabelToken',
38+
[
39+
{
40+
type: 'type',
41+
name: 'IssueLabelTokenProps',
42+
from: '@primer/react',
43+
},
44+
],
45+
],
46+
[
47+
'@primer/react/lib-esm/deprecated/ActionList',
48+
[
49+
{
50+
type: 'type',
51+
name: 'ItemProps',
52+
from: '@primer/react/deprecated',
53+
},
54+
],
55+
],
56+
[
57+
'@primer/react/lib-esm/deprecated/ActionList/List',
58+
[
59+
{
60+
type: 'type',
61+
name: 'GroupedListProps',
62+
from: '@primer/react/deprecated',
63+
},
64+
{
65+
name: 'ItemInput',
66+
from: '@primer/react/deprecated',
67+
},
68+
],
69+
],
70+
[
71+
'@primer/react/lib-esm/SelectPanel/SelectPanel',
72+
[
73+
{
74+
name: 'SelectPanel',
75+
from: '@primer/react/experimental',
76+
},
77+
{
78+
type: 'type',
79+
name: 'SelectPanelProps',
80+
from: '@primer/react/experimental',
81+
},
82+
],
83+
],
84+
[
85+
'@primer/react/lib-esm/_VisuallyHidden',
86+
[
87+
{
88+
name: 'default',
89+
from: '@primer/react',
90+
as: 'VisuallyHidden',
91+
},
92+
],
93+
],
94+
[
95+
'@primer/react/lib-esm/hooks/useResizeObserver',
96+
[
97+
{
98+
name: 'default',
99+
from: '@primer/react',
100+
as: 'useResizeObserver',
101+
},
102+
],
103+
],
104+
[
105+
'@primer/react/lib-esm/hooks/useProvidedRefOrCreate',
106+
[
107+
{
108+
name: 'default',
109+
from: '@primer/react',
110+
as: 'useProvidedRefOrCreate',
111+
},
112+
],
113+
],
114+
[
115+
'@primer/react/lib-esm/Button/types',
116+
[
117+
{
118+
type: 'type',
119+
name: 'ButtonBaseProps',
120+
from: '@primer/react',
121+
},
122+
{
123+
name: 'ButtonBase',
124+
from: '@primer/react',
125+
},
126+
],
127+
],
128+
[
129+
'@primer/react/lib-esm/utils/polymorphic',
130+
[
131+
{
132+
type: 'type',
133+
name: 'ForwardRefComponent',
134+
from: '@primer/react',
135+
},
136+
],
137+
],
138+
[
139+
'@primer/react/lib-esm/hooks/useResponsiveValue',
140+
[
141+
{
142+
type: 'type',
143+
name: 'useResponsiveValue',
144+
from: '@primer/react',
145+
as: 'useResponsiveValue',
146+
},
147+
],
148+
],
149+
[
150+
'@primer/react/lib-esm/Dialog/Dialog',
151+
[
152+
{
153+
name: 'Dialog',
154+
from: '@primer/react/experimental',
155+
},
156+
],
157+
],
158+
])
159+
160+
/**
161+
* @type {import('eslint').Rule.RuleModule}
162+
*/
163+
module.exports = {
164+
meta: {
165+
type: 'problem',
166+
docs: {
167+
description: 'Wildcard imports are discouraged. Import from a main entrypoint instead',
168+
recommended: true,
169+
url: url(module),
170+
},
171+
fixable: true,
172+
schema: [],
173+
},
174+
create(context) {
175+
return {
176+
ImportDeclaration(node) {
177+
if (!node.source.value.startsWith('@primer/react/lib-esm')) {
178+
return
179+
}
180+
181+
const wildcardImportMigrations = wildcardImports.get(node.source.value)
182+
if (!wildcardImportMigrations) {
183+
context.report({
184+
node,
185+
message: 'Wildcard imports from @primer/react are not allowed. Import from an entrypoint instead',
186+
})
187+
return
188+
}
189+
190+
/**
191+
* Maps entrypoint to array of changes. This tuple contains the new
192+
* imported name from the entrypoint along with the existing local name
193+
* @type {Map<string, Array<[string, string]>>}
194+
*/
195+
const changes = new Map()
196+
197+
for (const specifier of node.specifiers) {
198+
const migration = wildcardImportMigrations.find(migration => {
199+
if (specifier.type === 'ImportDefaultSpecifier') {
200+
return migration.name === 'default'
201+
}
202+
return specifier.imported.name === migration.name
203+
})
204+
205+
// If we do not have a migration, we should report an error even if we
206+
// cannot autofix it
207+
if (!migration) {
208+
context.report({
209+
node,
210+
message: `Wildcard import ${specifier.imported.name} from ${node.source.value} is not allowed. Import from an entrypoint instead`,
211+
})
212+
break
213+
}
214+
215+
if (!changes.has(migration.from)) {
216+
changes.set(migration.from, [])
217+
}
218+
219+
if (migration.as) {
220+
changes.get(migration.from).push([migration.as, migration.as, migration.type])
221+
} else {
222+
changes.get(migration.from).push([migration.name, specifier.local.name, migration.type])
223+
}
224+
}
225+
226+
if (changes.length !== 0) {
227+
context.report({
228+
node,
229+
message: `Wildcard imports from ${node.source.value} are not allowed. Import from an entrypoint instead`,
230+
*fix(fixer) {
231+
for (const [entrypoint, importSpecifiers] of changes) {
232+
const allTypeImports = importSpecifiers.every(([_, __, type]) => type === 'type')
233+
const importStatement = allTypeImports ? 'import type' : 'import'
234+
const specifiers = importSpecifiers
235+
.map(([imported, local, type]) => {
236+
const prefix = allTypeImports ? '' : type === 'type' ? 'type ' : ''
237+
if (imported === local) {
238+
return `${prefix}${imported}`
239+
}
240+
241+
return `${prefix}${imported} as ${local}`
242+
})
243+
.join(', ')
244+
yield fixer.replaceText(node, `${importStatement} {${specifiers}} from '${entrypoint}'`)
245+
}
246+
},
247+
})
248+
}
249+
},
250+
}
251+
},
252+
}

0 commit comments

Comments
 (0)