@studiometa/eslint-plugin-js-toolkit 4.0.0-alpha.1 → 4.0.0-alpha.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/README.md CHANGED
@@ -14,6 +14,16 @@ Oxlint/ESLint plugin enforcing best practices for [@studiometa/js-toolkit](https
14
14
  npm install --save-dev @studiometa/eslint-plugin-js-toolkit
15
15
  ```
16
16
 
17
+ ## Agent skills
18
+
19
+ The package ships the `studiometa-eslint-plugin-js-toolkit` [agent skill](https://agentskills.io). The skill teaches coding agents how to use the package, and it matches the installed version. Link the skills of your dependencies into your project with the [`skills` CLI](https://github.com/vercel-labs/skills):
20
+
21
+ ```bash
22
+ npx skills experimental_sync
23
+ ```
24
+
25
+ The command links each skill from `node_modules` into the agent folders (`.agents/skills/`, `.claude/skills/`…). When you update the package, the linked skill updates too.
26
+
17
27
  ## Configuration
18
28
 
19
29
  ### Oxlint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studiometa/eslint-plugin-js-toolkit",
3
- "version": "4.0.0-alpha.1",
3
+ "version": "4.0.0-alpha.2",
4
4
  "description": "Oxlint/ESLint plugin for @studiometa/js-toolkit best practices",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -15,6 +15,7 @@
15
15
  "sideEffects": false,
16
16
  "files": [
17
17
  "dist",
18
+ "skills",
18
19
  "README.md",
19
20
  "LICENSE"
20
21
  ],
@@ -29,7 +30,8 @@
29
30
  }
30
31
  },
31
32
  "scripts": {
32
- "build": "node scripts/build.js",
33
+ "build": "node scripts/build.js && npm run build:skills",
34
+ "build:skills": "node ../../scripts/build-skills.js",
33
35
  "test": "vitest run",
34
36
  "test:watch": "vitest",
35
37
  "lint:types": "tsc -p tsconfig.json"
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: studiometa-eslint-plugin-js-toolkit
3
+ description: Configure and fix the rules of @studiometa/eslint-plugin-js-toolkit, the ESLint and Oxlint plugin for @studiometa/js-toolkit components. Use when a project installs @studiometa/eslint-plugin-js-toolkit, when a lint report shows a js-toolkit/* rule, or when you add the plugin to eslint.config.js or .oxlintrc.json.
4
+ ---
5
+
6
+ # @studiometa/eslint-plugin-js-toolkit
7
+
8
+ The plugin checks that `@studiometa/js-toolkit` components follow the framework conventions: config, refs, options, event handlers, lifecycle and scheduling.
9
+
10
+ This skill matches the installed package version. Every rule, with its description and severity, is in [references/README.md](references/README.md).
11
+
12
+ ## Choose the rule set
13
+
14
+ The plugin has two rule sets. Use the one that matches the installed `@studiometa/js-toolkit` major version:
15
+
16
+ - js-toolkit v4: `jsToolkit.configs.v4`. It replaces `recommended`, it does not extend it.
17
+ - js-toolkit v3: `jsToolkit.configs.recommended`.
18
+
19
+ ```js
20
+ // eslint.config.js
21
+ import { jsToolkit } from '@studiometa/eslint-plugin-js-toolkit';
22
+
23
+ export default [jsToolkit.configs.v4];
24
+ ```
25
+
26
+ With Oxlint, add the plugin under the name `js-toolkit` in `jsPlugins` and copy the rules of the set from `references/README.md`:
27
+
28
+ ```json
29
+ {
30
+ "jsPlugins": [{ "name": "js-toolkit", "specifier": "@studiometa/eslint-plugin-js-toolkit" }]
31
+ }
32
+ ```
33
+
34
+ ## Fix a warning
35
+
36
+ - Read the rule description in `references/README.md` before the change.
37
+ - Fix the code. Do not disable the rule to make the warning go away.
38
+ - Some rules are fixable: run the linter with `--fix` for them.
39
+ - For the correct v4 API, read the `studiometa-js-toolkit` skill.
@@ -0,0 +1,245 @@
1
+ # @studiometa/eslint-plugin-js-toolkit
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@studiometa/eslint-plugin-js-toolkit.svg?style=flat&colorB=3e63dd&colorA=414853)](https://www.npmjs.com/package/@studiometa/eslint-plugin-js-toolkit/)
4
+ [![Downloads](https://img.shields.io/npm/dm/@studiometa/eslint-plugin-js-toolkit?style=flat&colorB=3e63dd&colorA=414853)](https://www.npmjs.com/package/@studiometa/eslint-plugin-js-toolkit/)
5
+ [![Size](https://img.shields.io/bundlephobia/minzip/@studiometa/eslint-plugin-js-toolkit?style=flat&colorB=3e63dd&colorA=414853&label=size)](https://bundlephobia.com/package/@studiometa/js-toolkit)
6
+ [![Dependency Status](https://img.shields.io/librariesio/release/npm/eslint-plugin-js-toolkit?style=flat&colorB=3e63dd&colorA=414853)](https://david-dm.org/studiometa/js-toolkit)
7
+ ![Codecov](https://img.shields.io/codecov/c/github/studiometa/js-toolkit?style=flat&colorB=3e63dd&colorA=414853)
8
+
9
+ Oxlint/ESLint plugin enforcing best practices for [@studiometa/js-toolkit](https://js-toolkit.studiometa.dev).
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install --save-dev @studiometa/eslint-plugin-js-toolkit
15
+ ```
16
+
17
+ ## Agent skills
18
+
19
+ The package ships the `studiometa-eslint-plugin-js-toolkit` [agent skill](https://agentskills.io). The skill teaches coding agents how to use the package, and it matches the installed version. Link the skills of your dependencies into your project with the [`skills` CLI](https://github.com/vercel-labs/skills):
20
+
21
+ ```bash
22
+ npx skills experimental_sync
23
+ ```
24
+
25
+ The command links each skill from `node_modules` into the agent folders (`.agents/skills/`, `.claude/skills/`…). When you update the package, the linked skill updates too.
26
+
27
+ ## Configuration
28
+
29
+ ### Oxlint
30
+
31
+ Add the plugin to your `.oxlintrc.json`:
32
+
33
+ ```json
34
+ {
35
+ "jsPlugins": [{ "name": "js-toolkit", "specifier": "@studiometa/eslint-plugin-js-toolkit" }],
36
+ "rules": {
37
+ "js-toolkit/require-config": "error",
38
+ "js-toolkit/require-config-name-pascal-case": "error",
39
+ "js-toolkit/refs-camel-case": "error",
40
+ "js-toolkit/refs-plural-multiple": "error",
41
+ "js-toolkit/options-camel-case": "error",
42
+ "js-toolkit/async-lifecycle-methods": "error",
43
+ "js-toolkit/on-handler-naming": "error",
44
+ "js-toolkit/on-global-handler-prefix": "warn",
45
+ "js-toolkit/no-deprecated-properties": "warn",
46
+ "js-toolkit/no-dispatch-event": "warn",
47
+ "js-toolkit/no-shadow-dom": "error",
48
+ "js-toolkit/no-create-app": "warn",
49
+ "js-toolkit/no-event-listener-methods": "error",
50
+ "js-toolkit/no-deep-utils-import": "error",
51
+ "js-toolkit/refs-no-bracket-access": "error",
52
+ "js-toolkit/no-redundant-with-mount-when-in-view": "warn",
53
+ "js-toolkit/prefer-ref-over-query-selector": "warn",
54
+ "js-toolkit/require-refs-declared-in-config": "error",
55
+ "js-toolkit/no-manual-intersection-observer": "warn",
56
+ "js-toolkit/no-manual-mutation-observer": "warn",
57
+ "js-toolkit/emits-kebab-case": "error",
58
+ "js-toolkit/emits-multi-word": "error",
59
+ "js-toolkit/components-pascal-case": "error",
60
+ "js-toolkit/require-emit-declared-in-config": "error",
61
+ "js-toolkit/require-children-declared-in-config": "error",
62
+ "js-toolkit/require-options-declared-in-config": "error",
63
+ "js-toolkit/prefer-destructured-lookups": "warn",
64
+ "js-toolkit/no-dollar-prefix": "error",
65
+ "js-toolkit/require-destroyed-cleanup": "warn"
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### v4
71
+
72
+ v4 is not v3 with a bigger version number, so it has its own rule set. `configs.v4` replaces `configs.recommended` rather than extending it: the rules policing something v4 removed — `config.emits`, `$children`, the v3 decorator names — are absent, and `async-lifecycle-methods` is absent because a v4 frame hook returns the function which writes rather than a promise.
73
+
74
+ ```js
75
+ import { jsToolkit } from '@studiometa/eslint-plugin-js-toolkit';
76
+
77
+ export default [jsToolkit.configs.v4];
78
+ ```
79
+
80
+ The same set in `.oxlintrc.json`:
81
+
82
+ ```json
83
+ {
84
+ "jsPlugins": [{ "name": "js-toolkit", "specifier": "@studiometa/eslint-plugin-js-toolkit" }],
85
+ "rules": {
86
+ "js-toolkit/no-write-in-read-phase": "error",
87
+ "js-toolkit/no-options-assignment": "error",
88
+ "js-toolkit/prefer-instance-scheduler": "warn",
89
+ "js-toolkit/option-default-factory": "error",
90
+ "js-toolkit/no-conflicting-negated-option": "error",
91
+ "js-toolkit/no-destroy-lifecycle": "error",
92
+ "js-toolkit/no-deprecated-properties": ["error", { "version": "v4" }],
93
+ "js-toolkit/no-dispatch-event": "warn",
94
+ "js-toolkit/no-shadow-dom": "error",
95
+ "js-toolkit/no-event-listener-methods": "error",
96
+ "js-toolkit/no-manual-mutation-observer": "warn",
97
+ "js-toolkit/refs-no-bracket-access": "error",
98
+ "js-toolkit/prefer-ref-over-query-selector": "warn"
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### ESLint
104
+
105
+ Add the recommended config to your `eslint.config.js` (ESLint v9 flat config):
106
+
107
+ ```js
108
+ import { jsToolkit } from '@studiometa/eslint-plugin-js-toolkit';
109
+
110
+ export default [
111
+ jsToolkit.configs.recommended,
112
+ // ...your other config
113
+ ];
114
+ ```
115
+
116
+ To customise individual rule severities, add an override entry after the recommended config:
117
+
118
+ ```js
119
+ import { jsToolkit } from '@studiometa/eslint-plugin-js-toolkit';
120
+
121
+ export default [
122
+ jsToolkit.configs.recommended,
123
+ {
124
+ rules: {
125
+ 'js-toolkit/no-create-app': 'error', // override the default 'warn'
126
+ },
127
+ },
128
+ ];
129
+ ```
130
+
131
+ ## Rules
132
+
133
+ ### Class structure
134
+
135
+ | Rule | Description | Recommended | Fixable |
136
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------- | ------- |
137
+ | `js-toolkit/require-config` | Requires a `static config` property with a `name` on every class extending `Base`. | error | |
138
+ | `js-toolkit/require-config-name-pascal-case` | Requires `config.name` to be PascalCase. | error | 🔧 |
139
+ | `js-toolkit/refs-camel-case` | Requires ref names in `config.refs` to be camelCase. Supports the `[]` multiple-ref suffix. | error | 🔧 |
140
+ | `js-toolkit/refs-plural-multiple` | Requires refs using the `[]` multiple-ref suffix to be pluralized (e.g. `links[]` not `link[]`). | error | |
141
+ | `js-toolkit/options-camel-case` | Requires option keys in `config.options` to be camelCase. | error | 🔧 |
142
+
143
+ ### Lifecycle methods
144
+
145
+ | Rule | Description | Recommended | Fixable |
146
+ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
147
+ | `js-toolkit/async-lifecycle-methods` | Requires lifecycle methods (`mounted`, `destroyed`, `updated`, `terminated`, `ticked`, `scrolled`, `resized`, `moved`, `loaded`, `keyed`) to be declared `async`. | error | 🔧 |
148
+
149
+ ### Event handlers
150
+
151
+ | Rule | Description | Recommended | Fixable |
152
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------- | ------- |
153
+ | `js-toolkit/on-handler-naming` | Requires event handler methods to follow the `onXxxYyy` camelCase convention. | error | |
154
+ | `js-toolkit/on-global-handler-prefix` | Requires handlers for window-only events (e.g. `resize`) to use the `onWindow` or `onDocument` prefix. | warn | |
155
+
156
+ ### Forbidden patterns
157
+
158
+ | Rule | Description | Recommended | Fixable |
159
+ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
160
+ | `js-toolkit/no-deprecated-properties` | Disallows deprecated properties (`$parent`, `$root`, `$children`). Use `$closest()` or `$query()` instead. With `{ "version": "v4" }`, reports the members v4 removed instead — see below. | warn | |
161
+ | `js-toolkit/no-dispatch-event` | Disallows `dispatchEvent()` inside `Base` subclasses. Use `this.$emit()` instead. | warn | |
162
+ | `js-toolkit/no-shadow-dom` | Disallows `attachShadow()` inside `Base` subclasses. The framework uses Light DOM only. | error | |
163
+ | `js-toolkit/no-create-app` | Disallows `createApp()` (deprecated). Use `registerComponent()` instead. | warn | |
164
+ | `js-toolkit/no-event-listener-methods` | Disallows `addEventListener()` and `removeEventListener()` inside `Base` subclasses. Define `on*` methods instead — the framework handles binding and cleanup automatically. | error | |
165
+ | `js-toolkit/no-deep-utils-import` | Disallows imports reaching into the package layout under `@studiometa/js-toolkit/utils/`. Use the per-symbol subpath `@studiometa/js-toolkit/utils/<name>`, or the `utils` barrel for several. | error | |
166
+ | `js-toolkit/no-internal-barrel-import` | Disallows relative imports of an internal barrel (`./utils/index.js`). Import the module declaring the symbol instead. For libraries published as unbundled ES modules. | off | |
167
+ | `js-toolkit/no-redundant-with-mount-when-in-view` | Disallows wrapping `withMountWhenInView` inside `withScrolledInView` — the latter already includes the former internally. | warn | |
168
+ | `js-toolkit/no-manual-intersection-observer` | Disallows `new IntersectionObserver()` inside `Base` subclasses. Use `withIntersectionObserver` or `withMountWhenInView` decorators instead. | warn | |
169
+ | `js-toolkit/no-manual-mutation-observer` | Disallows `new MutationObserver()` inside `Base` subclasses. Use the `withMutation` decorator instead. | warn | |
170
+
171
+ ### Emits
172
+
173
+ | Rule | Description | Recommended | Fixable |
174
+ | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
175
+ | `js-toolkit/emits-kebab-case` | Requires emit names in `config.emits` to be kebab-case (e.g. `content-change`). | error | 🔧 |
176
+ | `js-toolkit/emits-multi-word` | Requires emit names in `config.emits` to be multi-word to avoid collisions with native DOM events (e.g. `item-click` not `click`). | error | |
177
+ | `js-toolkit/require-emit-declared-in-config` | Requires all `this.$emit('name')` calls to use event names declared in `static config.emits`. | error | |
178
+
179
+ ### Components
180
+
181
+ | Rule | Description | Recommended | Fixable |
182
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------- | ----------- | ------- |
183
+ | `js-toolkit/components-pascal-case` | Requires component keys in `config.components` to be PascalCase. | error | 🔧 |
184
+ | `js-toolkit/require-children-declared-in-config` | Requires all `this.$children.<Name>` accesses to be declared in `static config.components`. | error | |
185
+ | `js-toolkit/require-options-declared-in-config` | Requires all `this.$options.<name>` accesses to be declared in `static config.options`. | error | |
186
+
187
+ ### Refs
188
+
189
+ | Rule | Description | Recommended | Fixable |
190
+ | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
191
+ | `js-toolkit/refs-no-bracket-access` | Disallows bracket access with a `[]` suffix on `this.$refs` (e.g. `this.$refs['items[]']`). Rewrites to dot notation camelCase. | error | 🔧 |
192
+ | `js-toolkit/prefer-ref-over-query-selector` | Warns when `this.$el.querySelector()` or `this.$el.querySelectorAll()` is used inside a `Base` subclass. Declare a ref in `static config` and use `this.$refs` instead. | warn | |
193
+ | `js-toolkit/require-refs-declared-in-config` | Requires all `this.$refs.<name>` accesses to be declared in `static config.refs`. | error | |
194
+
195
+ ### Best practices
196
+
197
+ | Rule | Description | Recommended | Fixable |
198
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------- |
199
+ | `js-toolkit/prefer-destructured-lookups` | Warns when `this.$refs`, `this.$options` or `this.$children` members are accessed more than once in the same method. Destructure into a local variable instead. | warn | |
200
+ | `js-toolkit/no-dollar-prefix` | Disallows user-defined instance methods and properties prefixed with `$` in `Base` subclasses. The `$` prefix is reserved for framework-provided members. | error | |
201
+ | `js-toolkit/require-destroyed-cleanup` | Requires a `destroyed()` method in `Base` subclasses that call `setTimeout`, `setInterval` or `requestAnimationFrame`, to clear timers and prevent memory leaks. | warn | |
202
+
203
+ ### v4
204
+
205
+ These rules describe v4 only, and ship in `configs.v4` rather than in `configs.recommended`.
206
+
207
+ | Rule | Description | v4 | Fixable |
208
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------- |
209
+ | `js-toolkit/no-write-in-read-phase` | Disallows a DOM write in a read-phase callback — a frame hook, a subscriber of a read-phase service, or a `$read()` task. Return a render function, or write from `$write()`. | error | |
210
+ | `js-toolkit/no-options-assignment` | Disallows writing to `this.$options`. Every option is a getter with no setter, so the assignment throws at runtime; this moves the failure to CI. | error | |
211
+ | `js-toolkit/prefer-instance-scheduler` | Warns on `defaultScheduler.read()` / `.write()` inside a component. `this.$read()` and `this.$write()` tie the task to the instance, so destroying it cancels the task. | warn | |
212
+ | `js-toolkit/option-default-factory` | Disallows a literal object or array as an option `default` — every instance would share it. Core warns at runtime, but only once a component mounts. | error | |
213
+ | `js-toolkit/no-conflicting-negated-option` | Disallows declaring both a boolean option `x` and an option named `noX`, which would make `data-option-no-x` mean two things. Core deliberately does not check this on every mount. | error | |
214
+ | `js-toolkit/no-deprecated-properties` | With `{ "version": "v4" }`: reports `$parent`, `$root`, `$children`, `$update`, `$warn`, `$log`, `$terminate`, `$services.enable()`/`.disable()`, `updated()`, `terminated()`, `config.emits`. | error | |
215
+ | `js-toolkit/no-destroy-lifecycle` | Renames `$destroy()` to `$unmount()` and the `destroyed()` hook to `unmounted()` — the v4 names. See below. | error | 🔧 |
216
+
217
+ #### `no-destroy-lifecycle`
218
+
219
+ v4 has no destroyed state. `$mount()` and `$unmount()` are one boolean with two values, and the method that used to be called `$destroy()` documented itself as the reversible inverse of `$mount()`. `$mount()`/`$unmount()` and `mounted()`/`unmounted()` are symmetric pairs where `$mount()`/`$destroy()` was not. Vue 3 made the same rename to `beforeDestroy`/`destroyed`.
220
+
221
+ The rename is not an entry in `no-deprecated-properties`, for three reasons. That rule only flags a member expression whose object is `this`, deliberately, so that `someLibrary.$parent` is not noise — but most `$destroy()` calls are `instance.$destroy()`, so folding the rename in would miss the common case, and lifting the guard for one entry would lift it for all of them. That rule is also not fixable and should not become so: `$parent` → `$closest()` is not a safe textual rewrite, while a rename is exactly the case a fixer fits. And the two rules say different things — `no-deprecated-properties` reports v3 names v4 dropped, whereas `$destroy` is v4's own name, renamed while v4 is unreleased.
222
+
223
+ The two halves are asymmetric, because the two names are.
224
+
225
+ `$destroy` is flagged on **any** receiver — `this.$destroy()`, `instance.$destroy()`, `super.$destroy()` — and on a `$destroy()` method definition, which is what a service mixin overriding the framework's teardown writes. The `$` prefix is the toolkit's own namespace, and the rule ships in `configs.v4` only; that opt-in is the guard against rewriting an unrelated library's API.
226
+
227
+ `destroyed` has no prefix to lean on — `emitter.destroyed`, `record.destroyed` and a plain `destroyed` boolean are ordinary code — so that half is narrowed to the two places where it can only be the hook: a non-static `destroyed()` method definition in a class that reads as a v4 component, and a `super.destroyed()` call inside one. A bare `foo.destroyed()` is left alone.
228
+
229
+ Computed access (`instance['$destroy']()`) is not flagged: it is rare, and the fixer would have to guess the quote style of a string it did not write.
230
+
231
+ #### `no-write-in-read-phase`
232
+
233
+ `useRaf()`, `useScroll()` and `useScrollProgress()` fan their subscribers out inside `defaultScheduler.read()`, so that every measurement of a frame precedes every write of that frame. A callback which measures **and** writes defeats the split: the write forces layout for every component still measuring. The contract is that a callback _returns_ the function which mutates.
234
+
235
+ A read-phase context is a `ticked()`, `scrolled()` or `scrolledInView()` method, a `subscribe()` callback of one of those services or of a `smoothTo()` value, or a `$read()` / `defaultScheduler.read()` task. A DOM write is an assignment to `style.*`, `textContent`, `innerHTML`, `className`, `scrollTop`, `scrollLeft` or `value`, or a call to `setProperty`, `setAttribute` and friends, `classList.*`, `append`/`prepend`/`remove`/`replaceChildren`/`insertAdjacentHTML`, `scrollTo`/`scrollIntoView` or `animate`. The receiver has to reach the DOM from `this.$el`, `this.$refs.*`, `document`, `window` or a local initialised from one of those, which is what makes a rule with no type information accurate enough to turn on.
236
+
237
+ Two mechanisms remove the false positives. The armed state clears at **any** nested function boundary, which is why a returned render function and a `$write()` callback are correct by construction. And a `this.<name>(…)` call is resolved **one** level, into that method, unless it carries `@read` or `@write` — so a helper which only mutates is caught, while `@write render()` is not.
238
+
239
+ #### What counts as a component
240
+
241
+ These rules have to recognise a v4 component with no type information and without leaving the file. A component always extends something — `Base`, a `with<Service>(…)` mixin, or a local abstract component — so a superclass is necessary, and nowhere near sufficient: `class Store extends Error` must not be told to call a `this.$read()` it does not have.
242
+
243
+ So the test is a superclass **plus** a second signal in the same class body: a `@component({ … })` decorator, a `static config`, or use of the framework surface off `this` (`$el`, `$refs`, `$options`, `$emit`, `$read`, `$write`, `$services` and the rest). Lifecycle and service hook names are deliberately excluded from that surface — they are what `no-write-in-read-phase` keys on, so counting them would make the test circular.
244
+
245
+ This keeps a subclass of a local abstract component, whose base is in another file, and it keeps `Base` itself out: `Base` declares `static config` and defines the whole surface, but it extends nothing. A component using none of the three signals anywhere in its body is missed, which is the right way to be wrong — a false negative rather than advice that does not apply.