eslint-plugin-primer-react 1.0.1-rc.b346b2e → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # eslint-plugin-primer-react
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#39](https://github.com/primer/eslint-plugin-primer-react/pull/39) [`96a675d`](https://github.com/primer/eslint-plugin-primer-react/commit/96a675d8865450d3be556135947a29181a56551c) Thanks [@colebemis](https://github.com/colebemis)! - Add `direct-slot-children` rule
8
+
9
+ ### Minor Changes
10
+
11
+ - [#21](https://github.com/primer/eslint-plugin-primer-react/pull/21) [`5f68eb7`](https://github.com/primer/eslint-plugin-primer-react/commit/5f68eb7e222e2a41c527b5036b1308ff293e7a0d) Thanks [@SferaDev](https://github.com/SferaDev)! - `no-system-props`: Add `skipImportCheck` option
12
+
3
13
  ## 1.0.1
4
14
 
5
15
  ### Patch Changes
package/README.md CHANGED
@@ -29,5 +29,6 @@ ESLint rules for Primer React
29
29
 
30
30
  ## Rules
31
31
 
32
+ - [direct-slot-children](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/direct-slot-children.md)
32
33
  - [no-deprecated-colors](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-deprecated-colors.md)
33
34
  - [no-system-props](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-system-props.md)
@@ -0,0 +1,39 @@
1
+ # Enforce direct parent-child relationship of slot components (direct-slot-children)
2
+
3
+ Some Primer React components use a slots pattern under the hood to render subcomponents in specific places. For example, the `PageLayout` component renders `PageLayout.Header` in the header area, and `PageLayout.Footer` in the footer area. These subcomponents must be direct children of the parent component, and cannot be nested inside other components.
4
+
5
+ ## Rule details
6
+
7
+ This rule enforces that slot components are direct children of their parent component.
8
+
9
+ 👎 Examples of **incorrect** code for this rule:
10
+
11
+ ```jsx
12
+ /* eslint primer-react/direct-slot-children: "error" */
13
+ import {PageLayout} from '@primer/react'
14
+
15
+ const MyHeader = () => <PageLayout.Header>Header</PageLayout.Header>
16
+
17
+ const App = () => (
18
+ <PageLayout>
19
+ <MyHeader />
20
+ </PageLayout>
21
+ )
22
+ ```
23
+
24
+ 👍 Examples of **correct** code for this rule:
25
+
26
+ ```jsx
27
+ /* eslint primer-react/direct-slot-children: "error" */
28
+ import {PageLayout} from '@primer/react'
29
+
30
+ const MyHeader = () => <div>Header</div>
31
+
32
+ const App = () => (
33
+ <PageLayout>
34
+ <PageLayout.Header>
35
+ <MyHeader />
36
+ </PageLayout.Header>
37
+ </PageLayout>
38
+ )
39
+ ```
@@ -1,4 +1,4 @@
1
- # Disallow use of styled-system props (no-system-colors)
1
+ # Disallow use of styled-system props (no-system-props)
2
2
 
3
3
  🔧 The `--fix` option on the [ESLint CLI](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
4
4
 
@@ -41,6 +41,10 @@ import {Avatar} from 'some-other-library'
41
41
 
42
42
  ## Options
43
43
 
44
+ - `skipImportCheck` (default: `false`)
45
+
46
+ By default, the `no-system-props` rule will only check for styled system props used in functions and components that are imported from `@primer/react`. You can disable this behavior by setting `skipImportCheck` to `true`. This is useful for linting custom components that pass styled system props down to Primer React components.
47
+
44
48
  - `includeUtilityComponents` (default: `false`)
45
49
 
46
50
  By default, `Box` and `Text` are excluded because styled system props are not deprecated in our utility components. If you prefer to avoid styled system props there as well for consistency, you can set `includeUtilityComponents` to `true`.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "0.7.4",
3
+ "version": "1.0.1",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "eslint-plugin-primer-react",
9
- "version": "0.7.4",
9
+ "version": "1.0.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@styled-system/props": "^5.1.5",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "1.0.1-rc.b346b2e",
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