kensington-eslint-plugin 0.2.2 → 0.3.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/README.md +267 -51
- package/index.js +36 -12
- package/package.json +2 -2
- package/rules/_utils.js +138 -0
- package/rules/attrs-canonical-shape.js +102 -0
- package/rules/attrs-on-call-line.js +82 -0
- package/rules/consistent-content-layout.js +109 -0
- package/rules/{no-set-in-transform.js → no-set-in-derivation.js} +17 -9
- package/rules/no-set-on-derived-signal.js +68 -0
- package/rules/prefer-array-for-multiline-content.js +83 -0
- package/rules/prefer-boolean-attribute-true.js +51 -0
- package/rules/prefer-camelcase-attrs.js +50 -0
- package/rules/prefer-nested-attr-groups.js +86 -0
- package/rules/prefer-style-object.js +60 -0
- package/rules/no-set-in-computed.js +0 -78
- package/rules/no-set-on-computed.js +0 -56
- package/rules/no-set-on-transform.js +0 -48
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
ESLint rules for [kensington](https://github.com/beezwax/kensington) signal correctness.
|
|
4
4
|
|
|
5
|
-
Catches common reactive programming mistakes
|
|
5
|
+
Catches common reactive programming mistakes (read/write loops, writes inside computed derivations, orphaned effects, and async subscription pitfalls) at lint time rather than at runtime.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -34,7 +34,7 @@ export default [
|
|
|
34
34
|
{
|
|
35
35
|
plugins: { kensington },
|
|
36
36
|
rules: {
|
|
37
|
-
'kensington/no-set-in-
|
|
37
|
+
'kensington/no-set-in-derivation': 'error',
|
|
38
38
|
'kensington/no-self-read-write': 'error',
|
|
39
39
|
// ...
|
|
40
40
|
},
|
|
@@ -42,22 +42,54 @@ export default [
|
|
|
42
42
|
];
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
The `style` config is opt-in and bundles the formatting rules at `warn` level:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import kensington from 'kensington-eslint-plugin';
|
|
49
|
+
|
|
50
|
+
export default [
|
|
51
|
+
kensington.configs.recommended,
|
|
52
|
+
kensington.configs.style,
|
|
53
|
+
];
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The formatting rules match calls on `t.<tag>(...)` by default. If your Kensington
|
|
57
|
+
instance is bound to a different name, set it once via plugin-level settings:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import kensington from 'kensington-eslint-plugin';
|
|
61
|
+
|
|
62
|
+
export default [
|
|
63
|
+
kensington.configs.recommended,
|
|
64
|
+
kensington.configs.style,
|
|
65
|
+
{
|
|
66
|
+
settings: { kensington: { objectNames: ['t', 'tag'] } },
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Per-rule options override the shared setting:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
'kensington/prefer-camelcase-attrs': ['warn', { objectNames: ['k'] }],
|
|
75
|
+
```
|
|
76
|
+
|
|
45
77
|
## Editor and tooling support
|
|
46
78
|
|
|
47
|
-
Because this is a standard ESLint plugin, it works anywhere ESLint runs
|
|
79
|
+
Because this is a standard ESLint plugin, it works anywhere ESLint runs with no extra configuration needed.
|
|
48
80
|
|
|
49
|
-
- **Editors
|
|
50
|
-
- **CI
|
|
51
|
-
- **Pre-commit hooks
|
|
52
|
-
- **Programmatic use
|
|
81
|
+
- **Editors.** VS Code, JetBrains IDEs (RubyMine, WebStorm, etc.), Neovim, and any editor with an ESLint language server show inline errors automatically once the plugin is configured.
|
|
82
|
+
- **CI.** Run `eslint --max-warnings 0` in any pipeline to enforce rules on every push.
|
|
83
|
+
- **Pre-commit hooks.** Works with `lint-staged` or any hook runner that invokes ESLint.
|
|
84
|
+
- **Programmatic use.** Available via the ESLint Node.js API (`new ESLint()`) for custom tooling.
|
|
53
85
|
|
|
54
86
|
## Rules
|
|
55
87
|
|
|
56
88
|
| Rule | Description | Recommended |
|
|
57
89
|
|------|-------------|-------------|
|
|
58
|
-
| [`no-set-in-
|
|
90
|
+
| [`no-set-in-derivation`](#no-set-in-derivation) | Disallow `.set()` inside a `computed()` body or `.transform()` callback | error |
|
|
59
91
|
| [`no-self-read-write`](#no-self-read-write) | Disallow reading and writing the same signal in the same reactive run | error |
|
|
60
|
-
| [`no-set-on-
|
|
92
|
+
| [`no-set-on-derived-signal`](#no-set-on-derived-signal) | Disallow `.set()` on a derived (computed or transform) signal | error |
|
|
61
93
|
| [`no-new-signal-in-effect`](#no-new-signal-in-effect) | Disallow creating a new `signal()` inside an `effect()` body | error |
|
|
62
94
|
| [`no-effect-in-computed`](#no-effect-in-computed) | Disallow calling `effect()` inside a `computed()` body | error |
|
|
63
95
|
| [`no-signal-async-write`](#no-signal-async-write) | Disallow writing a signal in an async callback when it was read in the enclosing `effect()` | warn |
|
|
@@ -70,23 +102,35 @@ Because this is a standard ESLint plugin, it works anywhere ESLint runs — no e
|
|
|
70
102
|
| [`no-effect-in-effect`](#no-effect-in-effect) | Disallow creating a new `effect()` inside an `effect()` body | error |
|
|
71
103
|
| [`no-async-effect`](#no-async-effect) | Disallow async callbacks passed to `effect()` | error |
|
|
72
104
|
| [`no-async-computed`](#no-async-computed) | Disallow async callbacks passed to `computed()` | error |
|
|
73
|
-
| [`
|
|
74
|
-
| [`
|
|
105
|
+
| [`prefer-boolean-attribute-true`](#prefer-boolean-attribute-true) | Prefer `true` over `''` for boolean HTML attributes | style |
|
|
106
|
+
| [`prefer-camelcase-attrs`](#prefer-camelcase-attrs) | Prefer camelCase identifier keys over quoted kebab-case | style |
|
|
107
|
+
| [`prefer-style-object`](#prefer-style-object) | Prefer a `style` object over a CSS string | style |
|
|
108
|
+
| [`prefer-nested-attr-groups`](#prefer-nested-attr-groups) | Prefer nested form when attrs share a kebab prefix | style |
|
|
109
|
+
| [`prefer-array-for-multiline-content`](#prefer-array-for-multiline-content) | Require array brackets around multi-line tag content | style |
|
|
110
|
+
| [`attrs-on-call-line`](#attrs-on-call-line) | Attributes object must hug the tag call on both ends | style |
|
|
111
|
+
| [`attrs-canonical-shape`](#attrs-canonical-shape) | Attributes object must be inline or canonically stacked | style |
|
|
112
|
+
| [`consistent-content-layout`](#consistent-content-layout) | Tag content must hug the attrs `}` and the call's `)` | style |
|
|
75
113
|
|
|
76
114
|
---
|
|
77
115
|
|
|
78
|
-
### `no-set-in-
|
|
116
|
+
### `no-set-in-derivation`
|
|
79
117
|
|
|
80
|
-
|
|
118
|
+
Derivations are `computed()` bodies and `.transform()` callbacks. They must be pure. Calling `.set()` inside one causes a write during a read pass.
|
|
81
119
|
|
|
82
120
|
```js
|
|
83
|
-
// Bad
|
|
121
|
+
// Bad. Write inside computed().
|
|
84
122
|
const doubled = computed(() => {
|
|
85
123
|
sideEffect.set(true); // error
|
|
86
124
|
return count.get() * 2;
|
|
87
125
|
});
|
|
88
126
|
|
|
89
|
-
//
|
|
127
|
+
// Bad. Write inside .transform().
|
|
128
|
+
const rows = items.transform(list => {
|
|
129
|
+
selectedId.set(null); // error
|
|
130
|
+
return list.map(item => t.li(item.name));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Good. Move the write into a separate effect.
|
|
90
134
|
effect(() => {
|
|
91
135
|
sideEffect.set(doubled.get() > 10);
|
|
92
136
|
});
|
|
@@ -102,10 +146,10 @@ Reading a signal with `.get()` subscribes to it. Writing it with `.set()` in the
|
|
|
102
146
|
// Bad
|
|
103
147
|
effect(() => {
|
|
104
148
|
const val = count.get();
|
|
105
|
-
count.set(val + 1); // error
|
|
149
|
+
count.set(val + 1); // error. Triggers the effect again.
|
|
106
150
|
});
|
|
107
151
|
|
|
108
|
-
// Good
|
|
152
|
+
// Good. Use .value to read without subscribing.
|
|
109
153
|
effect(() => {
|
|
110
154
|
const val = count.value;
|
|
111
155
|
count.set(val + 1);
|
|
@@ -114,13 +158,18 @@ effect(() => {
|
|
|
114
158
|
|
|
115
159
|
---
|
|
116
160
|
|
|
117
|
-
### `no-set-on-
|
|
161
|
+
### `no-set-on-derived-signal`
|
|
118
162
|
|
|
119
|
-
|
|
163
|
+
Derived signals are those produced by `computed()` or `.transform()`. They are read-only. Kensington throws at runtime if you call `.set()` on one; this catches it statically.
|
|
120
164
|
|
|
121
165
|
```js
|
|
166
|
+
// computed
|
|
122
167
|
const doubled = computed(() => count.get() * 2);
|
|
123
|
-
doubled.set(10); // error
|
|
168
|
+
doubled.set(10); // error. Use signal() for writable state.
|
|
169
|
+
|
|
170
|
+
// transform
|
|
171
|
+
const rows = items.transform(v => v.map(x => x.id));
|
|
172
|
+
rows.set([]); // error. Write to the source signal instead.
|
|
124
173
|
```
|
|
125
174
|
|
|
126
175
|
---
|
|
@@ -132,7 +181,7 @@ Each effect run creates a fresh signal with no cleanup path. The signal should b
|
|
|
132
181
|
```js
|
|
133
182
|
// Bad
|
|
134
183
|
effect(() => {
|
|
135
|
-
const local = signal(0); // error
|
|
184
|
+
const local = signal(0); // error. Orphaned on every run.
|
|
136
185
|
});
|
|
137
186
|
|
|
138
187
|
// Good
|
|
@@ -167,11 +216,11 @@ If a signal is read via `.get()` in an effect and then written in an async callb
|
|
|
167
216
|
effect(() => {
|
|
168
217
|
const val = count.get(); // subscribes
|
|
169
218
|
setTimeout(() => {
|
|
170
|
-
count.set(val + 1); // error
|
|
219
|
+
count.set(val + 1); // error. Re-triggers the effect.
|
|
171
220
|
}, 100);
|
|
172
221
|
});
|
|
173
222
|
|
|
174
|
-
// Good
|
|
223
|
+
// Good. Use .value to read without subscribing.
|
|
175
224
|
effect(() => {
|
|
176
225
|
setTimeout(() => {
|
|
177
226
|
count.set(count.value + 1);
|
|
@@ -188,7 +237,7 @@ effect(() => {
|
|
|
188
237
|
```js
|
|
189
238
|
// Bad
|
|
190
239
|
function setup() {
|
|
191
|
-
effect(() => console.log(count.get())); // warn
|
|
240
|
+
effect(() => console.log(count.get())); // warn. Can't stop it.
|
|
192
241
|
}
|
|
193
242
|
|
|
194
243
|
// Good
|
|
@@ -204,13 +253,13 @@ Module-level effects are intentionally long-lived and are not flagged.
|
|
|
204
253
|
|
|
205
254
|
### `prefer-value-in-async`
|
|
206
255
|
|
|
207
|
-
Once an effect's synchronous body completes, async callbacks run outside its reactive context. `.get()` registers no subscription there
|
|
256
|
+
Once an effect's synchronous body completes, async callbacks run outside its reactive context. `.get()` registers no subscription there. `.value` makes that explicit.
|
|
208
257
|
|
|
209
258
|
```js
|
|
210
259
|
// Bad
|
|
211
260
|
effect(() => {
|
|
212
261
|
fetch('/api').then(() => {
|
|
213
|
-
console.log(count.get()); // warn
|
|
262
|
+
console.log(count.get()); // warn. No subscription is registered.
|
|
214
263
|
});
|
|
215
264
|
});
|
|
216
265
|
|
|
@@ -231,7 +280,7 @@ Creating `computed()` inside an `effect()` creates a new orphaned derived signal
|
|
|
231
280
|
```js
|
|
232
281
|
// Bad
|
|
233
282
|
effect(() => {
|
|
234
|
-
const doubled = computed(() => count.get() * 2); // error
|
|
283
|
+
const doubled = computed(() => count.get() * 2); // error. Orphaned every run.
|
|
235
284
|
console.log(doubled.get());
|
|
236
285
|
});
|
|
237
286
|
|
|
@@ -249,7 +298,7 @@ Creating `signal()` inside `computed()` creates a new orphaned signal on every r
|
|
|
249
298
|
```js
|
|
250
299
|
// Bad
|
|
251
300
|
const c = computed(() => {
|
|
252
|
-
const temp = signal(0); // error
|
|
301
|
+
const temp = signal(0); // error. Orphaned every recompute.
|
|
253
302
|
return temp.get() + base.get();
|
|
254
303
|
});
|
|
255
304
|
|
|
@@ -266,7 +315,7 @@ const c = computed(() => temp.get() + base.get());
|
|
|
266
315
|
|
|
267
316
|
```js
|
|
268
317
|
// Bad
|
|
269
|
-
t.unsafeLiteral(userContent); // error
|
|
318
|
+
t.unsafeLiteral(userContent); // error. Bypasses XSS protection.
|
|
270
319
|
|
|
271
320
|
// Good
|
|
272
321
|
t.literal(userContent);
|
|
@@ -281,7 +330,7 @@ Creating `computed()` inside a `computed()` body creates a new orphaned derived
|
|
|
281
330
|
```js
|
|
282
331
|
// Bad
|
|
283
332
|
const outer = computed(() => {
|
|
284
|
-
const inner = computed(() => count.get() * 2); // error
|
|
333
|
+
const inner = computed(() => count.get() * 2); // error. Orphaned every recompute.
|
|
285
334
|
return inner.get() + 1;
|
|
286
335
|
});
|
|
287
336
|
|
|
@@ -294,16 +343,16 @@ const outer = computed(() => inner.get() + 1);
|
|
|
294
343
|
|
|
295
344
|
### `no-effect-in-effect`
|
|
296
345
|
|
|
297
|
-
Creating `effect()` inside an `effect()` body means every re-run of the outer effect adds a new inner effect without stopping the previous one
|
|
346
|
+
Creating `effect()` inside an `effect()` body means every re-run of the outer effect adds a new inner effect without stopping the previous one. Subscriptions accumulate indefinitely. Capturing the return handle does not fix this; the previous handle would need to be explicitly stopped at the top of each run.
|
|
298
347
|
|
|
299
348
|
```js
|
|
300
349
|
// Bad
|
|
301
350
|
effect(() => {
|
|
302
351
|
const items = list.get();
|
|
303
|
-
effect(() => console.log(items)); // error
|
|
352
|
+
effect(() => console.log(items)); // error. Previous inner effect never stopped.
|
|
304
353
|
});
|
|
305
354
|
|
|
306
|
-
// Good
|
|
355
|
+
// Good. Restructure as a single effect.
|
|
307
356
|
effect(() => {
|
|
308
357
|
console.log(list.get());
|
|
309
358
|
});
|
|
@@ -322,7 +371,7 @@ effect(async () => { // error
|
|
|
322
371
|
title.set(data.title); // runs outside reactive context
|
|
323
372
|
});
|
|
324
373
|
|
|
325
|
-
// Good
|
|
374
|
+
// Good. Keep reactive reads synchronous, push async work into .then().
|
|
326
375
|
effect(() => {
|
|
327
376
|
fetch(`/api/${id.get()}`).then(r => r.json()).then(data => title.set(data.title));
|
|
328
377
|
});
|
|
@@ -335,13 +384,13 @@ effect(() => {
|
|
|
335
384
|
The reactive system runs `computed()` callbacks synchronously. An async callback returns a `Promise` immediately, so the computed value is always a `Promise` object rather than the intended derived value.
|
|
336
385
|
|
|
337
386
|
```js
|
|
338
|
-
// Bad
|
|
387
|
+
// Bad. Computed value is a Promise, not the resolved data.
|
|
339
388
|
const data = computed(async () => { // error
|
|
340
389
|
return await fetch('/api').then(r => r.json());
|
|
341
390
|
});
|
|
342
391
|
t.p(data); // renders "[object Promise]"
|
|
343
392
|
|
|
344
|
-
// Good
|
|
393
|
+
// Good. Signal for the result, effect to populate it.
|
|
345
394
|
const data = signal(null);
|
|
346
395
|
effect(() => {
|
|
347
396
|
fetch('/api').then(r => r.json()).then(v => data.set(v));
|
|
@@ -350,34 +399,201 @@ effect(() => {
|
|
|
350
399
|
|
|
351
400
|
---
|
|
352
401
|
|
|
353
|
-
### `
|
|
402
|
+
### `prefer-boolean-attribute-true`
|
|
354
403
|
|
|
355
|
-
|
|
404
|
+
The HTML spec lists ~30 boolean attributes (`disabled`, `checked`, `hidden`, `selected`, etc.). Kensington treats `true` as "present" and `false`/`null`/`undefined` as "absent". An empty string is a confusing way to spell the same thing.
|
|
356
405
|
|
|
357
406
|
```js
|
|
358
407
|
// Bad
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
408
|
+
t.input({ disabled: '' });
|
|
409
|
+
|
|
410
|
+
// Good
|
|
411
|
+
t.input({ disabled: true });
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Auto-fixable. Extend the recognised set with the `extraBooleanAttrs` option.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
### `prefer-camelcase-attrs`
|
|
419
|
+
|
|
420
|
+
Quoted kebab-case keys (`'aria-label'`, `'data-key'`) work, but the camelCase identifier form is the idiomatic Kensington style and matches what `html-to-kensington` emits.
|
|
421
|
+
|
|
422
|
+
```js
|
|
423
|
+
// Bad
|
|
424
|
+
t.div({ 'aria-label': 'foo', 'data-key': k });
|
|
425
|
+
|
|
426
|
+
// Good
|
|
427
|
+
t.div({ ariaLabel: 'foo', dataKey: k });
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Auto-fixable.
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
### `prefer-style-object`
|
|
435
|
+
|
|
436
|
+
A `style` object lets TypeScript validate CSS property names and lets Kensington skip runtime string parsing. The fixer parses the CSS string and converts each declaration to a camelCase property.
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
// Bad
|
|
440
|
+
t.div({ style: 'background-color: red; z-index: 2' });
|
|
441
|
+
|
|
442
|
+
// Good
|
|
443
|
+
t.div({ style: { backgroundColor: 'red', zIndex: '2' } });
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Auto-fixable. The rule does **not** trigger when any property name fails to round-trip cleanly through Kensington's kebab/camelCase conversion. The two notable cases:
|
|
447
|
+
|
|
448
|
+
- CSS custom properties (`--bg-color`) would require quoted JS keys.
|
|
449
|
+
- Vendor prefixes (`-webkit-appearance`) lose their leading dash when Kensington serialises the object back to CSS.
|
|
450
|
+
|
|
451
|
+
Strings containing template expressions (`style: \`color: ${c}\``) are also skipped.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
### `prefer-nested-attr-groups`
|
|
456
|
+
|
|
457
|
+
When two or more keys share the same kebab prefix (`data-*`, `aria-*`, `hx-*`, etc.), the nested form is shorter and groups related attributes visually.
|
|
458
|
+
|
|
459
|
+
```js
|
|
460
|
+
// Bad
|
|
461
|
+
t.input({ hxGet: '/x', hxTrigger: 'change', hxTarget: '#y' });
|
|
462
|
+
|
|
463
|
+
// Good
|
|
464
|
+
t.input({ hx: { get: '/x', trigger: 'change', target: '#y' } });
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Auto-fixable when the group's members are contiguous in the source. Non-contiguous members and groups whose prefix is already in use by a sibling key are reported but not fixed.
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### `prefer-array-for-multiline-content`
|
|
472
|
+
|
|
473
|
+
Mirrors what `html-to-kensington` emits: when a tag's content can't fit on the same line as the opening paren, it goes in an array, even when it's the only item. The array form makes line-by-line edits easier (no need to add `[ ]` when adding a sibling).
|
|
474
|
+
|
|
475
|
+
```js
|
|
476
|
+
// Bad
|
|
477
|
+
t.div({ class: 'x' },
|
|
478
|
+
t.p('only')
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// Good
|
|
482
|
+
t.div({ class: 'x' }, [
|
|
483
|
+
t.p('only'),
|
|
484
|
+
]);
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Auto-fixable. Single-line calls (`t.div({…}, t.p('inner'))`) are left alone.
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
### `attrs-on-call-line`
|
|
492
|
+
|
|
493
|
+
The attributes object hugs the call on both ends:
|
|
494
|
+
|
|
495
|
+
- the opening `{` sits on the same line as the call's `(`, and
|
|
496
|
+
- the closing `}` sits on the same line as the content (or its `[`) when there is one, or on the same line as the call's `)` when there isn't.
|
|
497
|
+
|
|
498
|
+
```js
|
|
499
|
+
// Bad. { not on call line.
|
|
500
|
+
t.div(
|
|
501
|
+
{ class: 'x' }
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
// Bad. } not on the ) line.
|
|
505
|
+
t.div({
|
|
506
|
+
class: 'x',
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
// Bad. } not on the content's line.
|
|
511
|
+
t.div({
|
|
512
|
+
class: 'x',
|
|
513
|
+
},
|
|
514
|
+
t.p('inner')
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
// Good
|
|
518
|
+
t.div({ class: 'x' });
|
|
519
|
+
t.div({
|
|
520
|
+
class: 'x',
|
|
362
521
|
});
|
|
522
|
+
t.div({
|
|
523
|
+
class: 'x',
|
|
524
|
+
}, [
|
|
525
|
+
t.p('a'),
|
|
526
|
+
]);
|
|
527
|
+
t.div({ class: 'x' }, t.p('inner'));
|
|
528
|
+
```
|
|
363
529
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
530
|
+
Auto-fixable. A comment in the gap blocks the auto-fix for that side; the issue is still reported.
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
### `attrs-canonical-shape`
|
|
535
|
+
|
|
536
|
+
The attributes object is either fully inline or fully stacked with one property per line and the braces on their own lines. Mixed forms are inconsistent and harder to diff.
|
|
537
|
+
|
|
538
|
+
```js
|
|
539
|
+
// Bad. Open brace shares a line with the first prop.
|
|
540
|
+
t.div({ class: 'x',
|
|
541
|
+
id: 'y',
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Bad. Close brace shares a line with the last prop.
|
|
545
|
+
t.div({
|
|
546
|
+
class: 'x',
|
|
547
|
+
id: 'y' });
|
|
548
|
+
|
|
549
|
+
// Bad. Two props on the same line in an otherwise multi-line object.
|
|
550
|
+
t.div({
|
|
551
|
+
class: 'x', id: 'y',
|
|
552
|
+
role: 'button',
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Good
|
|
556
|
+
t.div({ class: 'x', id: 'y' });
|
|
557
|
+
t.div({
|
|
558
|
+
class: 'x',
|
|
559
|
+
id: 'y',
|
|
367
560
|
});
|
|
368
561
|
```
|
|
369
562
|
|
|
563
|
+
Auto-fixable when the object contains no interior comments. The fixer rewrites to the stacked form using the call's leading indentation as the base.
|
|
564
|
+
|
|
370
565
|
---
|
|
371
566
|
|
|
372
|
-
### `
|
|
567
|
+
### `consistent-content-layout`
|
|
373
568
|
|
|
374
|
-
|
|
569
|
+
The content argument (or its opening `[`) hugs the attrs object's `}` (or the call's `(` when there are no attrs), and the content's last token (or `]`) hugs the closing `)`. This is the shape `html-to-kensington` emits and what makes line-by-line diffs clean.
|
|
375
570
|
|
|
376
571
|
```js
|
|
377
|
-
// Bad
|
|
378
|
-
|
|
379
|
-
|
|
572
|
+
// Bad. Content on a separate line from the attrs/call.
|
|
573
|
+
t.div({ class: 'x' },
|
|
574
|
+
t.p('only')
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
// Bad. [ on its own line.
|
|
578
|
+
t.div({ class: 'x' },
|
|
579
|
+
[
|
|
580
|
+
t.p('a'),
|
|
581
|
+
]);
|
|
582
|
+
|
|
583
|
+
// Bad. ] not on the closing-paren line.
|
|
584
|
+
t.div({ class: 'x' }, [
|
|
585
|
+
t.p('a'),
|
|
586
|
+
]
|
|
587
|
+
);
|
|
380
588
|
|
|
381
|
-
// Good
|
|
382
|
-
|
|
589
|
+
// Good
|
|
590
|
+
t.div({ class: 'x' }, t.p('only'));
|
|
591
|
+
t.div({ class: 'x' }, [
|
|
592
|
+
t.p('a'),
|
|
593
|
+
]);
|
|
594
|
+
t.div([
|
|
595
|
+
t.p('a'),
|
|
596
|
+
]);
|
|
383
597
|
```
|
|
598
|
+
|
|
599
|
+
Auto-fixable. A comment between the anchor and the content (or between the content and `)`) suppresses the auto-fix for that side.
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import noSetInDerivation from './rules/no-set-in-derivation.js';
|
|
2
2
|
import noSelfReadWrite from './rules/no-self-read-write.js';
|
|
3
3
|
import noSignalAsyncWrite from './rules/no-signal-async-write.js';
|
|
4
|
-
import
|
|
4
|
+
import noSetOnDerivedSignal from './rules/no-set-on-derived-signal.js';
|
|
5
5
|
import noNewSignalInEffect from './rules/no-new-signal-in-effect.js';
|
|
6
6
|
import noEffectInComputed from './rules/no-effect-in-computed.js';
|
|
7
7
|
import noIgnoredEffectReturn from './rules/no-ignored-effect-return.js';
|
|
@@ -13,16 +13,22 @@ import noNewComputedInComputed from './rules/no-new-computed-in-computed.js';
|
|
|
13
13
|
import noEffectInEffect from './rules/no-effect-in-effect.js';
|
|
14
14
|
import noAsyncEffect from './rules/no-async-effect.js';
|
|
15
15
|
import noAsyncComputed from './rules/no-async-computed.js';
|
|
16
|
-
import
|
|
17
|
-
import
|
|
16
|
+
import preferBooleanAttributeTrue from './rules/prefer-boolean-attribute-true.js';
|
|
17
|
+
import preferCamelcaseAttrs from './rules/prefer-camelcase-attrs.js';
|
|
18
|
+
import preferStyleObject from './rules/prefer-style-object.js';
|
|
19
|
+
import preferNestedAttrGroups from './rules/prefer-nested-attr-groups.js';
|
|
20
|
+
import preferArrayForMultilineContent from './rules/prefer-array-for-multiline-content.js';
|
|
21
|
+
import attrsOnCallLine from './rules/attrs-on-call-line.js';
|
|
22
|
+
import attrsCanonicalShape from './rules/attrs-canonical-shape.js';
|
|
23
|
+
import consistentContentLayout from './rules/consistent-content-layout.js';
|
|
18
24
|
|
|
19
25
|
const plugin = {
|
|
20
26
|
meta: { name: 'eslint-plugin-kensington' },
|
|
21
27
|
rules: {
|
|
22
|
-
'no-set-in-
|
|
28
|
+
'no-set-in-derivation': noSetInDerivation,
|
|
23
29
|
'no-self-read-write': noSelfReadWrite,
|
|
24
30
|
'no-signal-async-write': noSignalAsyncWrite,
|
|
25
|
-
'no-set-on-
|
|
31
|
+
'no-set-on-derived-signal': noSetOnDerivedSignal,
|
|
26
32
|
'no-new-signal-in-effect': noNewSignalInEffect,
|
|
27
33
|
'no-effect-in-computed': noEffectInComputed,
|
|
28
34
|
'no-ignored-effect-return': noIgnoredEffectReturn,
|
|
@@ -34,8 +40,14 @@ const plugin = {
|
|
|
34
40
|
'no-effect-in-effect': noEffectInEffect,
|
|
35
41
|
'no-async-effect': noAsyncEffect,
|
|
36
42
|
'no-async-computed': noAsyncComputed,
|
|
37
|
-
'
|
|
38
|
-
'
|
|
43
|
+
'prefer-boolean-attribute-true': preferBooleanAttributeTrue,
|
|
44
|
+
'prefer-camelcase-attrs': preferCamelcaseAttrs,
|
|
45
|
+
'prefer-style-object': preferStyleObject,
|
|
46
|
+
'prefer-nested-attr-groups': preferNestedAttrGroups,
|
|
47
|
+
'prefer-array-for-multiline-content': preferArrayForMultilineContent,
|
|
48
|
+
'attrs-on-call-line': attrsOnCallLine,
|
|
49
|
+
'attrs-canonical-shape': attrsCanonicalShape,
|
|
50
|
+
'consistent-content-layout': consistentContentLayout,
|
|
39
51
|
},
|
|
40
52
|
configs: {},
|
|
41
53
|
};
|
|
@@ -43,10 +55,10 @@ const plugin = {
|
|
|
43
55
|
plugin.configs.recommended = {
|
|
44
56
|
plugins: { kensington: plugin },
|
|
45
57
|
rules: {
|
|
46
|
-
'kensington/no-set-in-
|
|
58
|
+
'kensington/no-set-in-derivation': 'error',
|
|
47
59
|
'kensington/no-self-read-write': 'error',
|
|
48
60
|
'kensington/no-signal-async-write': 'warn',
|
|
49
|
-
'kensington/no-set-on-
|
|
61
|
+
'kensington/no-set-on-derived-signal': 'error',
|
|
50
62
|
'kensington/no-new-signal-in-effect': 'error',
|
|
51
63
|
'kensington/no-effect-in-computed': 'error',
|
|
52
64
|
'kensington/no-ignored-effect-return': 'warn',
|
|
@@ -58,8 +70,20 @@ plugin.configs.recommended = {
|
|
|
58
70
|
'kensington/no-effect-in-effect': 'error',
|
|
59
71
|
'kensington/no-async-effect': 'error',
|
|
60
72
|
'kensington/no-async-computed': 'error',
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
plugin.configs.style = {
|
|
77
|
+
plugins: { kensington: plugin },
|
|
78
|
+
rules: {
|
|
79
|
+
'kensington/prefer-boolean-attribute-true': 'warn',
|
|
80
|
+
'kensington/prefer-camelcase-attrs': 'warn',
|
|
81
|
+
'kensington/prefer-style-object': 'warn',
|
|
82
|
+
'kensington/prefer-nested-attr-groups': 'warn',
|
|
83
|
+
'kensington/prefer-array-for-multiline-content': 'warn',
|
|
84
|
+
'kensington/attrs-on-call-line': 'warn',
|
|
85
|
+
'kensington/attrs-canonical-shape': 'warn',
|
|
86
|
+
'kensington/consistent-content-layout': 'warn',
|
|
63
87
|
},
|
|
64
88
|
};
|
|
65
89
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kensington-eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "ESLint rules for kensington signal correctness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node --test tests/no-set-in-
|
|
8
|
+
"test": "node --test tests/no-set-in-derivation.test.js tests/no-self-read-write.test.js tests/no-signal-async-write.test.js tests/no-set-on-derived-signal.test.js tests/no-new-signal-in-effect.test.js tests/no-effect-in-computed.test.js tests/no-ignored-effect-return.test.js tests/prefer-value-in-async.test.js tests/no-new-computed-in-effect.test.js tests/no-new-signal-in-computed.test.js tests/no-unsafe-literal.test.js tests/no-new-computed-in-computed.test.js tests/no-effect-in-effect.test.js tests/no-async-effect.test.js tests/no-async-computed.test.js tests/prefer-boolean-attribute-true.test.js tests/prefer-camelcase-attrs.test.js tests/prefer-style-object.test.js tests/prefer-nested-attr-groups.test.js tests/prefer-array-for-multiline-content.test.js tests/attrs-on-call-line.test.js tests/attrs-canonical-shape.test.js tests/consistent-content-layout.test.js"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
11
|
"eslint": ">=9"
|