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.
@@ -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
- }