@studiometa/eslint-plugin-ui 0.1.0 → 1.9.0-beta.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 ADDED
@@ -0,0 +1,158 @@
1
+ # @studiometa/eslint-plugin-ui
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@studiometa/eslint-plugin-ui.svg?style=flat&colorB=3e63dd&colorA=414853)](https://www.npmjs.com/package/@studiometa/eslint-plugin-ui/)
4
+ [![Downloads](https://img.shields.io/npm/dm/@studiometa/eslint-plugin-ui?style=flat&colorB=3e63dd&colorA=414853)](https://www.npmjs.com/package/@studiometa/eslint-plugin-ui/)
5
+ [![Size](https://img.shields.io/bundlephobia/minzip/@studiometa/eslint-plugin-ui?style=flat&colorB=3e63dd&colorA=414853&label=size)](https://bundlephobia.com/package/@studiometa/eslint-plugin-ui)
6
+ ![Codecov](https://img.shields.io/codecov/c/github/studiometa/ui?style=flat&colorB=3e63dd&colorA=414853)
7
+
8
+ ESLint plugin to help developers discover and use components from [@studiometa/ui](https://ui.studiometa.dev).
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install --save-dev @studiometa/eslint-plugin-ui
14
+ ```
15
+
16
+ ## Configuration
17
+
18
+ ### ESLint
19
+
20
+ Add the recommended config to your `eslint.config.js` (ESLint v9 flat config):
21
+
22
+ ```js
23
+ import { ui } from '@studiometa/eslint-plugin-ui';
24
+
25
+ export default [
26
+ ui.configs.recommended,
27
+ // ...your other config
28
+ ];
29
+ ```
30
+
31
+ To customise individual rule severities, add an override entry after the recommended config:
32
+
33
+ ```js
34
+ import { ui } from '@studiometa/eslint-plugin-ui';
35
+
36
+ export default [
37
+ ui.configs.recommended,
38
+ {
39
+ rules: {
40
+ 'ui/prefer-ui-component': 'error',
41
+ },
42
+ },
43
+ ];
44
+ ```
45
+
46
+ ### Oxlint
47
+
48
+ Add the plugin to your `.oxlintrc.json` using the `"ui"` name to get the `ui/` rule prefix:
49
+
50
+ ```json
51
+ {
52
+ "jsPlugins": [{ "name": "ui", "specifier": "@studiometa/eslint-plugin-ui" }],
53
+ "rules": {
54
+ "ui/prefer-ui-component": "warn",
55
+ "ui/prefer-transition": "warn",
56
+ "ui/no-manual-fetch": "warn",
57
+ "ui/prefer-data-model": "warn",
58
+ "ui/prefer-action": "warn"
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Rules
64
+
65
+ | Rule | Description | Recommended |
66
+ | ---- | ----------- | ----------- |
67
+ | `ui/prefer-ui-component` | Warn when a class named after a `@studiometa/ui` component extends `Base` directly instead of importing from the library | warn |
68
+ | `ui/prefer-transition` | Warn when a `Base` subclass manually implements `open()` and `close()` — suggest `Transition` | warn |
69
+ | `ui/no-manual-fetch` | Warn when a `Base` subclass combines `fetch()` with DOM injection — suggest the `Fetch` component | warn |
70
+ | `ui/prefer-data-model` | Warn when a `Base` subclass manually syncs input values to the DOM — suggest `DataModel`/`DataEffect` | warn |
71
+ | `ui/prefer-action` | Warn when a `Base` subclass only defines a single simple event handler — suggest `Action` | warn |
72
+
73
+ ### `ui/prefer-ui-component`
74
+
75
+ Detects classes whose name matches a component exported by `@studiometa/ui` (e.g. `Menu`, `Accordion`, `Modal`) that extend `Base` directly. Suggests importing and extending the existing component instead.
76
+
77
+ ```js
78
+ // ❌ Incorrect
79
+ import { Base } from '@studiometa/js-toolkit';
80
+ class Menu extends Base { }
81
+
82
+ // ✅ Correct
83
+ import { Menu } from '@studiometa/ui';
84
+ class MyMenu extends Menu { }
85
+ ```
86
+
87
+ ### `ui/prefer-transition`
88
+
89
+ Detects `Base` subclasses that manually implement both `open()` and `close()` methods. This pattern is already handled by the `Transition` component.
90
+
91
+ ```js
92
+ // ❌ Incorrect
93
+ import { Base } from '@studiometa/js-toolkit';
94
+ class Drawer extends Base {
95
+ open() { this.$el.classList.add('is-open'); }
96
+ close() { this.$el.classList.remove('is-open'); }
97
+ }
98
+
99
+ // ✅ Correct
100
+ import { Transition } from '@studiometa/ui';
101
+ class Drawer extends Transition { }
102
+ ```
103
+
104
+ ### `ui/no-manual-fetch`
105
+
106
+ Detects `Base` subclasses that combine a `fetch()` call with DOM injection via `innerHTML` or `insertAdjacentHTML`. The `Fetch` component handles this declaratively.
107
+
108
+ ```js
109
+ // ❌ Incorrect
110
+ import { Base } from '@studiometa/js-toolkit';
111
+ class ProductList extends Base {
112
+ async loadMore() {
113
+ const res = await fetch('/products?page=2');
114
+ this.$el.innerHTML = await res.text();
115
+ }
116
+ }
117
+
118
+ // ✅ Correct
119
+ import { Fetch } from '@studiometa/ui';
120
+ class ProductList extends Fetch { }
121
+ ```
122
+
123
+ ### `ui/prefer-data-model`
124
+
125
+ Detects `Base` subclasses with an `onInput*` or `onChange*` method that writes to `this.$refs.*` DOM properties. The `DataModel` and `DataEffect` components handle reactive bindings declaratively.
126
+
127
+ ```js
128
+ // ❌ Incorrect
129
+ import { Base } from '@studiometa/js-toolkit';
130
+ class LiveSearch extends Base {
131
+ onQueryInput() {
132
+ this.$refs.results.innerHTML = '';
133
+ }
134
+ }
135
+
136
+ // ✅ Correct
137
+ import { DataModel, DataEffect } from '@studiometa/ui';
138
+ ```
139
+
140
+ ### `ui/prefer-action`
141
+
142
+ Detects `Base` subclasses whose only logic is a single simple event handler (`onClick`, `onMouseenter`, etc.) with no other methods. The `Action` component handles this use case declaratively via data attributes.
143
+
144
+ ```js
145
+ // ❌ Incorrect
146
+ import { Base } from '@studiometa/js-toolkit';
147
+ class Toggle extends Base {
148
+ onClick() {
149
+ this.$el.classList.toggle('is-active');
150
+ }
151
+ }
152
+
153
+ // ✅ Correct — use the Action component via data attributes
154
+ // <div data-component="Action"
155
+ // data-option-on="click"
156
+ // data-option-target="#panel"
157
+ // data-option-effect="toggle-class:is-active">
158
+ ```
@@ -0,0 +1,5 @@
1
+ declare const plugin: import("@oxlint/plugins").Plugin & {
2
+ configs: Record<string, object>;
3
+ };
4
+ export default plugin;
5
+ export { plugin as ui };
package/dist/index.js ADDED
@@ -0,0 +1,582 @@
1
+ // ../../node_modules/@oxlint/plugins/index.js
2
+ var EMPTY_VISITOR = {};
3
+ function eslintCompatPlugin(plugin2) {
4
+ if (typeof plugin2 != "object" || !plugin2) throw Error("Plugin must be an object");
5
+ let { rules: rules2 } = plugin2;
6
+ if (typeof rules2 != "object" || !rules2) throw Error("Plugin must have an object as `rules` property");
7
+ let afterHooksState = new AfterHooksState();
8
+ for (let ruleName in rules2) Object.hasOwn(rules2, ruleName) && convertRule(rules2[ruleName], afterHooksState);
9
+ return plugin2;
10
+ }
11
+ var AfterHooksState = class {
12
+ resetFunctions = [];
13
+ pendingStates = [];
14
+ pendingCount = 0;
15
+ lintFinishedCount = 0;
16
+ resetIsScheduled = false;
17
+ sourceCode = null;
18
+ resetMicrotask = this.resetMicrotaskImpl.bind(this);
19
+ /**
20
+ * Register a function to run `after` hook for a rule, and reset state.
21
+ * @param reset - Function to run `after` hook and reset state
22
+ * @returns Index of rule
23
+ */
24
+ registerResetFunction(reset) {
25
+ let { pendingStates } = this, index = pendingStates.length;
26
+ return pendingStates.push(0), this.resetFunctions.push(reset), index;
27
+ }
28
+ /**
29
+ * Register that a rule with `after` hook has completed linting a file.
30
+ * Called by `onCodePathEnd` CFG event handler which is added to visitor for rules with `after` hooks.
31
+ *
32
+ * If all rules with an `after` hook which needs to be run have completed linting the file, run all `after` hooks.
33
+ */
34
+ ruleFinished() {
35
+ this.lintFinishedCount++, this.lintFinishedCount === this.pendingCount && this.reset(false);
36
+ }
37
+ /**
38
+ * Call all reset functions where corresponding entry in `pendingStates` is `AFTER_HOOK_PENDING`.
39
+ * Should only be called when some `after` hooks are pending.
40
+ *
41
+ * @param ignoreErrors - `true` to catch and silently ignore any errors which occur in `after` hooks.
42
+ * `false` to throw them,
43
+ * @throws {unknown} If `ignoreErrors` is `false` and an error occurs in any `after` hooks.
44
+ */
45
+ reset(ignoreErrors) {
46
+ this.pendingCount;
47
+ let { resetFunctions, pendingStates } = this, hooksLen = pendingStates.length, hasError = false, error;
48
+ for (let i = 0; i < hooksLen; i++) if (pendingStates[i] !== 0) {
49
+ pendingStates[i] = 0;
50
+ try {
51
+ resetFunctions[i]();
52
+ } catch (e) {
53
+ hasError === false && (hasError = true, error = e);
54
+ }
55
+ }
56
+ if (this.pendingCount = 0, this.lintFinishedCount = 0, this.sourceCode = null, hasError === true && ignoreErrors === false) throw error;
57
+ }
58
+ /**
59
+ * Schedule a microtask to run `reset` functions.
60
+ */
61
+ scheduleReset() {
62
+ queueMicrotask(this.resetMicrotask), this.resetIsScheduled = true;
63
+ }
64
+ /**
65
+ * Function which is scheduled as the cleanup microtask.
66
+ * `scheduleReset` uses `resetMicrotask` which is this method bound to `this`.
67
+ */
68
+ resetMicrotaskImpl() {
69
+ this.resetIsScheduled = false, this.pendingCount !== 0 && this.reset(true);
70
+ }
71
+ };
72
+ function convertRule(rule, afterHooksState) {
73
+ if (typeof rule != "object" || !rule) throw Error("Rule must be an object");
74
+ if ("create" in rule) return;
75
+ let context = null, visitor, beforeHook, setupAfterHook;
76
+ rule.create = (eslintContext) => {
77
+ context === null && ({ context, visitor, beforeHook, setupAfterHook } = createContextAndVisitor(rule, afterHooksState));
78
+ let eslintFileContext = Object.getPrototypeOf(eslintContext);
79
+ if (setupAfterHook !== null) {
80
+ let { sourceCode } = eslintFileContext;
81
+ afterHooksState.sourceCode !== sourceCode && (afterHooksState.sourceCode = sourceCode, afterHooksState.pendingCount !== 0 && afterHooksState.reset(true));
82
+ }
83
+ return Object.defineProperties(context, {
84
+ id: { value: eslintContext.id },
85
+ options: { value: eslintContext.options },
86
+ report: { value: eslintContext.report }
87
+ }), Object.setPrototypeOf(context, eslintFileContext), beforeHook !== null && beforeHook() === false ? EMPTY_VISITOR : (setupAfterHook !== null && (setupAfterHook(eslintFileContext.sourceCode.ast), afterHooksState.resetIsScheduled === false && afterHooksState.scheduleReset()), visitor);
88
+ };
89
+ }
90
+ var FILE_CONTEXT = Object.freeze({
91
+ get filename() {
92
+ throw Error("Cannot access `context.filename` in `createOnce`");
93
+ },
94
+ getFilename() {
95
+ throw Error("Cannot call `context.getFilename` in `createOnce`");
96
+ },
97
+ get physicalFilename() {
98
+ throw Error("Cannot access `context.physicalFilename` in `createOnce`");
99
+ },
100
+ getPhysicalFilename() {
101
+ throw Error("Cannot call `context.getPhysicalFilename` in `createOnce`");
102
+ },
103
+ get cwd() {
104
+ throw Error("Cannot access `context.cwd` in `createOnce`");
105
+ },
106
+ getCwd() {
107
+ throw Error("Cannot call `context.getCwd` in `createOnce`");
108
+ },
109
+ get sourceCode() {
110
+ throw Error("Cannot access `context.sourceCode` in `createOnce`");
111
+ },
112
+ getSourceCode() {
113
+ throw Error("Cannot call `context.getSourceCode` in `createOnce`");
114
+ },
115
+ get languageOptions() {
116
+ throw Error("Cannot access `context.languageOptions` in `createOnce`");
117
+ },
118
+ get settings() {
119
+ throw Error("Cannot access `context.settings` in `createOnce`");
120
+ },
121
+ extend(extension) {
122
+ return Object.freeze(Object.assign(Object.create(this), extension));
123
+ },
124
+ get parserOptions() {
125
+ throw Error("Cannot access `context.parserOptions` in `createOnce`");
126
+ },
127
+ get parserPath() {
128
+ throw Error("Cannot access `context.parserPath` in `createOnce`");
129
+ }
130
+ });
131
+ function createContextAndVisitor(rule, afterHooksState) {
132
+ let { createOnce } = rule;
133
+ if (createOnce == null) throw Error("Rules must define either a `create` or `createOnce` method");
134
+ if (typeof createOnce != "function") throw Error("Rule `createOnce` property must be a function");
135
+ let context = Object.create(FILE_CONTEXT, {
136
+ id: {
137
+ value: null,
138
+ enumerable: true,
139
+ configurable: true
140
+ },
141
+ options: {
142
+ value: null,
143
+ enumerable: true,
144
+ configurable: true
145
+ },
146
+ report: {
147
+ value() {
148
+ throw Error("Cannot report errors in `createOnce`");
149
+ },
150
+ enumerable: true,
151
+ configurable: true
152
+ }
153
+ }), { before: beforeHook, after: afterHook, ...visitor } = createOnce.call(rule, context);
154
+ if (beforeHook === void 0) beforeHook = null;
155
+ else if (beforeHook !== null && typeof beforeHook != "function") throw Error("`before` property of visitor must be a function if defined");
156
+ let setupAfterHook = null;
157
+ if (afterHook != null) {
158
+ if (typeof afterHook != "function") throw Error("`after` property of visitor must be a function if defined");
159
+ let program = null, ruleIndex = afterHooksState.registerResetFunction(() => {
160
+ program = null, afterHook();
161
+ });
162
+ setupAfterHook = (ast) => {
163
+ program = ast, afterHooksState.pendingStates[ruleIndex] = 1, afterHooksState.pendingCount++;
164
+ };
165
+ let onCodePathEnd = visitor.onCodePathEnd;
166
+ visitor.onCodePathEnd = onCodePathEnd == null ? function(_codePath, node) {
167
+ node === program && afterHooksState.ruleFinished();
168
+ } : function(codePath, node) {
169
+ onCodePathEnd.call(this, codePath, node), node === program && afterHooksState.ruleFinished();
170
+ };
171
+ }
172
+ return {
173
+ context,
174
+ visitor,
175
+ beforeHook,
176
+ setupAfterHook
177
+ };
178
+ }
179
+
180
+ // src/utils/ast.ts
181
+ function createRule(rule) {
182
+ return rule;
183
+ }
184
+ var UI_PACKAGE = "@studiometa/ui";
185
+ var TOOLKIT_PACKAGE = "@studiometa/js-toolkit";
186
+ var UI_COMPONENT_NAMES = /* @__PURE__ */ new Set([
187
+ "Accordion",
188
+ "AccordionItem",
189
+ "Action",
190
+ "AnchorNav",
191
+ "AnchorNavLink",
192
+ "AnchorNavTarget",
193
+ "CircularMarquee",
194
+ "Cursor",
195
+ "DataBind",
196
+ "DataComputed",
197
+ "DataEffect",
198
+ "DataModel",
199
+ "Draggable",
200
+ "Figure",
201
+ "FigureVideo",
202
+ "Frame",
203
+ "FrameAnchor",
204
+ "FrameForm",
205
+ "FrameTarget",
206
+ "FrameLoader",
207
+ "Hoverable",
208
+ "LargeText",
209
+ "LazyInclude",
210
+ "Menu",
211
+ "MenuBtn",
212
+ "MenuList",
213
+ "Modal",
214
+ "Panel",
215
+ "AbstractPrefetch",
216
+ "PrefetchWhenOver",
217
+ "PrefetchWhenVisible",
218
+ "AbstractScrollAnimation",
219
+ "ScrollAnimationTimeline",
220
+ "ScrollAnimationTarget",
221
+ "ScrollReveal",
222
+ "Sentinel",
223
+ "Slider",
224
+ "SliderItem",
225
+ "SliderDrag",
226
+ "Sticky",
227
+ "Tabs",
228
+ "Transition"
229
+ ]);
230
+ function getSuperClassName(node) {
231
+ if (node.type !== "ClassDeclaration" && node.type !== "ClassExpression") return null;
232
+ if (!node.superClass) return null;
233
+ if (node.superClass.type === "Identifier") return node.superClass.name;
234
+ return null;
235
+ }
236
+ function isBaseSubclass(node, context) {
237
+ const superName = getSuperClassName(node);
238
+ if (!superName) return false;
239
+ if (superName === "Base") return true;
240
+ const sourceCode = context.sourceCode ?? context.getSourceCode?.();
241
+ const ast = sourceCode?.ast;
242
+ if (!ast) return false;
243
+ for (const n of ast.body) {
244
+ if (n.type !== "ImportDeclaration") continue;
245
+ if (n.source.value !== TOOLKIT_PACKAGE) continue;
246
+ for (const specifier of n.specifiers) {
247
+ if (specifier.type === "ImportSpecifier" && specifier.local.name === superName) return true;
248
+ }
249
+ }
250
+ return false;
251
+ }
252
+ function isImportedFromUI(className, context) {
253
+ const sourceCode = context.sourceCode ?? context.getSourceCode?.();
254
+ const ast = sourceCode?.ast;
255
+ if (!ast) return false;
256
+ for (const n of ast.body) {
257
+ if (n.type !== "ImportDeclaration") continue;
258
+ if (n.source.value !== UI_PACKAGE) continue;
259
+ for (const specifier of n.specifiers) {
260
+ if (specifier.type === "ImportSpecifier" && specifier.imported?.name === className)
261
+ return true;
262
+ }
263
+ }
264
+ return false;
265
+ }
266
+ function findEnclosingClass(ancestors) {
267
+ for (let i = ancestors.length - 1; i >= 0; i--) {
268
+ const node = ancestors[i];
269
+ if (node.type === "ClassDeclaration" || node.type === "ClassExpression") return node;
270
+ }
271
+ return null;
272
+ }
273
+ function getAncestors(context, node) {
274
+ return context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
275
+ }
276
+ function getClassMethods(classNode) {
277
+ const methods = /* @__PURE__ */ new Map();
278
+ for (const member of classNode.body?.body ?? []) {
279
+ if (member.type === "MethodDefinition" && member.key?.type === "Identifier") {
280
+ methods.set(member.key.name, member);
281
+ }
282
+ }
283
+ return methods;
284
+ }
285
+
286
+ // src/rules/prefer-ui-component.ts
287
+ var preferUiComponent = createRule({
288
+ meta: {
289
+ type: "suggestion",
290
+ docs: {
291
+ description: "Prefer importing components from @studiometa/ui over reimplementing them from scratch"
292
+ },
293
+ messages: {
294
+ preferImport: `"{{name}}" is available in @studiometa/ui. Extend it instead of reimplementing from Base: import { {{name}} } from '@studiometa/ui'.`
295
+ }
296
+ },
297
+ createOnce(context) {
298
+ return {
299
+ ClassDeclaration(node) {
300
+ check(node, context);
301
+ },
302
+ ClassExpression(node) {
303
+ check(node, context);
304
+ }
305
+ };
306
+ }
307
+ });
308
+ function check(node, context) {
309
+ const name = node.id?.name ?? "";
310
+ if (!name || !UI_COMPONENT_NAMES.has(name)) return;
311
+ if (!isBaseSubclass(node, context)) return;
312
+ if (isImportedFromUI(name, context)) return;
313
+ context.report({ node: node.id ?? node, messageId: "preferImport", data: { name } });
314
+ }
315
+
316
+ // src/rules/prefer-transition.ts
317
+ var preferTransition = createRule({
318
+ meta: {
319
+ type: "suggestion",
320
+ docs: {
321
+ description: "Prefer @studiometa/ui Transition over manually implementing open/close logic in a Base subclass"
322
+ },
323
+ messages: {
324
+ preferTransition: "This class manually implements open() and close() methods. Consider extending Transition from @studiometa/ui instead, which handles show/hide with CSS class transitions: import { Transition } from '@studiometa/ui'."
325
+ }
326
+ },
327
+ createOnce(context) {
328
+ return {
329
+ ClassDeclaration(node) {
330
+ check2(node, context);
331
+ },
332
+ ClassExpression(node) {
333
+ check2(node, context);
334
+ }
335
+ };
336
+ }
337
+ });
338
+ function check2(node, context) {
339
+ if (!isBaseSubclass(node, context)) return;
340
+ if (isImportedFromUI(node.superClass?.name, context)) return;
341
+ const methods = getClassMethods(node);
342
+ if (!methods.has("open") || !methods.has("close")) return;
343
+ context.report({ node: node.id ?? node, messageId: "preferTransition" });
344
+ }
345
+
346
+ // src/rules/no-manual-fetch.ts
347
+ var DOM_WRITE_METHODS = /* @__PURE__ */ new Set(["insertAdjacentHTML", "insertAdjacentElement"]);
348
+ var noManualFetch = createRule({
349
+ meta: {
350
+ type: "suggestion",
351
+ docs: {
352
+ description: "Prefer the Fetch component from @studiometa/ui over manually fetching and injecting HTML"
353
+ },
354
+ messages: {
355
+ preferFetch: "Manual fetch() with DOM injection detected. Consider using the Fetch component from @studiometa/ui instead: import { Fetch } from '@studiometa/ui'."
356
+ }
357
+ },
358
+ createOnce(context) {
359
+ const classesWithFetch = /* @__PURE__ */ new WeakSet();
360
+ const classesWithDomWrite = /* @__PURE__ */ new WeakSet();
361
+ const reported = /* @__PURE__ */ new WeakSet();
362
+ function getEnclosingBaseClass(node) {
363
+ const ancestors = getAncestors(context, node);
364
+ const cls = findEnclosingClass(ancestors);
365
+ if (!cls || !isBaseSubclass(cls, context)) return null;
366
+ if (isImportedFromUI("Fetch", context)) return null;
367
+ return cls;
368
+ }
369
+ function maybeReport(cls) {
370
+ if (reported.has(cls)) return;
371
+ if (classesWithFetch.has(cls) && classesWithDomWrite.has(cls)) {
372
+ reported.add(cls);
373
+ context.report({ node: cls.id ?? cls, messageId: "preferFetch" });
374
+ }
375
+ }
376
+ return {
377
+ CallExpression(node) {
378
+ const callee = node.callee;
379
+ const isFetch = callee.type === "Identifier" && callee.name === "fetch" || callee.type === "MemberExpression" && callee.property?.name === "fetch" && callee.object?.type === "ThisExpression";
380
+ const isDomWrite = callee.type === "MemberExpression" && DOM_WRITE_METHODS.has(callee.property?.name);
381
+ if (!isFetch && !isDomWrite) return;
382
+ const cls = getEnclosingBaseClass(node);
383
+ if (!cls) return;
384
+ if (isFetch) classesWithFetch.add(cls);
385
+ if (isDomWrite) classesWithDomWrite.add(cls);
386
+ maybeReport(cls);
387
+ },
388
+ AssignmentExpression(node) {
389
+ const left = node.left;
390
+ if (left.type !== "MemberExpression") return;
391
+ if (left.property?.name !== "innerHTML") return;
392
+ const cls = getEnclosingBaseClass(node);
393
+ if (!cls) return;
394
+ classesWithDomWrite.add(cls);
395
+ maybeReport(cls);
396
+ }
397
+ };
398
+ }
399
+ });
400
+
401
+ // src/rules/prefer-data-model.ts
402
+ function isThisComponentAccess(node) {
403
+ let current = node;
404
+ while (current?.type === "MemberExpression") {
405
+ if (current.object?.type === "ThisExpression" && (current.property?.name === "$refs" || current.property?.name === "$el" || current.property?.name === "$children")) {
406
+ return true;
407
+ }
408
+ current = current.object;
409
+ }
410
+ return false;
411
+ }
412
+ var preferDataModel = createRule({
413
+ meta: {
414
+ type: "suggestion",
415
+ docs: {
416
+ description: "Prefer DataModel/DataEffect from @studiometa/ui over manually syncing input values to the DOM"
417
+ },
418
+ messages: {
419
+ preferDataModel: "Manual input\u2192DOM synchronization detected. Consider using DataModel and DataEffect from @studiometa/ui for reactive bindings: import { DataModel, DataEffect } from '@studiometa/ui'."
420
+ }
421
+ },
422
+ createOnce(context) {
423
+ const classesWithInputListener = /* @__PURE__ */ new WeakSet();
424
+ const classesWithDomWrite = /* @__PURE__ */ new WeakSet();
425
+ const reported = /* @__PURE__ */ new WeakSet();
426
+ function getEnclosingBaseClass(node) {
427
+ const ancestors = getAncestors(context, node);
428
+ const cls = findEnclosingClass(ancestors);
429
+ if (!cls || !isBaseSubclass(cls, context)) return null;
430
+ if (isImportedFromUI("DataModel", context) || isImportedFromUI("DataEffect", context))
431
+ return null;
432
+ return cls;
433
+ }
434
+ function maybeReport(cls) {
435
+ if (reported.has(cls)) return;
436
+ if (classesWithInputListener.has(cls) && classesWithDomWrite.has(cls)) {
437
+ reported.add(cls);
438
+ context.report({ node: cls.id ?? cls, messageId: "preferDataModel" });
439
+ }
440
+ }
441
+ return {
442
+ // Detect on-handler methods named onInput*, onChange*, onModelInput, etc.
443
+ MethodDefinition(node) {
444
+ const name = node.key?.name ?? "";
445
+ if (!/^on.*(Input|Change|Model)/i.test(name)) return;
446
+ const ancestors = getAncestors(context, node);
447
+ const cls = findEnclosingClass(ancestors);
448
+ if (!cls || !isBaseSubclass(cls, context)) return;
449
+ if (isImportedFromUI("DataModel", context)) return;
450
+ classesWithInputListener.add(cls);
451
+ maybeReport(cls);
452
+ },
453
+ AssignmentExpression(node) {
454
+ const left = node.left;
455
+ if (left.type !== "MemberExpression") return;
456
+ const prop = left.property?.name;
457
+ const isDomWrite = prop === "textContent" || prop === "innerHTML" || prop === "value";
458
+ if (!isDomWrite) return;
459
+ if (!isThisComponentAccess(left.object)) return;
460
+ const cls = getEnclosingBaseClass(node);
461
+ if (!cls) return;
462
+ classesWithDomWrite.add(cls);
463
+ maybeReport(cls);
464
+ }
465
+ };
466
+ }
467
+ });
468
+
469
+ // src/rules/prefer-action.ts
470
+ var ACTION_EVENT_METHODS = /* @__PURE__ */ new Set([
471
+ "onClick",
472
+ "onMouseenter",
473
+ "onMouseleave",
474
+ "onMouseover",
475
+ "onMouseout",
476
+ "onFocus",
477
+ "onBlur",
478
+ "onKeydown",
479
+ "onKeyup",
480
+ "onKeypress",
481
+ "onPointerenter",
482
+ "onPointerleave",
483
+ "onPointerdown",
484
+ "onPointerup"
485
+ ]);
486
+ var NON_LOGIC_METHODS = /* @__PURE__ */ new Set(["mounted", "destroyed", "updated"]);
487
+ var preferAction = createRule({
488
+ meta: {
489
+ type: "suggestion",
490
+ docs: {
491
+ description: "Prefer the Action component from @studiometa/ui for simple event-triggered effects"
492
+ },
493
+ messages: {
494
+ preferAction: `This component only handles "{{method}}" on the root element. Consider using the Action component from @studiometa/ui instead, which declaratively wires events to effects via data attributes: import { Action } from '@studiometa/ui'.`
495
+ }
496
+ },
497
+ createOnce(context) {
498
+ return {
499
+ ClassDeclaration(node) {
500
+ check3(node, context);
501
+ },
502
+ ClassExpression(node) {
503
+ check3(node, context);
504
+ }
505
+ };
506
+ }
507
+ });
508
+ function check3(node, context) {
509
+ if (!isBaseSubclass(node, context)) return;
510
+ if (isImportedFromUI("Action", context)) return;
511
+ const methods = getClassMethods(node);
512
+ const actionMethods = [];
513
+ const otherMethods = [];
514
+ for (const [name] of methods) {
515
+ if (ACTION_EVENT_METHODS.has(name)) {
516
+ actionMethods.push(name);
517
+ } else if (name !== "config" && !NON_LOGIC_METHODS.has(name)) {
518
+ otherMethods.push(name);
519
+ }
520
+ }
521
+ if (actionMethods.length !== 1 || otherMethods.length > 0) return;
522
+ const method = methods.get(actionMethods[0]);
523
+ if (hasComplexBody(method)) return;
524
+ context.report({
525
+ node: node.id ?? node,
526
+ messageId: "preferAction",
527
+ data: { method: actionMethods[0] }
528
+ });
529
+ }
530
+ function hasComplexBody(methodNode) {
531
+ let complex = false;
532
+ function walk(n) {
533
+ if (complex) return;
534
+ if (!n || typeof n !== "object") return;
535
+ if (n.type === "MemberExpression" && n.object?.type === "ThisExpression" && (n.property?.name === "$children" || n.property?.name === "$root")) {
536
+ complex = true;
537
+ return;
538
+ }
539
+ for (const key of Object.keys(n)) {
540
+ if (key === "parent") continue;
541
+ const child = n[key];
542
+ if (Array.isArray(child)) child.forEach(walk);
543
+ else if (child && typeof child === "object" && child.type) walk(child);
544
+ }
545
+ }
546
+ walk(methodNode.value ?? methodNode);
547
+ return complex;
548
+ }
549
+
550
+ // src/index.ts
551
+ var PLUGIN_NAME = "ui";
552
+ var rules = {
553
+ "prefer-ui-component": preferUiComponent,
554
+ "prefer-transition": preferTransition,
555
+ "no-manual-fetch": noManualFetch,
556
+ "prefer-data-model": preferDataModel,
557
+ "prefer-action": preferAction
558
+ };
559
+ var recommendedRules = {
560
+ [`${PLUGIN_NAME}/prefer-ui-component`]: "warn",
561
+ [`${PLUGIN_NAME}/prefer-transition`]: "warn",
562
+ [`${PLUGIN_NAME}/no-manual-fetch`]: "warn",
563
+ [`${PLUGIN_NAME}/prefer-data-model`]: "warn",
564
+ [`${PLUGIN_NAME}/prefer-action`]: "warn"
565
+ };
566
+ var base = eslintCompatPlugin({
567
+ meta: {
568
+ name: "@studiometa/eslint-plugin-ui"
569
+ },
570
+ rules
571
+ });
572
+ var plugin = Object.assign(base, { configs: {} });
573
+ plugin.configs["recommended"] = {
574
+ plugins: { [PLUGIN_NAME]: plugin },
575
+ rules: recommendedRules
576
+ };
577
+ var index_default = plugin;
578
+ export {
579
+ index_default as default,
580
+ plugin as ui
581
+ };
582
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../node_modules/@oxlint/plugins/index.js", "../src/utils/ast.ts", "../src/rules/prefer-ui-component.ts", "../src/rules/prefer-transition.ts", "../src/rules/no-manual-fetch.ts", "../src/rules/prefer-data-model.ts", "../src/rules/prefer-action.ts", "../src/index.ts"],
4
+ "sourcesContent": ["//#region src-js/package/define.ts\n/**\n* Define a plugin.\n*\n* No-op function, just to provide type safety. Input is passed through unchanged.\n*\n* @param plugin - Plugin to define\n* @returns Same plugin as passed in\n*/\nfunction definePlugin(plugin) {\n\treturn plugin;\n}\n/**\n* Define a rule.\n*\n* No-op function, just to provide type safety. Input is passed through unchanged.\n*\n* @param rule - Rule to define\n* @returns Same rule as passed in\n*/\nfunction defineRule(rule) {\n\treturn rule;\n}\n//#endregion\n//#region src-js/package/compat.ts\nconst EMPTY_VISITOR = {};\n/**\n* Convert a plugin which used Oxlint's `createOnce` API to also work with ESLint.\n*\n* If any of the plugin's rules use the Oxlint alternative `createOnce` API,\n* add ESLint-compatible `create` methods to those rules, which delegate to `createOnce`.\n* This makes the plugin compatible with ESLint.\n*\n* The `plugin` object passed in is mutated in-place.\n*\n* @param plugin - Plugin to convert\n* @returns Plugin with all rules having `create` method\n* @throws {Error} If `plugin` is not an object, or `plugin.rules` is not an object\n*/\nfunction eslintCompatPlugin(plugin) {\n\tif (typeof plugin != \"object\" || !plugin) throw Error(\"Plugin must be an object\");\n\tlet { rules } = plugin;\n\tif (typeof rules != \"object\" || !rules) throw Error(\"Plugin must have an object as `rules` property\");\n\tlet afterHooksState = new AfterHooksState();\n\tfor (let ruleName in rules) Object.hasOwn(rules, ruleName) && convertRule(rules[ruleName], afterHooksState);\n\treturn plugin;\n}\n/**\n* Class containing state for tracking if any `after` hooks for a plugin's rules need to be called.\n*\n* # Aims\n*\n* Aims are:\n* 1. `after` hook of each rule runs after all other AST visit functions, and CFG event handlers.\n* 2. `after` hooks for *all* a plugin's rules run after *all* that plugin's rules have completed visiting AST.\n* 3. `after` hooks for *all* a plugin's rules run before *any* of plugin's rules begin linting another file.\n* 4. In the case of an error during AST traversal, `after` hooks are always still run.\n*\n* The above exactly matches the behavior when running a `createOnce` rule in Oxlint.\n*\n* # Why this is important\n*\n* All the complication comes from ensuring `after` hooks run even after an error during AST traversal.\n*\n* In ESLint CLI, an error will crash the process, so it doesn't particularly matter if `after` hooks run or not,\n* but language servers will typically swallow errors, and keep the process running.\n*\n* Rules using `before` and `after` hooks will often rely on both hooks running in a predictable order,\n* to maintain some internal state. For example, they may use `before` and `after` hooks to maintain a per-file\n* cache of data which is shared between rules. The cache use case is why rule (2) above is important.\n*\n* Below is an example of using `before` and `after` hooks to maintain a per-file cache, shared between rules.\n* It relies on all `before` hooks running before any rule starts visiting the AST,\n* and all `after` hooks running after all rules have finished visiting the AST.\n*\n* ```ts\n* let cache: Data | null = null;\n*\n* let numRunningRules = 0;\n*\n* const setupCache = (context) => {\n* if (cache === null) cache = new Data(context);\n* numRunningRules++;\n* };\n*\n* const teardownCache = () => {\n* numRunningRules--;\n* if (numRunningRules === 0) cache = null;\n* };\n*\n* const rule1 = {\n* createOnce(context) {\n* return {\n* before() {\n* setupCache(context);\n* },\n* Identifier(node) {\n* // Use `cache`\n* },\n* after: teardownCache,\n* };\n* },\n* };\n*\n* const rule2 = {\n* // Same as above\n* };\n*\n* const rule3 = {\n* // Same as above\n* };\n* ```\n*\n* If `after` hooks did not always run, the next lint run could get stale state, and malfunction.\n* If `after` hooks ran in the wrong order (e.g. after some `before` hooks for next file),\n* `numRunningRules` would never get to 0, and cache would never be cleared.\n*\n* Note that because all rules run together in a single AST traversal, if a rule from plugin X throws an error,\n* it can disrupt rules from plugin Y. This would make it hard to debug.\n*\n* # Mechanism\n*\n* ## Initialization\n*\n* Rules with an `after` hook register themselves by:\n*\n* 1. Calling `registerResetFunction` to register a function to run `after` hook and clean up internal state.\n* This call adds the reset fn to `resetFunctions`, and adds `AFTER_HOOK_INACTIVE` to `pendingStates`.\n* 2. Adding an `onCodePathEnd` CFG event handler to the visitor which calls `ruleFinished` at end of AST traversal.\n*\n* ## Per-file setup\n*\n* Before linting a file, `create` will call `setupAfterHook` which is created by `createContextAndVisitor`.\n* This registers that the `after` hook for the rule needs to run, by setting `pendingStates[ruleIndex]`\n* to `AFTER_HOOK_PENDING`, and incrementing `pendingCount`.\n*\n* If a cleanup microtask has not been scheduled yet, one is scheduled now (see reason below).\n*\n* ## Normal operation\n*\n* AST traversal for each rule ends with `ruleFinished` hook being called from `onCodePathEnd` CFG event handler.\n* It increments `lintFinishedCount`. If `lintFinishedCount` equals `pendingCount`, all rules have finished linting\n* the file, and `reset` is called, which calls all the pending `after` hooks.\n*\n* ## Error handling\n*\n* If an error is thrown during AST traversal, we ensure that `after` hooks are still run by 2 mechanisms:\n*\n* ### 1. Next microtick\n*\n* Before any rules began linting files, a microtask was scheduled, which runs on next micro-tick.\n* All language servers we're aware of run each lint task in a separate tick, so this microtask will run in next tick\n* after a linting run, before the next lint task starts.\n*\n* If the linting run completed successfully, the microtask does nothing.\n*\n* But if an error was thrown during AST traversal, this will be visible from the state of `pendingCount`.\n* The microtask will run any `after` hooks which need to be run, and reset state to reflect that there are\n* no more pending `after` hooks.\n*\n* ### 2. Fallback: Next lint run\n*\n* Before linting any file, the state of `pendingCount` is checked.\n* If any `after` hooks are still pending, they are run immediately.\n* They're run before the `context` objects in `createOnce` closures are updated to the next file,\n* so they run with access to the old `context` object from the last file.\n*\n* This fallback should not be required, but it's included as \"belt and braces\", to handle if any language server\n* or other environment running ESLint programmatically, does not pause a tick between linting runs.\n*/\nvar AfterHooksState = class {\n\tresetFunctions = [];\n\tpendingStates = [];\n\tpendingCount = 0;\n\tlintFinishedCount = 0;\n\tresetIsScheduled = !1;\n\tsourceCode = null;\n\tresetMicrotask = this.resetMicrotaskImpl.bind(this);\n\t/**\n\t* Register a function to run `after` hook for a rule, and reset state.\n\t* @param reset - Function to run `after` hook and reset state\n\t* @returns Index of rule\n\t*/\n\tregisterResetFunction(reset) {\n\t\tlet { pendingStates } = this, index = pendingStates.length;\n\t\treturn pendingStates.push(0), this.resetFunctions.push(reset), index;\n\t}\n\t/**\n\t* Register that a rule with `after` hook has completed linting a file.\n\t* Called by `onCodePathEnd` CFG event handler which is added to visitor for rules with `after` hooks.\n\t*\n\t* If all rules with an `after` hook which needs to be run have completed linting the file, run all `after` hooks.\n\t*/\n\truleFinished() {\n\t\tthis.lintFinishedCount++, this.lintFinishedCount === this.pendingCount && this.reset(!1);\n\t}\n\t/**\n\t* Call all reset functions where corresponding entry in `pendingStates` is `AFTER_HOOK_PENDING`.\n\t* Should only be called when some `after` hooks are pending.\n\t*\n\t* @param ignoreErrors - `true` to catch and silently ignore any errors which occur in `after` hooks.\n\t* `false` to throw them,\n\t* @throws {unknown} If `ignoreErrors` is `false` and an error occurs in any `after` hooks.\n\t*/\n\treset(ignoreErrors) {\n\t\tthis.pendingCount;\n\t\tlet { resetFunctions, pendingStates } = this, hooksLen = pendingStates.length, hasError = !1, error;\n\t\tfor (let i = 0; i < hooksLen; i++) if (pendingStates[i] !== 0) {\n\t\t\tpendingStates[i] = 0;\n\t\t\ttry {\n\t\t\t\tresetFunctions[i]();\n\t\t\t} catch (e) {\n\t\t\t\thasError === !1 && (hasError = !0, error = e);\n\t\t\t}\n\t\t}\n\t\tif (this.pendingCount = 0, this.lintFinishedCount = 0, this.sourceCode = null, hasError === !0 && ignoreErrors === !1) throw error;\n\t}\n\t/**\n\t* Schedule a microtask to run `reset` functions.\n\t*/\n\tscheduleReset() {\n\t\tqueueMicrotask(this.resetMicrotask), this.resetIsScheduled = !0;\n\t}\n\t/**\n\t* Function which is scheduled as the cleanup microtask.\n\t* `scheduleReset` uses `resetMicrotask` which is this method bound to `this`.\n\t*/\n\tresetMicrotaskImpl() {\n\t\tthis.resetIsScheduled = !1, this.pendingCount !== 0 && this.reset(!0);\n\t}\n};\n/**\n* Convert a rule.\n*\n* The `rule` object passed in is mutated in-place.\n*\n* @param rule - Rule to convert\n* @param afterHooksState - State of `after` hooks\n* @throws {Error} If `rule` is not an object\n*/\nfunction convertRule(rule, afterHooksState) {\n\tif (typeof rule != \"object\" || !rule) throw Error(\"Rule must be an object\");\n\tif (\"create\" in rule) return;\n\tlet context = null, visitor, beforeHook, setupAfterHook;\n\trule.create = (eslintContext) => {\n\t\tcontext === null && ({context, visitor, beforeHook, setupAfterHook} = createContextAndVisitor(rule, afterHooksState));\n\t\tlet eslintFileContext = Object.getPrototypeOf(eslintContext);\n\t\tif (setupAfterHook !== null) {\n\t\t\tlet { sourceCode } = eslintFileContext;\n\t\t\tafterHooksState.sourceCode !== sourceCode && (afterHooksState.sourceCode = sourceCode, afterHooksState.pendingCount !== 0 && afterHooksState.reset(!0));\n\t\t}\n\t\treturn Object.defineProperties(context, {\n\t\t\tid: { value: eslintContext.id },\n\t\t\toptions: { value: eslintContext.options },\n\t\t\treport: { value: eslintContext.report }\n\t\t}), Object.setPrototypeOf(context, eslintFileContext), beforeHook !== null && beforeHook() === !1 ? EMPTY_VISITOR : (setupAfterHook !== null && (setupAfterHook(eslintFileContext.sourceCode.ast), afterHooksState.resetIsScheduled === !1 && afterHooksState.scheduleReset()), visitor);\n\t};\n}\nconst FILE_CONTEXT = Object.freeze({\n\tget filename() {\n\t\tthrow Error(\"Cannot access `context.filename` in `createOnce`\");\n\t},\n\tgetFilename() {\n\t\tthrow Error(\"Cannot call `context.getFilename` in `createOnce`\");\n\t},\n\tget physicalFilename() {\n\t\tthrow Error(\"Cannot access `context.physicalFilename` in `createOnce`\");\n\t},\n\tgetPhysicalFilename() {\n\t\tthrow Error(\"Cannot call `context.getPhysicalFilename` in `createOnce`\");\n\t},\n\tget cwd() {\n\t\tthrow Error(\"Cannot access `context.cwd` in `createOnce`\");\n\t},\n\tgetCwd() {\n\t\tthrow Error(\"Cannot call `context.getCwd` in `createOnce`\");\n\t},\n\tget sourceCode() {\n\t\tthrow Error(\"Cannot access `context.sourceCode` in `createOnce`\");\n\t},\n\tgetSourceCode() {\n\t\tthrow Error(\"Cannot call `context.getSourceCode` in `createOnce`\");\n\t},\n\tget languageOptions() {\n\t\tthrow Error(\"Cannot access `context.languageOptions` in `createOnce`\");\n\t},\n\tget settings() {\n\t\tthrow Error(\"Cannot access `context.settings` in `createOnce`\");\n\t},\n\textend(extension) {\n\t\treturn Object.freeze(Object.assign(Object.create(this), extension));\n\t},\n\tget parserOptions() {\n\t\tthrow Error(\"Cannot access `context.parserOptions` in `createOnce`\");\n\t},\n\tget parserPath() {\n\t\tthrow Error(\"Cannot access `context.parserPath` in `createOnce`\");\n\t}\n});\n/**\n* Call `createOnce` method of rule, and return `Context`, `Visitor`, and `beforeHook` (if any).\n*\n* @param rule - Rule with `createOnce` method\n* @param afterHooksState - State of `after` hooks\n* @returns Object with `context`, `visitor`, and `beforeHook` properties,\n* and `setupAfterHook` function if visitor has an `after` hook\n*/\nfunction createContextAndVisitor(rule, afterHooksState) {\n\tlet { createOnce } = rule;\n\tif (createOnce == null) throw Error(\"Rules must define either a `create` or `createOnce` method\");\n\tif (typeof createOnce != \"function\") throw Error(\"Rule `createOnce` property must be a function\");\n\tlet context = Object.create(FILE_CONTEXT, {\n\t\tid: {\n\t\t\tvalue: null,\n\t\t\tenumerable: !0,\n\t\t\tconfigurable: !0\n\t\t},\n\t\toptions: {\n\t\t\tvalue: null,\n\t\t\tenumerable: !0,\n\t\t\tconfigurable: !0\n\t\t},\n\t\treport: {\n\t\t\tvalue() {\n\t\t\t\tthrow Error(\"Cannot report errors in `createOnce`\");\n\t\t\t},\n\t\t\tenumerable: !0,\n\t\t\tconfigurable: !0\n\t\t}\n\t}), { before: beforeHook, after: afterHook, ...visitor } = createOnce.call(rule, context);\n\tif (beforeHook === void 0) beforeHook = null;\n\telse if (beforeHook !== null && typeof beforeHook != \"function\") throw Error(\"`before` property of visitor must be a function if defined\");\n\tlet setupAfterHook = null;\n\tif (afterHook != null) {\n\t\tif (typeof afterHook != \"function\") throw Error(\"`after` property of visitor must be a function if defined\");\n\t\tlet program = null, ruleIndex = afterHooksState.registerResetFunction(() => {\n\t\t\tprogram = null, afterHook();\n\t\t});\n\t\tsetupAfterHook = (ast) => {\n\t\t\tprogram = ast, afterHooksState.pendingStates[ruleIndex] = 1, afterHooksState.pendingCount++;\n\t\t};\n\t\tlet onCodePathEnd = visitor.onCodePathEnd;\n\t\tvisitor.onCodePathEnd = onCodePathEnd == null ? function(_codePath, node) {\n\t\t\tnode === program && afterHooksState.ruleFinished();\n\t\t} : function(codePath, node) {\n\t\t\tonCodePathEnd.call(this, codePath, node), node === program && afterHooksState.ruleFinished();\n\t\t};\n\t}\n\treturn {\n\t\tcontext,\n\t\tvisitor,\n\t\tbeforeHook,\n\t\tsetupAfterHook\n\t};\n}\n//#endregion\nexport { definePlugin, defineRule, eslintCompatPlugin };\n", "export type Node = Record<string, any>;\nexport type RuleContext = Record<string, any>;\n\nexport type RuleMeta = {\n type?: 'problem' | 'suggestion' | 'layout';\n fixable?: 'code' | 'whitespace';\n hasSuggestions?: boolean;\n docs?: { description?: string };\n messages?: Record<string, string>;\n};\n\nexport function createRule<V extends Record<string, (node: Node) => unknown>>(rule: {\n meta?: RuleMeta;\n createOnce(context: RuleContext): V;\n}): { meta?: RuleMeta; createOnce(context: RuleContext): V } {\n return rule;\n}\n\nexport const UI_PACKAGE = '@studiometa/ui';\nexport const TOOLKIT_PACKAGE = '@studiometa/js-toolkit';\n\n/**\n * All component names exported by @studiometa/ui.\n * Keep in sync with packages/ui/index.ts exports.\n */\nexport const UI_COMPONENT_NAMES = new Set([\n 'Accordion',\n 'AccordionItem',\n 'Action',\n 'AnchorNav',\n 'AnchorNavLink',\n 'AnchorNavTarget',\n 'CircularMarquee',\n 'Cursor',\n 'DataBind',\n 'DataComputed',\n 'DataEffect',\n 'DataModel',\n 'Draggable',\n 'Figure',\n 'FigureVideo',\n 'Frame',\n 'FrameAnchor',\n 'FrameForm',\n 'FrameTarget',\n 'FrameLoader',\n 'Hoverable',\n 'LargeText',\n 'LazyInclude',\n 'Menu',\n 'MenuBtn',\n 'MenuList',\n 'Modal',\n 'Panel',\n 'AbstractPrefetch',\n 'PrefetchWhenOver',\n 'PrefetchWhenVisible',\n 'AbstractScrollAnimation',\n 'ScrollAnimationTimeline',\n 'ScrollAnimationTarget',\n 'ScrollReveal',\n 'Sentinel',\n 'Slider',\n 'SliderItem',\n 'SliderDrag',\n 'Sticky',\n 'Tabs',\n 'Transition',\n]);\n\n/**\n * Returns the local name of the superclass for a class declaration/expression.\n */\nexport function getSuperClassName(node: Node): string | null {\n if (node.type !== 'ClassDeclaration' && node.type !== 'ClassExpression') return null;\n if (!node.superClass) return null;\n if (node.superClass.type === 'Identifier') return node.superClass.name;\n return null;\n}\n\n/**\n * Returns true when a class extends Base (directly or via a toolkit import).\n */\nexport function isBaseSubclass(node: Node, context: RuleContext): boolean {\n const superName = getSuperClassName(node);\n if (!superName) return false;\n if (superName === 'Base') return true;\n\n const sourceCode = context.sourceCode ?? context.getSourceCode?.();\n const ast = sourceCode?.ast;\n if (!ast) return false;\n\n for (const n of ast.body) {\n if (n.type !== 'ImportDeclaration') continue;\n if (n.source.value !== TOOLKIT_PACKAGE) continue;\n for (const specifier of n.specifiers) {\n if (specifier.type === 'ImportSpecifier' && specifier.local.name === superName) return true;\n }\n }\n\n return false;\n}\n\n/**\n * Returns true when a class extends a component imported from @studiometa/ui.\n */\nexport function isUISubclass(node: Node, context: RuleContext): boolean {\n const superName = getSuperClassName(node);\n if (!superName) return false;\n\n const sourceCode = context.sourceCode ?? context.getSourceCode?.();\n const ast = sourceCode?.ast;\n if (!ast) return false;\n\n for (const n of ast.body) {\n if (n.type !== 'ImportDeclaration') continue;\n if (n.source.value !== UI_PACKAGE) continue;\n for (const specifier of n.specifiers) {\n if (specifier.type === 'ImportSpecifier' && specifier.local.name === superName) return true;\n }\n }\n\n return false;\n}\n\n/**\n * Returns true when className is already imported from @studiometa/ui.\n */\nexport function isImportedFromUI(className: string, context: RuleContext): boolean {\n const sourceCode = context.sourceCode ?? context.getSourceCode?.();\n const ast = sourceCode?.ast;\n if (!ast) return false;\n\n for (const n of ast.body) {\n if (n.type !== 'ImportDeclaration') continue;\n if (n.source.value !== UI_PACKAGE) continue;\n for (const specifier of n.specifiers) {\n if (specifier.type === 'ImportSpecifier' && specifier.imported?.name === className)\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Walks up ancestor nodes to find the nearest class declaration/expression.\n */\nexport function findEnclosingClass(ancestors: Node[]): Node | null {\n for (let i = ancestors.length - 1; i >= 0; i--) {\n const node = ancestors[i];\n if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') return node;\n }\n return null;\n}\n\nexport function getAncestors(context: RuleContext, node: Node): Node[] {\n return context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];\n}\n\n/**\n * Returns all method definitions in a class body by name.\n */\nexport function getClassMethods(classNode: Node): Map<string, Node> {\n const methods = new Map<string, Node>();\n for (const member of classNode.body?.body ?? []) {\n if (member.type === 'MethodDefinition' && member.key?.type === 'Identifier') {\n methods.set(member.key.name, member);\n }\n }\n return methods;\n}\n", "import {\n UI_COMPONENT_NAMES,\n isBaseSubclass,\n isImportedFromUI,\n type Node,\n type RuleContext,\n createRule,\n} from '../utils/ast.ts';\n\nexport const preferUiComponent = createRule({\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Prefer importing components from @studiometa/ui over reimplementing them from scratch',\n },\n messages: {\n preferImport:\n '\"{{name}}\" is available in @studiometa/ui. ' +\n 'Extend it instead of reimplementing from Base: import { {{name}} } from \\'@studiometa/ui\\'.',\n },\n },\n createOnce(context: RuleContext) {\n return {\n ClassDeclaration(node: Node) {\n check(node, context);\n },\n ClassExpression(node: Node) {\n check(node, context);\n },\n };\n },\n});\n\nfunction check(node: Node, context: RuleContext) {\n const name: string = node.id?.name ?? '';\n if (!name || !UI_COMPONENT_NAMES.has(name)) return;\n if (!isBaseSubclass(node, context)) return;\n // Already importing it from @studiometa/ui \u2014 they're extending it properly\n if (isImportedFromUI(name, context)) return;\n\n context.report({ node: node.id ?? node, messageId: 'preferImport', data: { name } });\n}\n", "import {\n isBaseSubclass,\n isImportedFromUI,\n getClassMethods,\n type Node,\n type RuleContext,\n createRule,\n} from '../utils/ast.ts';\n\n/**\n * Detect classes that extend Base and define both open() and close() methods \u2014\n * a pattern that duplicates what Transition (or Modal) already provides.\n */\nexport const preferTransition = createRule({\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Prefer @studiometa/ui Transition over manually implementing open/close logic in a Base subclass',\n },\n messages: {\n preferTransition:\n 'This class manually implements open() and close() methods. ' +\n 'Consider extending Transition from @studiometa/ui instead, which handles show/hide with CSS class transitions: ' +\n \"import { Transition } from '@studiometa/ui'.\",\n },\n },\n createOnce(context: RuleContext) {\n return {\n ClassDeclaration(node: Node) {\n check(node, context);\n },\n ClassExpression(node: Node) {\n check(node, context);\n },\n };\n },\n});\n\nfunction check(node: Node, context: RuleContext) {\n if (!isBaseSubclass(node, context)) return;\n // Already using a @studiometa/ui component as the base\n if (isImportedFromUI(node.superClass?.name, context)) return;\n\n const methods = getClassMethods(node);\n if (!methods.has('open') || !methods.has('close')) return;\n\n context.report({ node: node.id ?? node, messageId: 'preferTransition' });\n}\n", "import {\n findEnclosingClass,\n isBaseSubclass,\n isImportedFromUI,\n getAncestors,\n type Node,\n type RuleContext,\n createRule,\n} from '../utils/ast.ts';\n\nconst DOM_WRITE_METHODS = new Set(['insertAdjacentHTML', 'insertAdjacentElement']);\n\n/**\n * Detect manual fetch() calls inside Base subclasses that also write to innerHTML\n * or use insertAdjacentHTML \u2014 suggest using the Fetch component from @studiometa/ui.\n */\nexport const noManualFetch = createRule({\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Prefer the Fetch component from @studiometa/ui over manually fetching and injecting HTML',\n },\n messages: {\n preferFetch:\n 'Manual fetch() with DOM injection detected. ' +\n \"Consider using the Fetch component from @studiometa/ui instead: import { Fetch } from '@studiometa/ui'.\",\n },\n },\n createOnce(context: RuleContext) {\n const classesWithFetch = new WeakSet<Node>();\n const classesWithDomWrite = new WeakSet<Node>();\n const reported = new WeakSet<Node>();\n\n function getEnclosingBaseClass(node: Node): Node | null {\n const ancestors = getAncestors(context, node);\n const cls = findEnclosingClass(ancestors);\n if (!cls || !isBaseSubclass(cls, context)) return null;\n if (isImportedFromUI('Fetch', context)) return null;\n return cls;\n }\n\n function maybeReport(cls: Node) {\n if (reported.has(cls)) return;\n if (classesWithFetch.has(cls) && classesWithDomWrite.has(cls)) {\n reported.add(cls);\n context.report({ node: cls.id ?? cls, messageId: 'preferFetch' });\n }\n }\n\n return {\n CallExpression(node: Node) {\n const callee = node.callee;\n\n // Detect fetch(url)\n const isFetch =\n (callee.type === 'Identifier' && callee.name === 'fetch') ||\n (callee.type === 'MemberExpression' &&\n callee.property?.name === 'fetch' &&\n callee.object?.type === 'ThisExpression');\n\n // Detect el.insertAdjacentHTML / el.insertAdjacentElement\n const isDomWrite =\n callee.type === 'MemberExpression' &&\n DOM_WRITE_METHODS.has(callee.property?.name);\n\n if (!isFetch && !isDomWrite) return;\n\n const cls = getEnclosingBaseClass(node);\n if (!cls) return;\n\n if (isFetch) classesWithFetch.add(cls);\n if (isDomWrite) classesWithDomWrite.add(cls);\n\n maybeReport(cls);\n },\n\n AssignmentExpression(node: Node) {\n const left = node.left;\n if (left.type !== 'MemberExpression') return;\n if (left.property?.name !== 'innerHTML') return;\n\n const cls = getEnclosingBaseClass(node);\n if (!cls) return;\n\n classesWithDomWrite.add(cls);\n maybeReport(cls);\n },\n };\n },\n});\n", "import {\n findEnclosingClass,\n isBaseSubclass,\n isImportedFromUI,\n getAncestors,\n type Node,\n type RuleContext,\n createRule,\n} from '../utils/ast.ts';\n\n/**\n * Detect manual input/change event listeners that update DOM \u2014 suggest DataModel/DataEffect.\n *\n * Signal: addEventListener('input', ...) or an onInput* method that writes to\n * textContent / innerHTML / value on another element inside a Base subclass.\n */\n/** Walk up a member expression chain and check if the root is this.$refs / this.$el / this.$children */\nfunction isThisComponentAccess(node: Node): boolean {\n let current = node;\n while (current?.type === 'MemberExpression') {\n if (\n current.object?.type === 'ThisExpression' &&\n (current.property?.name === '$refs' ||\n current.property?.name === '$el' ||\n current.property?.name === '$children')\n ) {\n return true;\n }\n current = current.object;\n }\n return false;\n}\n\nexport const preferDataModel = createRule({\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Prefer DataModel/DataEffect from @studiometa/ui over manually syncing input values to the DOM',\n },\n messages: {\n preferDataModel:\n 'Manual input\u2192DOM synchronization detected. ' +\n 'Consider using DataModel and DataEffect from @studiometa/ui for reactive bindings: ' +\n \"import { DataModel, DataEffect } from '@studiometa/ui'.\",\n },\n },\n createOnce(context: RuleContext) {\n const classesWithInputListener = new WeakSet<Node>();\n const classesWithDomWrite = new WeakSet<Node>();\n const reported = new WeakSet<Node>();\n\n function getEnclosingBaseClass(node: Node): Node | null {\n const ancestors = getAncestors(context, node);\n const cls = findEnclosingClass(ancestors);\n if (!cls || !isBaseSubclass(cls, context)) return null;\n if (isImportedFromUI('DataModel', context) || isImportedFromUI('DataEffect', context))\n return null;\n return cls;\n }\n\n function maybeReport(cls: Node) {\n if (reported.has(cls)) return;\n if (classesWithInputListener.has(cls) && classesWithDomWrite.has(cls)) {\n reported.add(cls);\n context.report({ node: cls.id ?? cls, messageId: 'preferDataModel' });\n }\n }\n\n return {\n // Detect on-handler methods named onInput*, onChange*, onModelInput, etc.\n MethodDefinition(node: Node) {\n const name: string = node.key?.name ?? '';\n if (!/^on.*(Input|Change|Model)/i.test(name)) return;\n\n const ancestors = getAncestors(context, node);\n const cls = findEnclosingClass(ancestors);\n if (!cls || !isBaseSubclass(cls, context)) return;\n if (isImportedFromUI('DataModel', context)) return;\n\n classesWithInputListener.add(cls);\n maybeReport(cls);\n },\n\n AssignmentExpression(node: Node) {\n const left = node.left;\n if (left.type !== 'MemberExpression') return;\n\n const prop = left.property?.name;\n const isDomWrite =\n prop === 'textContent' || prop === 'innerHTML' || prop === 'value';\n if (!isDomWrite) return;\n\n // Must ultimately access this.$refs, this.$el, or this.$children\n if (!isThisComponentAccess(left.object)) return;\n\n const cls = getEnclosingBaseClass(node);\n if (!cls) return;\n\n classesWithDomWrite.add(cls);\n maybeReport(cls);\n },\n };\n },\n});\n", "import {\n isBaseSubclass,\n getClassMethods,\n isImportedFromUI,\n type Node,\n type RuleContext,\n createRule,\n} from '../utils/ast.ts';\n\n/**\n * Simple event handler method names that the Action component already handles.\n * These are on-handler names on this.$el (root element) \u2014 if the class body\n * contains ONLY these kinds of methods (plus config), it's a good Action candidate.\n */\nconst ACTION_EVENT_METHODS = new Set([\n 'onClick',\n 'onMouseenter',\n 'onMouseleave',\n 'onMouseover',\n 'onMouseout',\n 'onFocus',\n 'onBlur',\n 'onKeydown',\n 'onKeyup',\n 'onKeypress',\n 'onPointerenter',\n 'onPointerleave',\n 'onPointerdown',\n 'onPointerup',\n]);\n\nconst NON_LOGIC_METHODS = new Set(['mounted', 'destroyed', 'updated']);\n\n/**\n * Detect Base subclasses whose only logic is a single simple event handler\n * (onClick, onMouseenter, etc.) \u2014 these can be replaced by the Action component.\n */\nexport const preferAction = createRule({\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Prefer the Action component from @studiometa/ui for simple event-triggered effects',\n },\n messages: {\n preferAction:\n 'This component only handles \"{{method}}\" on the root element. ' +\n 'Consider using the Action component from @studiometa/ui instead, ' +\n \"which declaratively wires events to effects via data attributes: import { Action } from '@studiometa/ui'.\",\n },\n },\n createOnce(context: RuleContext) {\n return {\n ClassDeclaration(node: Node) {\n check(node, context);\n },\n ClassExpression(node: Node) {\n check(node, context);\n },\n };\n },\n});\n\nfunction check(node: Node, context: RuleContext) {\n if (!isBaseSubclass(node, context)) return;\n if (isImportedFromUI('Action', context)) return;\n\n const methods = getClassMethods(node);\n\n // Find action-handler methods defined in this class\n const actionMethods: string[] = [];\n const otherMethods: string[] = [];\n\n for (const [name] of methods) {\n if (ACTION_EVENT_METHODS.has(name)) {\n actionMethods.push(name);\n } else if (name !== 'config' && !NON_LOGIC_METHODS.has(name)) {\n otherMethods.push(name);\n }\n }\n\n // Only flag when: exactly one action method, no other logic methods\n if (actionMethods.length !== 1 || otherMethods.length > 0) return;\n\n // Check the method body is simple \u2014 does not access this.$children or this.$refs\n // to avoid false positives on components that orchestrate children\n const method = methods.get(actionMethods[0])!;\n if (hasComplexBody(method)) return;\n\n context.report({\n node: node.id ?? node,\n messageId: 'preferAction',\n data: { method: actionMethods[0] },\n });\n}\n\nfunction hasComplexBody(methodNode: Node): boolean {\n let complex = false;\n\n function walk(n: Node) {\n if (complex) return;\n if (!n || typeof n !== 'object') return;\n\n // Accessing $children or using await suggests complex logic\n if (\n n.type === 'MemberExpression' &&\n n.object?.type === 'ThisExpression' &&\n (n.property?.name === '$children' || n.property?.name === '$root')\n ) {\n complex = true;\n return;\n }\n\n for (const key of Object.keys(n)) {\n if (key === 'parent') continue;\n const child = n[key];\n if (Array.isArray(child)) child.forEach(walk);\n else if (child && typeof child === 'object' && child.type) walk(child);\n }\n }\n\n walk(methodNode.value ?? methodNode);\n return complex;\n}\n", "import { eslintCompatPlugin } from '@oxlint/plugins';\nimport {\n preferUiComponent,\n preferTransition,\n noManualFetch,\n preferDataModel,\n preferAction,\n} from './rules/index.ts';\n\nconst PLUGIN_NAME = 'ui';\n\nconst rules = {\n 'prefer-ui-component': preferUiComponent,\n 'prefer-transition': preferTransition,\n 'no-manual-fetch': noManualFetch,\n 'prefer-data-model': preferDataModel,\n 'prefer-action': preferAction,\n};\n\nconst recommendedRules: Record<string, string> = {\n [`${PLUGIN_NAME}/prefer-ui-component`]: 'warn',\n [`${PLUGIN_NAME}/prefer-transition`]: 'warn',\n [`${PLUGIN_NAME}/no-manual-fetch`]: 'warn',\n [`${PLUGIN_NAME}/prefer-data-model`]: 'warn',\n [`${PLUGIN_NAME}/prefer-action`]: 'warn',\n};\n\nconst base = eslintCompatPlugin({\n meta: {\n name: '@studiometa/eslint-plugin-ui',\n },\n rules,\n});\n\nconst plugin = Object.assign(base, { configs: {} as Record<string, object> });\n\nplugin.configs['recommended'] = {\n plugins: { [PLUGIN_NAME]: plugin },\n rules: recommendedRules,\n};\n\nexport default plugin;\nexport { plugin as ui };\n"],
5
+ "mappings": "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",
6
+ "names": ["plugin", "rules", "check", "check"]
7
+ }
@@ -0,0 +1,5 @@
1
+ export { preferUiComponent } from './prefer-ui-component.ts';
2
+ export { preferTransition } from './prefer-transition.ts';
3
+ export { noManualFetch } from './no-manual-fetch.ts';
4
+ export { preferDataModel } from './prefer-data-model.ts';
5
+ export { preferAction } from './prefer-action.ts';
@@ -0,0 +1,12 @@
1
+ import { type Node, type RuleContext } from '../utils/ast.ts';
2
+ /**
3
+ * Detect manual fetch() calls inside Base subclasses that also write to innerHTML
4
+ * or use insertAdjacentHTML — suggest using the Fetch component from @studiometa/ui.
5
+ */
6
+ export declare const noManualFetch: {
7
+ meta?: import("../utils/ast.ts").RuleMeta;
8
+ createOnce(context: RuleContext): {
9
+ CallExpression(node: Node): void;
10
+ AssignmentExpression(node: Node): void;
11
+ };
12
+ };
@@ -0,0 +1,12 @@
1
+ import { type Node, type RuleContext } from '../utils/ast.ts';
2
+ /**
3
+ * Detect Base subclasses whose only logic is a single simple event handler
4
+ * (onClick, onMouseenter, etc.) — these can be replaced by the Action component.
5
+ */
6
+ export declare const preferAction: {
7
+ meta?: import("../utils/ast.ts").RuleMeta;
8
+ createOnce(context: RuleContext): {
9
+ ClassDeclaration(node: Node): void;
10
+ ClassExpression(node: Node): void;
11
+ };
12
+ };
@@ -0,0 +1,8 @@
1
+ import { type Node, type RuleContext } from '../utils/ast.ts';
2
+ export declare const preferDataModel: {
3
+ meta?: import("../utils/ast.ts").RuleMeta;
4
+ createOnce(context: RuleContext): {
5
+ MethodDefinition(node: Node): void;
6
+ AssignmentExpression(node: Node): void;
7
+ };
8
+ };
@@ -0,0 +1,12 @@
1
+ import { type Node, type RuleContext } from '../utils/ast.ts';
2
+ /**
3
+ * Detect classes that extend Base and define both open() and close() methods —
4
+ * a pattern that duplicates what Transition (or Modal) already provides.
5
+ */
6
+ export declare const preferTransition: {
7
+ meta?: import("../utils/ast.ts").RuleMeta;
8
+ createOnce(context: RuleContext): {
9
+ ClassDeclaration(node: Node): void;
10
+ ClassExpression(node: Node): void;
11
+ };
12
+ };
@@ -0,0 +1,8 @@
1
+ import { type Node, type RuleContext } from '../utils/ast.ts';
2
+ export declare const preferUiComponent: {
3
+ meta?: import("../utils/ast.ts").RuleMeta;
4
+ createOnce(context: RuleContext): {
5
+ ClassDeclaration(node: Node): void;
6
+ ClassExpression(node: Node): void;
7
+ };
8
+ };
@@ -0,0 +1,50 @@
1
+ export type Node = Record<string, any>;
2
+ export type RuleContext = Record<string, any>;
3
+ export type RuleMeta = {
4
+ type?: 'problem' | 'suggestion' | 'layout';
5
+ fixable?: 'code' | 'whitespace';
6
+ hasSuggestions?: boolean;
7
+ docs?: {
8
+ description?: string;
9
+ };
10
+ messages?: Record<string, string>;
11
+ };
12
+ export declare function createRule<V extends Record<string, (node: Node) => unknown>>(rule: {
13
+ meta?: RuleMeta;
14
+ createOnce(context: RuleContext): V;
15
+ }): {
16
+ meta?: RuleMeta;
17
+ createOnce(context: RuleContext): V;
18
+ };
19
+ export declare const UI_PACKAGE = "@studiometa/ui";
20
+ export declare const TOOLKIT_PACKAGE = "@studiometa/js-toolkit";
21
+ /**
22
+ * All component names exported by @studiometa/ui.
23
+ * Keep in sync with packages/ui/index.ts exports.
24
+ */
25
+ export declare const UI_COMPONENT_NAMES: Set<string>;
26
+ /**
27
+ * Returns the local name of the superclass for a class declaration/expression.
28
+ */
29
+ export declare function getSuperClassName(node: Node): string | null;
30
+ /**
31
+ * Returns true when a class extends Base (directly or via a toolkit import).
32
+ */
33
+ export declare function isBaseSubclass(node: Node, context: RuleContext): boolean;
34
+ /**
35
+ * Returns true when a class extends a component imported from @studiometa/ui.
36
+ */
37
+ export declare function isUISubclass(node: Node, context: RuleContext): boolean;
38
+ /**
39
+ * Returns true when className is already imported from @studiometa/ui.
40
+ */
41
+ export declare function isImportedFromUI(className: string, context: RuleContext): boolean;
42
+ /**
43
+ * Walks up ancestor nodes to find the nearest class declaration/expression.
44
+ */
45
+ export declare function findEnclosingClass(ancestors: Node[]): Node | null;
46
+ export declare function getAncestors(context: RuleContext, node: Node): Node[];
47
+ /**
48
+ * Returns all method definitions in a class body by name.
49
+ */
50
+ export declare function getClassMethods(classNode: Node): Map<string, Node>;
@@ -0,0 +1,6 @@
1
+ import { RuleTester } from 'eslint';
2
+ import { eslintCompatPlugin } from '@oxlint/plugins';
3
+ export { RuleTester };
4
+ export declare const tester: {
5
+ run(name: string, rule: Parameters<typeof eslintCompatPlugin>[0]["rules"][string], tests: Parameters<InstanceType<typeof RuleTester>["run"]>[2]): void;
6
+ };
package/package.json CHANGED
@@ -1,11 +1,47 @@
1
1
  {
2
2
  "name": "@studiometa/eslint-plugin-ui",
3
- "version": "0.1.0",
3
+ "version": "1.9.0-beta.0",
4
4
  "description": "ESLint plugin to help developers discover and use @studiometa/ui components",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "author": "Studio Meta <agence@studiometa.fr> (https://www.studiometa.fr)",
9
9
  "license": "MIT",
10
- "type": "module"
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/studiometa/ui",
13
+ "directory": "packages/eslint-plugin-ui"
14
+ },
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "main": "./dist/index.js",
22
+ "module": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "import": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
31
+ "scripts": {
32
+ "build": "node scripts/build.js",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest",
35
+ "lint:types": "tsgo --build tsconfig.json"
36
+ },
37
+ "devDependencies": {
38
+ "@vitest/coverage-v8": "4.1.2",
39
+ "esbuild": "0.27.4",
40
+ "eslint": "9.34.0",
41
+ "typescript": "6.0.2",
42
+ "vitest": "4.1.2"
43
+ },
44
+ "dependencies": {
45
+ "@oxlint/plugins": "1.63.0"
46
+ }
11
47
  }