@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 ADDED
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ rules: {
3
+ 'rules-of-hooks': require('./rules/rules-of-hooks'),
4
+ 'text-requires-color': require('./rules/text-requires-color'),
5
+ },
6
+ }
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
+ }