kensington-eslint-plugin 0.2.0 → 0.2.2

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": "kensington-eslint-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "ESLint rules for kensington signal correctness",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,15 +1,20 @@
1
- // Reports signal() called inside a computed() callback. Each recompute creates a new
2
- // orphaned signal with no cleanup path declare the signal outside the computed.
1
+ // Reports unkeyed signal() calls inside a computed() callback. Kensington handles the
2
+ // unkeyed case correctly via reconciler-driven node replacement, but local signal state
3
+ // resets to the initial value on every outer re-render and DOM identity is not preserved.
4
+ // Pass a stable key as the second argument (e.g. signal(false, item.id)) to scope the
5
+ // signal to the surrounding computed so the same instance is reused across re-runs.
3
6
  export default {
4
7
  meta: {
5
- type: 'problem',
8
+ type: 'suggestion',
6
9
  docs: {
7
- description: 'disallow creating a new signal() inside a computed() body',
10
+ description: 'require a stable key for signal() calls inside a computed() body',
8
11
  },
9
12
  messages: {
10
13
  noNewSignalInComputed:
11
- 'signal() called inside a computed() body. Each recompute creates a new orphaned signal. ' +
12
- 'Declare the signal outside the computed instead.',
14
+ 'signal() called inside a computed() body without a key. Local state resets on ' +
15
+ 'every outer re-render. Pass a stable key as the second argument ' +
16
+ '(e.g. signal(initial, item.id)) so the same signal instance is reused across ' +
17
+ 'computed re-runs.',
13
18
  },
14
19
  },
15
20
 
@@ -59,6 +64,8 @@ export default {
59
64
  node.callee.type !== 'Identifier' ||
60
65
  !signalNames.has(node.callee.name)
61
66
  ) { return; }
67
+ // A key was supplied — this is the intended pattern, not a problem.
68
+ if (node.arguments.length >= 2) { return; }
62
69
 
63
70
  for (let i = fnStack.length - 1; i >= 0; i--) {
64
71
  if (fnStack[i] === 'computed') {
@@ -31,6 +31,10 @@ export default {
31
31
 
32
32
  ':matches(ArrowFunctionExpression, FunctionExpression)'(node) {
33
33
  const { parent } = node;
34
+ if (parent.type === 'Property') {
35
+ fnStack.push('handler');
36
+ return;
37
+ }
34
38
  if (
35
39
  parent.type === 'CallExpression' &&
36
40
  parent.arguments[0] === node &&
@@ -66,7 +70,7 @@ export default {
66
70
  context.report({ node, messageId: 'noSetInComputed' });
67
71
  return;
68
72
  }
69
- if (fnStack[i] === 'effect') { return; }
73
+ if (fnStack[i] === 'effect' || fnStack[i] === 'handler') { return; }
70
74
  }
71
75
  },
72
76
  };
@@ -31,6 +31,10 @@ export default {
31
31
 
32
32
  ':matches(ArrowFunctionExpression, FunctionExpression)'(node) {
33
33
  const { parent } = node;
34
+ if (parent.type === 'Property') {
35
+ fnStack.push('handler');
36
+ return;
37
+ }
34
38
  if (parent.type === 'CallExpression' && parent.arguments[0] === node) {
35
39
  if (
36
40
  parent.callee.type === 'MemberExpression' &&
@@ -72,7 +76,7 @@ export default {
72
76
  context.report({ node, messageId: 'noSetInTransform' });
73
77
  return;
74
78
  }
75
- if (fnStack[i] === 'effect' || fnStack[i] === 'computed') { return; }
79
+ if (fnStack[i] === 'effect' || fnStack[i] === 'computed' || fnStack[i] === 'handler') { return; }
76
80
  }
77
81
  },
78
82
  };