eslint-plugin-primer-react 8.6.1 → 9.0.0-rc.e1d461c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -3
- package/package.json +2 -4
- package/src/configs/recommended.js +2 -2
- package/src/index.js +1 -3
- package/docs/rules/no-system-props.md +0 -67
- package/docs/rules/no-unnecessary-components.md +0 -69
- package/docs/rules/use-styled-react-import.md +0 -175
- package/src/rules/__tests__/no-system-props.test.js +0 -190
- package/src/rules/__tests__/no-unnecessary-components.test.js +0 -240
- package/src/rules/__tests__/use-styled-react-import.test.js +0 -537
- package/src/rules/no-system-props.js +0 -206
- package/src/rules/no-unnecessary-components.js +0 -160
- package/src/rules/use-styled-react-import.js +0 -435
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const url = require('../url')
|
|
4
|
-
const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Format a specifier name, preserving the `type` keyword if present.
|
|
8
|
-
* @param {import('estree').ImportSpecifier} spec
|
|
9
|
-
* @returns {string}
|
|
10
|
-
*/
|
|
11
|
-
function formatSpecifier(spec) {
|
|
12
|
-
return spec.importKind === 'type' ? `type ${spec.imported.name}` : spec.imported.name
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Default components that should be imported from @primer/styled-react when used with sx prop
|
|
16
|
-
const defaultStyledComponents = [
|
|
17
|
-
'ActionList',
|
|
18
|
-
'ActionMenu',
|
|
19
|
-
'Box',
|
|
20
|
-
'Breadcrumbs',
|
|
21
|
-
'Button',
|
|
22
|
-
'Flash',
|
|
23
|
-
'FormControl',
|
|
24
|
-
'Heading',
|
|
25
|
-
'IconButton',
|
|
26
|
-
'Label',
|
|
27
|
-
'Link',
|
|
28
|
-
'LinkButton',
|
|
29
|
-
'PageLayout',
|
|
30
|
-
'Text',
|
|
31
|
-
'TextInput',
|
|
32
|
-
'Truncate',
|
|
33
|
-
'Octicon',
|
|
34
|
-
'Dialog',
|
|
35
|
-
'ThemeProvider',
|
|
36
|
-
'BaseStyles',
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
const componentsToAlwaysImportFromStyledReact = new Set(['ThemeProvider', 'BaseStyles'])
|
|
40
|
-
|
|
41
|
-
// Default types that should be imported from @primer/styled-react
|
|
42
|
-
const defaultStyledTypes = ['BoxProps', 'SxProp', 'BetterSystemStyleObject']
|
|
43
|
-
|
|
44
|
-
// Default utilities that should be imported from @primer/styled-react
|
|
45
|
-
const defaultStyledUtilities = ['sx', 'useTheme']
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @type {import('eslint').Rule.RuleModule}
|
|
49
|
-
*/
|
|
50
|
-
module.exports = {
|
|
51
|
-
meta: {
|
|
52
|
-
type: 'suggestion',
|
|
53
|
-
docs: {
|
|
54
|
-
description: 'Enforce importing components that use sx prop from @primer/styled-react',
|
|
55
|
-
recommended: false,
|
|
56
|
-
url: url(module),
|
|
57
|
-
},
|
|
58
|
-
fixable: 'code',
|
|
59
|
-
schema: [
|
|
60
|
-
{
|
|
61
|
-
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
styledComponents: {
|
|
64
|
-
type: 'array',
|
|
65
|
-
items: {type: 'string'},
|
|
66
|
-
description: 'Components that should be imported from @primer/styled-react when used with sx prop',
|
|
67
|
-
},
|
|
68
|
-
styledTypes: {
|
|
69
|
-
type: 'array',
|
|
70
|
-
items: {type: 'string'},
|
|
71
|
-
description: 'Types that should be imported from @primer/styled-react',
|
|
72
|
-
},
|
|
73
|
-
styledUtilities: {
|
|
74
|
-
type: 'array',
|
|
75
|
-
items: {type: 'string'},
|
|
76
|
-
description: 'Utilities that should be imported from @primer/styled-react',
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
additionalProperties: false,
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
messages: {
|
|
83
|
-
useStyledReactImport: 'Import {{ componentName }} from "@primer/styled-react" when using with sx prop',
|
|
84
|
-
moveToStyledReact: 'Move {{ importName }} import to "@primer/styled-react"',
|
|
85
|
-
usePrimerReactImport: 'Import {{ componentName }} from "@primer/react" when not using sx prop',
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
create(context) {
|
|
89
|
-
// Get configuration options or use defaults
|
|
90
|
-
const options = context.options[0] || {}
|
|
91
|
-
const styledComponents = new Set(options.styledComponents || defaultStyledComponents)
|
|
92
|
-
const styledTypes = new Set(options.styledTypes || defaultStyledTypes)
|
|
93
|
-
const styledUtilities = new Set(options.styledUtilities || defaultStyledUtilities)
|
|
94
|
-
const componentsWithSx = new Set()
|
|
95
|
-
const componentsWithoutSx = new Set() // Track components used without sx
|
|
96
|
-
const allUsedComponents = new Set() // Track all used components
|
|
97
|
-
const primerReactImports = new Map() // Map of component name to import node
|
|
98
|
-
const styledReactImports = new Map() // Map of components imported from styled-react to import node
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
ImportDeclaration(node) {
|
|
102
|
-
const importSource = node.source.value
|
|
103
|
-
|
|
104
|
-
if (importSource === '@primer/react' || importSource.startsWith('@primer/react/')) {
|
|
105
|
-
// Track imports from @primer/react and its subpaths
|
|
106
|
-
for (const specifier of node.specifiers) {
|
|
107
|
-
if (specifier.type === 'ImportSpecifier') {
|
|
108
|
-
const importedName = specifier.imported.name
|
|
109
|
-
if (
|
|
110
|
-
styledComponents.has(importedName) ||
|
|
111
|
-
styledTypes.has(importedName) ||
|
|
112
|
-
styledUtilities.has(importedName)
|
|
113
|
-
) {
|
|
114
|
-
primerReactImports.set(importedName, {node, specifier, importSource})
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} else if (importSource === '@primer/styled-react' || importSource.startsWith('@primer/styled-react/')) {
|
|
119
|
-
// Track what's imported from styled-react and its subpaths
|
|
120
|
-
for (const specifier of node.specifiers) {
|
|
121
|
-
if (specifier.type === 'ImportSpecifier') {
|
|
122
|
-
const importedName = specifier.imported.name
|
|
123
|
-
styledReactImports.set(importedName, {node, specifier, importSource})
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
JSXElement(node) {
|
|
130
|
-
const openingElement = node.openingElement
|
|
131
|
-
const componentName = getJSXOpeningElementName(openingElement)
|
|
132
|
-
|
|
133
|
-
// For compound components like "ActionList.Item", we need to check the parent component
|
|
134
|
-
const parentComponentName = componentName.includes('.') ? componentName.split('.')[0] : componentName
|
|
135
|
-
|
|
136
|
-
// Track all used components that are in our styled components list
|
|
137
|
-
if (styledComponents.has(parentComponentName)) {
|
|
138
|
-
allUsedComponents.add(parentComponentName)
|
|
139
|
-
|
|
140
|
-
// Check if this component has an sx prop
|
|
141
|
-
const hasSxProp = openingElement.attributes.some(
|
|
142
|
-
attr => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'sx',
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
if (hasSxProp) {
|
|
146
|
-
componentsWithSx.add(parentComponentName)
|
|
147
|
-
} else {
|
|
148
|
-
componentsWithoutSx.add(parentComponentName)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
'Program:exit': function () {
|
|
154
|
-
// Group components by import node to handle multiple changes to same import
|
|
155
|
-
const importNodeChanges = new Map()
|
|
156
|
-
|
|
157
|
-
// Collect all changes needed for components used with sx prop
|
|
158
|
-
for (const componentName of componentsWithSx) {
|
|
159
|
-
const importInfo = primerReactImports.get(componentName)
|
|
160
|
-
if (importInfo && !styledReactImports.has(componentName)) {
|
|
161
|
-
const {node: importNode} = importInfo
|
|
162
|
-
|
|
163
|
-
if (!importNodeChanges.has(importNode)) {
|
|
164
|
-
importNodeChanges.set(importNode, {
|
|
165
|
-
toMove: [],
|
|
166
|
-
originalSpecifiers: [...importNode.specifiers],
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
importNodeChanges.get(importNode).toMove.push(componentName)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Report errors for components used with sx prop that are imported from @primer/react
|
|
175
|
-
for (const componentName of componentsWithSx) {
|
|
176
|
-
const importInfo = primerReactImports.get(componentName)
|
|
177
|
-
if (importInfo && !styledReactImports.has(componentName)) {
|
|
178
|
-
context.report({
|
|
179
|
-
node: importInfo.specifier,
|
|
180
|
-
messageId: 'useStyledReactImport',
|
|
181
|
-
data: {componentName},
|
|
182
|
-
fix(fixer) {
|
|
183
|
-
const {node: importNode, importSource} = importInfo
|
|
184
|
-
const changes = importNodeChanges.get(importNode)
|
|
185
|
-
|
|
186
|
-
if (!changes) {
|
|
187
|
-
return null
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Only apply the fix once per import node (for the first component processed)
|
|
191
|
-
const isFirstComponent = changes.toMove[0] === componentName
|
|
192
|
-
|
|
193
|
-
if (!isFirstComponent) {
|
|
194
|
-
return null
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const fixes = []
|
|
198
|
-
const componentsToMove = new Set(changes.toMove)
|
|
199
|
-
|
|
200
|
-
// Find specifiers that remain in original import
|
|
201
|
-
const remainingSpecifiers = changes.originalSpecifiers.filter(spec => {
|
|
202
|
-
const name = spec.imported.name
|
|
203
|
-
return !componentsToMove.has(name)
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
// Convert @primer/react path to @primer/styled-react path
|
|
207
|
-
const styledReactPath = importSource.replace('@primer/react', '@primer/styled-react')
|
|
208
|
-
|
|
209
|
-
// Find the original specifier nodes for moved components to preserve importKind
|
|
210
|
-
const movedSpecifiers = changes.originalSpecifiers.filter(spec =>
|
|
211
|
-
componentsToMove.has(spec.imported.name),
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
// If no components remain, replace with new import directly
|
|
215
|
-
if (remainingSpecifiers.length === 0) {
|
|
216
|
-
const movedComponents = movedSpecifiers.map(formatSpecifier).join(', ')
|
|
217
|
-
fixes.push(fixer.replaceText(importNode, `import { ${movedComponents} } from '${styledReactPath}'`))
|
|
218
|
-
} else {
|
|
219
|
-
// Otherwise, update the import to only include remaining components
|
|
220
|
-
const remainingNames = remainingSpecifiers.map(formatSpecifier)
|
|
221
|
-
fixes.push(
|
|
222
|
-
fixer.replaceText(importNode, `import { ${remainingNames.join(', ')} } from '${importSource}'`),
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
// Add new styled-react import
|
|
226
|
-
const movedComponents = movedSpecifiers.map(formatSpecifier).join(', ')
|
|
227
|
-
fixes.push(
|
|
228
|
-
fixer.insertTextAfter(importNode, `\nimport { ${movedComponents} } from '${styledReactPath}'`),
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return fixes
|
|
233
|
-
},
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Group styled-react imports that need to be moved to primer-react
|
|
239
|
-
const styledReactImportNodeChanges = new Map()
|
|
240
|
-
|
|
241
|
-
// Collect components that need to be moved from styled-react to primer-react
|
|
242
|
-
for (const componentName of allUsedComponents) {
|
|
243
|
-
if (!componentsWithSx.has(componentName) && styledReactImports.has(componentName)) {
|
|
244
|
-
const importInfo = styledReactImports.get(componentName)
|
|
245
|
-
const {node: importNode} = importInfo
|
|
246
|
-
|
|
247
|
-
if (!styledReactImportNodeChanges.has(importNode)) {
|
|
248
|
-
styledReactImportNodeChanges.set(importNode, {
|
|
249
|
-
toMove: [],
|
|
250
|
-
originalSpecifiers: [...importNode.specifiers],
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
styledReactImportNodeChanges.get(importNode).toMove.push(componentName)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Find existing primer-react import nodes to merge with
|
|
259
|
-
const primerReactImportNodes = new Set()
|
|
260
|
-
for (const [, {node}] of primerReactImports) {
|
|
261
|
-
primerReactImportNodes.add(node)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Report errors for components used WITHOUT sx prop that are imported from @primer/styled-react
|
|
265
|
-
for (const componentName of allUsedComponents) {
|
|
266
|
-
// If component is used but NOT with sx prop, and it's imported from styled-react
|
|
267
|
-
if (
|
|
268
|
-
!componentsWithSx.has(componentName) &&
|
|
269
|
-
styledReactImports.has(componentName) &&
|
|
270
|
-
!componentsToAlwaysImportFromStyledReact.has(componentName)
|
|
271
|
-
) {
|
|
272
|
-
const importInfo = styledReactImports.get(componentName)
|
|
273
|
-
context.report({
|
|
274
|
-
node: importInfo.specifier,
|
|
275
|
-
messageId: 'usePrimerReactImport',
|
|
276
|
-
data: {componentName},
|
|
277
|
-
fix(fixer) {
|
|
278
|
-
const {node: importNode, importSource} = importInfo
|
|
279
|
-
const changes = styledReactImportNodeChanges.get(importNode)
|
|
280
|
-
|
|
281
|
-
if (!changes) {
|
|
282
|
-
return null
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Only apply the fix once per import node (for the first component processed)
|
|
286
|
-
const isFirstComponent = changes.toMove[0] === componentName
|
|
287
|
-
|
|
288
|
-
if (!isFirstComponent) {
|
|
289
|
-
return null
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const fixes = []
|
|
293
|
-
const componentsToMove = new Set(changes.toMove)
|
|
294
|
-
|
|
295
|
-
// Find specifiers that remain in styled-react import
|
|
296
|
-
const remainingSpecifiers = changes.originalSpecifiers.filter(spec => {
|
|
297
|
-
const name = spec.imported.name
|
|
298
|
-
return !componentsToMove.has(name)
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
// Convert @primer/styled-react path to @primer/react path
|
|
302
|
-
const primerReactPath = importSource.replace('@primer/styled-react', '@primer/react')
|
|
303
|
-
|
|
304
|
-
// Check if there's an existing primer-react import to merge with
|
|
305
|
-
const existingPrimerReactImport = Array.from(primerReactImportNodes)[0]
|
|
306
|
-
|
|
307
|
-
// Find the original specifier nodes for moved components to preserve importKind
|
|
308
|
-
const movedSpecifiers = changes.originalSpecifiers.filter(spec =>
|
|
309
|
-
componentsToMove.has(spec.imported.name),
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
if (existingPrimerReactImport && remainingSpecifiers.length === 0) {
|
|
313
|
-
// Case: No remaining styled-react imports, merge with existing primer-react import
|
|
314
|
-
const existingNames = existingPrimerReactImport.specifiers.map(formatSpecifier)
|
|
315
|
-
const movedNames = movedSpecifiers.map(formatSpecifier)
|
|
316
|
-
const newSpecifiers = [...existingNames, ...movedNames].filter(
|
|
317
|
-
(name, index, arr) => arr.indexOf(name) === index,
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
fixes.push(
|
|
321
|
-
fixer.replaceText(
|
|
322
|
-
existingPrimerReactImport,
|
|
323
|
-
`import { ${newSpecifiers.join(', ')} } from '${primerReactPath}'`,
|
|
324
|
-
),
|
|
325
|
-
)
|
|
326
|
-
fixes.push(fixer.remove(importNode))
|
|
327
|
-
} else if (existingPrimerReactImport && remainingSpecifiers.length > 0) {
|
|
328
|
-
// Case: Some styled-react imports remain, merge moved components with existing primer-react
|
|
329
|
-
const existingNames = existingPrimerReactImport.specifiers.map(formatSpecifier)
|
|
330
|
-
const movedNames = movedSpecifiers.map(formatSpecifier)
|
|
331
|
-
const newSpecifiers = [...existingNames, ...movedNames].filter(
|
|
332
|
-
(name, index, arr) => arr.indexOf(name) === index,
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
fixes.push(
|
|
336
|
-
fixer.replaceText(
|
|
337
|
-
existingPrimerReactImport,
|
|
338
|
-
`import { ${newSpecifiers.join(', ')} } from '${primerReactPath}'`,
|
|
339
|
-
),
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
const remainingNames = remainingSpecifiers.map(formatSpecifier)
|
|
343
|
-
fixes.push(
|
|
344
|
-
fixer.replaceText(importNode, `import { ${remainingNames.join(', ')} } from '${importSource}'`),
|
|
345
|
-
)
|
|
346
|
-
} else if (remainingSpecifiers.length === 0) {
|
|
347
|
-
// Case: No existing primer-react import, no remaining styled-react imports
|
|
348
|
-
const movedComponents = movedSpecifiers.map(formatSpecifier).join(', ')
|
|
349
|
-
fixes.push(fixer.replaceText(importNode, `import { ${movedComponents} } from '${primerReactPath}'`))
|
|
350
|
-
} else {
|
|
351
|
-
// Case: No existing primer-react import, some styled-react imports remain
|
|
352
|
-
const remainingNames = remainingSpecifiers.map(formatSpecifier)
|
|
353
|
-
fixes.push(
|
|
354
|
-
fixer.replaceText(importNode, `import { ${remainingNames.join(', ')} } from '${importSource}'`),
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
const movedComponents = movedSpecifiers.map(formatSpecifier).join(', ')
|
|
358
|
-
fixes.push(
|
|
359
|
-
fixer.insertTextAfter(importNode, `\nimport { ${movedComponents} } from '${primerReactPath}'`),
|
|
360
|
-
)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return fixes
|
|
364
|
-
},
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Also report for types, utilities and components that should always be from styled-react
|
|
370
|
-
for (const [importName, importInfo] of primerReactImports) {
|
|
371
|
-
if (
|
|
372
|
-
(styledTypes.has(importName) ||
|
|
373
|
-
styledUtilities.has(importName) ||
|
|
374
|
-
componentsToAlwaysImportFromStyledReact.has(importName)) &&
|
|
375
|
-
!styledReactImports.has(importName)
|
|
376
|
-
) {
|
|
377
|
-
context.report({
|
|
378
|
-
node: importInfo.specifier,
|
|
379
|
-
messageId: 'moveToStyledReact',
|
|
380
|
-
data: {importName},
|
|
381
|
-
fix(fixer) {
|
|
382
|
-
const {node: importNode, specifier, importSource} = importInfo
|
|
383
|
-
|
|
384
|
-
const fixes = []
|
|
385
|
-
|
|
386
|
-
// we consolidate all the fixes for the import in the first specifier
|
|
387
|
-
const isFirst = importNode.specifiers[0] === specifier
|
|
388
|
-
if (!isFirst) return null
|
|
389
|
-
|
|
390
|
-
const specifiersToMove = importNode.specifiers.filter(specifier => {
|
|
391
|
-
const name = specifier.imported.name
|
|
392
|
-
return (
|
|
393
|
-
styledUtilities.has(name) ||
|
|
394
|
-
styledTypes.has(name) ||
|
|
395
|
-
componentsToAlwaysImportFromStyledReact.has(name)
|
|
396
|
-
)
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
const remainingSpecifiers = importNode.specifiers.filter(specifier => {
|
|
400
|
-
return !specifiersToMove.includes(specifier)
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
// Convert @primer/react path to @primer/styled-react path
|
|
404
|
-
const styledReactPath = importSource.replace('@primer/react', '@primer/styled-react')
|
|
405
|
-
|
|
406
|
-
if (remainingSpecifiers.length === 0) {
|
|
407
|
-
// if there are no remaining specifiers, we can remove the whole import
|
|
408
|
-
fixes.push(fixer.remove(importNode))
|
|
409
|
-
} else {
|
|
410
|
-
const remainingNames = remainingSpecifiers.map(formatSpecifier)
|
|
411
|
-
fixes.push(
|
|
412
|
-
fixer.replaceText(importNode, `import { ${remainingNames.join(', ')} } from '${importSource}'`),
|
|
413
|
-
)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (specifiersToMove.length > 0) {
|
|
417
|
-
const movedComponents = specifiersToMove.map(formatSpecifier)
|
|
418
|
-
const onNewLine = remainingSpecifiers.length > 0
|
|
419
|
-
fixes.push(
|
|
420
|
-
fixer.insertTextAfter(
|
|
421
|
-
importNode,
|
|
422
|
-
`${onNewLine ? '\n' : ''}import { ${movedComponents.join(', ')} } from '${styledReactPath}'`,
|
|
423
|
-
),
|
|
424
|
-
)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return fixes
|
|
428
|
-
},
|
|
429
|
-
})
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
},
|
|
433
|
-
}
|
|
434
|
-
},
|
|
435
|
-
}
|