eslint-plugin-primer-react 1.0.1 → 2.0.0-rc.022b246

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "1.0.1",
3
+ "version": "2.0.0-rc.022b246",
4
4
  "description": "ESLint rules for Primer React",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -29,8 +29,8 @@
29
29
  "jest": "^27.0.6"
30
30
  },
31
31
  "peerDependencies": {
32
- "eslint": "^8.0.1",
33
- "@primer/primitives": ">=4.6.2"
32
+ "@primer/primitives": ">=4.6.2",
33
+ "eslint": "^8.0.1"
34
34
  },
35
35
  "prettier": "@github/prettier-config",
36
36
  "dependencies": {
@@ -0,0 +1,67 @@
1
+ const rule = require('../direct-slot-children')
2
+ const {RuleTester} = require('eslint')
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 'latest',
7
+ sourceType: 'module',
8
+ ecmaFeatures: {
9
+ jsx: true
10
+ }
11
+ }
12
+ })
13
+
14
+ ruleTester.run('direct-slot-children', rule, {
15
+ valid: [
16
+ `import {PageLayout} from '@primer/react'; <PageLayout><PageLayout.Header>Header</PageLayout.Header><PageLayout.Footer>Footer</PageLayout.Footer></PageLayout>`,
17
+ `import {PageLayout} from '@primer/react'; <PageLayout><div><PageLayout.Pane>Header</PageLayout.Pane></div></PageLayout>`,
18
+ `import {PageLayout} from 'some-library'; <PageLayout.Header>Header</PageLayout.Header>`
19
+ ],
20
+ invalid: [
21
+ {
22
+ code: `import {PageLayout} from '@primer/react'; <PageLayout.Header>Header</PageLayout.Header>`,
23
+ errors: [
24
+ {
25
+ messageId: 'directSlotChildren',
26
+ data: {childName: 'PageLayout.Header', parentName: 'PageLayout'}
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ code: `import {PageLayout} from '@primer/react/drafts'; <PageLayout.Header>Header</PageLayout.Header>`,
32
+ errors: [
33
+ {
34
+ messageId: 'directSlotChildren',
35
+ data: {childName: 'PageLayout.Header', parentName: 'PageLayout'}
36
+ }
37
+ ]
38
+ },
39
+ {
40
+ code: `import {PageLayout} from '@primer/react'; <div><PageLayout.Header>Header</PageLayout.Header></div>`,
41
+ errors: [
42
+ {
43
+ messageId: 'directSlotChildren',
44
+ data: {childName: 'PageLayout.Header', parentName: 'PageLayout'}
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ code: `import {PageLayout} from '@primer/react'; <PageLayout><div><PageLayout.Header>Header</PageLayout.Header></div></PageLayout>`,
50
+ errors: [
51
+ {
52
+ messageId: 'directSlotChildren',
53
+ data: {childName: 'PageLayout.Header', parentName: 'PageLayout'}
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ code: `import {TreeView} from '@primer/react'; <TreeView><TreeView.Item><div><TreeView.LeadingVisual>Visual</TreeView.LeadingVisual></div></TreeView.Item></TreeView>`,
59
+ errors: [
60
+ {
61
+ messageId: 'directSlotChildren',
62
+ data: {childName: 'TreeView.LeadingVisual', parentName: 'TreeView.Item'}
63
+ }
64
+ ]
65
+ }
66
+ ]
67
+ })
@@ -0,0 +1,60 @@
1
+ const {isPrimerComponent} = require('../utils/is-primer-component')
2
+
3
+ const slotParentToChildMap = {
4
+ PageLayout: ['PageLayout.Header', 'PageLayout.Footer'],
5
+ FormControl: ['FormControl.Label', 'FormControl.Caption', 'FormControl.LeadingVisual', 'FormControl.TrailingVisual'],
6
+ MarkdownEditor: ['MarkdownEditor.Toolbar', 'MarkdownEditor.Actions', 'MarkdownEditor.Label'],
7
+ 'ActionList.Item': ['ActionList.LeadingVisual', 'ActionList.TrailingVisual', 'ActionList.Description'],
8
+ 'TreeView.Item': ['TreeView.LeadingVisual', 'TreeView.TrailingVisual'],
9
+ RadioGroup: ['RadioGroup.Label', 'RadioGroup.Caption', 'RadioGroup.Validation'],
10
+ CheckboxGroup: ['CheckboxGroup.Label', 'CheckboxGroup.Caption', 'CheckboxGroup.Validation']
11
+ }
12
+
13
+ const slotChildToParentMap = Object.entries(slotParentToChildMap).reduce((acc, [parent, children]) => {
14
+ for (const child of children) {
15
+ acc[child] = parent
16
+ }
17
+ return acc
18
+ }, {})
19
+
20
+ module.exports = {
21
+ meta: {
22
+ type: 'problem',
23
+ schema: [],
24
+ messages: {
25
+ directSlotChildren: '{{childName}} must be a direct child of {{parentName}}.'
26
+ }
27
+ },
28
+ create(context) {
29
+ return {
30
+ JSXOpeningElement(jsxNode) {
31
+ const name = getJSXOpeningElementName(jsxNode)
32
+
33
+ // If component is a Primer component and a slot child,
34
+ // check if it's a direct child of the slot parent
35
+ if (isPrimerComponent(jsxNode.name, context.getScope(jsxNode)) && slotChildToParentMap[name]) {
36
+ const JSXElement = jsxNode.parent
37
+ const parent = JSXElement.parent
38
+
39
+ const expectedParentName = slotChildToParentMap[name]
40
+ if (parent.type !== 'JSXElement' || getJSXOpeningElementName(parent.openingElement) !== expectedParentName) {
41
+ context.report({
42
+ node: jsxNode,
43
+ messageId: 'directSlotChildren',
44
+ data: {childName: name, parentName: expectedParentName}
45
+ })
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ // Convert JSXOpeningElement name to string
54
+ function getJSXOpeningElementName(jsxNode) {
55
+ if (jsxNode.name.type === 'JSXIdentifier') {
56
+ return jsxNode.name.name
57
+ } else if (jsxNode.name.type === 'JSXMemberExpression') {
58
+ return `${jsxNode.name.object.name}.${jsxNode.name.property.name}`
59
+ }
60
+ }
@@ -13,7 +13,16 @@ const utilityComponents = new Set(['Box', 'Text'])
13
13
  // Components for which we allow a set of prop names
14
14
  const excludedComponentProps = new Map([
15
15
  ['AnchoredOverlay', new Set(['width', 'height'])],
16
+ ['Avatar', new Set(['size'])],
17
+ ['AvatarToken', new Set(['size'])],
18
+ ['CircleOcticon', new Set(['size'])],
16
19
  ['Dialog', new Set(['width', 'height'])],
20
+ ['IssueLabelToken', new Set(['size'])],
21
+ ['ProgressBar', new Set(['bg'])],
22
+ ['Spinner', new Set(['size'])],
23
+ ['StyledOcticon', new Set(['size'])],
24
+ ['PointerBox', new Set(['bg'])],
25
+ ['Token', new Set(['size'])],
17
26
  ['PageLayout', new Set(['padding'])],
18
27
  ['ProgressBar', new Set(['bg'])],
19
28
  ['PointerBox', new Set(['bg'])]
@@ -28,6 +37,9 @@ module.exports = {
28
37
  schema: [
29
38
  {
30
39
  properties: {
40
+ skipImportCheck: {
41
+ type: 'boolean'
42
+ },
31
43
  includeUtilityComponents: {
32
44
  type: 'boolean'
33
45
  }
@@ -39,6 +51,10 @@ module.exports = {
39
51
  }
40
52
  },
41
53
  create(context) {
54
+ // If `skipImportCheck` is true, this rule will check for deprecated styled system props
55
+ // used in any components (not just ones that are imported from `@primer/components`).
56
+ const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
57
+
42
58
  const includeUtilityComponents = context.options[0] ? context.options[0].includeUtilityComponents : false
43
59
 
44
60
  const excludedComponents = new Set([
@@ -48,7 +64,7 @@ module.exports = {
48
64
 
49
65
  return {
50
66
  JSXOpeningElement(jsxNode) {
51
- if (!isPrimerComponent(jsxNode.name, context.getScope(jsxNode))) return
67
+ if (!skipImportCheck && !isPrimerComponent(jsxNode.name, context.getScope(jsxNode))) return
52
68
  if (excludedComponents.has(jsxNode.name.name)) return
53
69
 
54
70
  // Create an object mapping from prop name to the AST node for that attribute
@@ -64,7 +80,7 @@ module.exports = {
64
80
  // Create an array of system prop attribute nodes
65
81
  let systemProps = Object.values(pick(propsByNameObject))
66
82
 
67
- let excludedProps = excludedComponentProps.has(jsxNode.name.name)
83
+ const excludedProps = excludedComponentProps.has(jsxNode.name.name)
68
84
  ? new Set([...alwaysExcludedProps, ...excludedComponentProps.get(jsxNode.name.name)])
69
85
  : alwaysExcludedProps
70
86
 
@@ -142,12 +158,12 @@ const excludeSxEntriesFromStyleMap = (stylesMap, sxProp) => {
142
158
  if (
143
159
  !sxProp.value ||
144
160
  sxProp.value.type !== 'JSXExpressionContainer' ||
145
- sxProp.value.expression.type != 'ObjectExpression'
161
+ sxProp.value.expression.type !== 'ObjectExpression'
146
162
  ) {
147
163
  return stylesMap
148
164
  }
149
165
  return new Map(
150
- [...stylesMap].filter(([key, _value]) => {
166
+ [...stylesMap].filter(([key]) => {
151
167
  return !some(sxProp.value.expression.properties, p => p.type === 'Property' && p.key.name === key)
152
168
  })
153
169
  )
@@ -1,6 +1,19 @@
1
1
  const {isImportedFrom} = require('./is-imported-from')
2
2
 
3
- function isPrimerComponent(identifier, scope) {
3
+ function isPrimerComponent(name, scope) {
4
+ let identifier
5
+
6
+ switch (name.type) {
7
+ case 'JSXIdentifier':
8
+ identifier = name
9
+ break
10
+ case 'JSXMemberExpression':
11
+ identifier = name.object
12
+ break
13
+ default:
14
+ return false
15
+ }
16
+
4
17
  return isImportedFrom(/^@primer\/react/, identifier, scope)
5
18
  }
6
19
  exports.isPrimerComponent = isPrimerComponent