@codeleap/eslint-plugin 6.8.0
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/index.js +6 -0
- package/package.json +15 -0
- package/rules/rules-of-hooks.js +94 -0
- package/rules/text-requires-color.js +75 -0
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeleap/eslint-plugin",
|
|
3
|
+
"version": "6.8.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
8
|
+
"type": "git",
|
|
9
|
+
"directory": "packages/eslint-plugin-codeleap"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"eslint": ">=8.0.0",
|
|
13
|
+
"eslint-plugin-react-hooks": ">=7.0.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const reactHooksPlugin = require('eslint-plugin-react-hooks')
|
|
2
|
+
|
|
3
|
+
const originalRule = reactHooksPlugin.rules['rules-of-hooks']
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PORTAL_METHODS = ['content']
|
|
6
|
+
|
|
7
|
+
function isFunction(node) {
|
|
8
|
+
return (
|
|
9
|
+
node.type === 'ArrowFunctionExpression' ||
|
|
10
|
+
node.type === 'FunctionExpression' ||
|
|
11
|
+
node.type === 'FunctionDeclaration'
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getEnclosingFunction(node) {
|
|
16
|
+
let current = node.parent
|
|
17
|
+
while (current) {
|
|
18
|
+
if (isFunction(current)) return current
|
|
19
|
+
current = current.parent
|
|
20
|
+
}
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Returns true when `fn` is the first argument of a `.method(fn)` call
|
|
25
|
+
// where method name is in the configured portal render methods set.
|
|
26
|
+
function isPortalRenderCallback(fn, portalMethods) {
|
|
27
|
+
const parent = fn.parent
|
|
28
|
+
if (!parent || parent.type !== 'CallExpression') return false
|
|
29
|
+
const callee = parent.callee
|
|
30
|
+
if (!callee || callee.type !== 'MemberExpression') return false
|
|
31
|
+
const methodName = callee.property?.name
|
|
32
|
+
if (!portalMethods.has(methodName)) return false
|
|
33
|
+
return parent.arguments[0] === fn
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extends `eslint-plugin-react-hooks/rules-of-hooks` to allow hooks inside
|
|
38
|
+
* portal render callbacks (e.g. `somePortal.content(() => { useMyHook() })`).
|
|
39
|
+
*
|
|
40
|
+
* The upstream rule has no awareness of portal patterns: it sees an anonymous
|
|
41
|
+
* function passed as a method argument and flags any hook call inside as a
|
|
42
|
+
* Rules-of-Hooks violation, even though that callback is always invoked as a
|
|
43
|
+
* React component subtree. This wrapper suppresses those false positives by
|
|
44
|
+
* checking whether the enclosing function is the first argument of a
|
|
45
|
+
* configured portal method call (default: `content`), then delegating
|
|
46
|
+
* everything else to the original rule unchanged.
|
|
47
|
+
*
|
|
48
|
+
* Configure additional method names via the `portalMethods` option when portal
|
|
49
|
+
* APIs in your project use a different render method name.
|
|
50
|
+
*/
|
|
51
|
+
module.exports = {
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'problem',
|
|
54
|
+
docs: originalRule.meta?.docs,
|
|
55
|
+
schema: [
|
|
56
|
+
{
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
portalMethods: {
|
|
60
|
+
type: 'array',
|
|
61
|
+
items: { type: 'string' },
|
|
62
|
+
description:
|
|
63
|
+
'Method names whose first function argument is treated as a React component context',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
create(context) {
|
|
71
|
+
const options = context.options[0] || {}
|
|
72
|
+
const portalMethods = new Set(options.portalMethods ?? DEFAULT_PORTAL_METHODS)
|
|
73
|
+
|
|
74
|
+
const originalReport = context.report.bind(context)
|
|
75
|
+
const wrappedContext = Object.create(context)
|
|
76
|
+
// context.report is non-writable on FileContext — use defineProperty to
|
|
77
|
+
// force an own property that shadows the prototype.
|
|
78
|
+
Object.defineProperty(wrappedContext, 'report', {
|
|
79
|
+
value: function (descriptor) {
|
|
80
|
+
const reportNode = descriptor.node
|
|
81
|
+
if (reportNode) {
|
|
82
|
+
const fn = getEnclosingFunction(reportNode)
|
|
83
|
+
if (fn && isPortalRenderCallback(fn, portalMethods)) return
|
|
84
|
+
}
|
|
85
|
+
originalReport(descriptor)
|
|
86
|
+
},
|
|
87
|
+
writable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
enumerable: true,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return originalRule.create(wrappedContext)
|
|
93
|
+
},
|
|
94
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforces that every `<Text>` JSX element carries a `color:*` design-token
|
|
3
|
+
* variant in its `style` prop.
|
|
4
|
+
*
|
|
5
|
+
* Without this constraint it is easy to render text that inherits or defaults
|
|
6
|
+
* to an implicit colour, making dark-mode and theme-switching bugs hard to
|
|
7
|
+
* catch at review time. The rule catches the most common static forms
|
|
8
|
+
* (string literal, expression string, and all-literal arrays) and deliberately
|
|
9
|
+
* skips dynamic expressions (identifiers, ternaries, calls) that cannot be
|
|
10
|
+
* resolved statically, so it will not produce false positives on computed
|
|
11
|
+
* style values.
|
|
12
|
+
*/
|
|
13
|
+
module.exports = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Text components must include a color variant (color:*) in their style prop',
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
missingColorVariant: 'Text component is missing a color variant. Add a color:* variant to the style prop (e.g. "color:foreground0Default").',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
JSXOpeningElement(node) {
|
|
27
|
+
if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'Text') return
|
|
28
|
+
|
|
29
|
+
const styleProp = node.attributes.find(
|
|
30
|
+
attr => attr.type === 'JSXAttribute' && attr.name?.name === 'style'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if (!styleProp) {
|
|
34
|
+
context.report({ node, messageId: 'missingColorVariant' })
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const value = styleProp.value
|
|
39
|
+
|
|
40
|
+
// style='some string' (bare JSX attribute string)
|
|
41
|
+
if (value?.type === 'Literal') {
|
|
42
|
+
if (typeof value.value === 'string' && value.value.startsWith('color:')) return
|
|
43
|
+
context.report({ node, messageId: 'missingColorVariant' })
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (value?.type !== 'JSXExpressionContainer') return
|
|
48
|
+
|
|
49
|
+
const expr = value.expression
|
|
50
|
+
|
|
51
|
+
// style={'some string'}
|
|
52
|
+
if (expr.type === 'Literal') {
|
|
53
|
+
if (typeof expr.value === 'string' && expr.value.startsWith('color:')) return
|
|
54
|
+
context.report({ node, messageId: 'missingColorVariant' })
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// style={['a', 'b', ...]}
|
|
59
|
+
if (expr.type === 'ArrayExpression') {
|
|
60
|
+
// Any dynamic element — skip, can't determine statically
|
|
61
|
+
const hasDynamic = expr.elements.some(el => el && el.type !== 'Literal')
|
|
62
|
+
if (hasDynamic) return
|
|
63
|
+
|
|
64
|
+
const hasColor = expr.elements.some(
|
|
65
|
+
el => el?.type === 'Literal' && typeof el.value === 'string' && el.value.startsWith('color:')
|
|
66
|
+
)
|
|
67
|
+
if (!hasColor) context.report({ node, messageId: 'missingColorVariant' })
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Any other expression (identifier, member, call, ternary, …) — skip
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}
|