@studiometa/eslint-plugin-js-toolkit 3.9.0 → 4.0.0-alpha.1

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.
Files changed (38) hide show
  1. package/README.md +78 -1
  2. package/dist/index.d.ts +5 -3
  3. package/dist/index.js +2061 -1537
  4. package/dist/index.js.map +1 -7
  5. package/package.json +7 -7
  6. package/dist/rules/async-lifecycle-methods.d.ts +0 -7
  7. package/dist/rules/components-pascal-case.d.ts +0 -8
  8. package/dist/rules/emits-kebab-case.d.ts +0 -8
  9. package/dist/rules/emits-multi-word.d.ts +0 -8
  10. package/dist/rules/index.d.ts +0 -30
  11. package/dist/rules/no-create-app.d.ts +0 -7
  12. package/dist/rules/no-deep-utils-import.d.ts +0 -7
  13. package/dist/rules/no-deprecated-properties.d.ts +0 -7
  14. package/dist/rules/no-dispatch-event.d.ts +0 -7
  15. package/dist/rules/no-dollar-prefix.d.ts +0 -8
  16. package/dist/rules/no-event-listener-methods.d.ts +0 -7
  17. package/dist/rules/no-internal-barrel-import.d.ts +0 -7
  18. package/dist/rules/no-manual-intersection-observer.d.ts +0 -7
  19. package/dist/rules/no-manual-mutation-observer.d.ts +0 -7
  20. package/dist/rules/no-redundant-with-mount-when-in-view.d.ts +0 -7
  21. package/dist/rules/no-shadow-dom.d.ts +0 -7
  22. package/dist/rules/on-global-handler-prefix.d.ts +0 -7
  23. package/dist/rules/on-handler-naming.d.ts +0 -7
  24. package/dist/rules/options-camel-case.d.ts +0 -8
  25. package/dist/rules/prefer-destructured-lookups.d.ts +0 -8
  26. package/dist/rules/prefer-ref-over-query-selector.d.ts +0 -7
  27. package/dist/rules/refs-camel-case.d.ts +0 -8
  28. package/dist/rules/refs-no-bracket-access.d.ts +0 -7
  29. package/dist/rules/refs-plural-multiple.d.ts +0 -8
  30. package/dist/rules/require-children-declared-in-config.d.ts +0 -7
  31. package/dist/rules/require-config-name-pascal-case.d.ts +0 -8
  32. package/dist/rules/require-config.d.ts +0 -8
  33. package/dist/rules/require-destroyed-cleanup.d.ts +0 -12
  34. package/dist/rules/require-emit-declared-in-config.d.ts +0 -7
  35. package/dist/rules/require-options-declared-in-config.d.ts +0 -7
  36. package/dist/rules/require-refs-declared-in-config.d.ts +0 -7
  37. package/dist/utils/ast.d.ts +0 -51
  38. package/dist/utils/rule-tester.d.ts +0 -6
package/dist/index.js CHANGED
@@ -1,1631 +1,2155 @@
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
1
+ import { eslintCompatPlugin } from "@oxlint/plugins";
2
+ //#region src/utils/ast.ts
181
3
  function createRule(rule) {
182
- return rule;
4
+ return rule;
183
5
  }
184
- var LIFECYCLE_METHODS = /* @__PURE__ */ new Set([
185
- "mounted",
186
- "destroyed",
187
- "updated",
188
- "terminated",
189
- "ticked",
190
- "scrolled",
191
- "resized",
192
- "moved",
193
- "loaded",
194
- "keyed"
6
+ const LIFECYCLE_METHODS = /* @__PURE__ */ new Set([
7
+ "mounted",
8
+ "destroyed",
9
+ "updated",
10
+ "terminated",
11
+ "ticked",
12
+ "scrolled",
13
+ "resized",
14
+ "moved",
15
+ "loaded",
16
+ "keyed"
195
17
  ]);
196
- var TOOLKIT_PACKAGE = "@studiometa/js-toolkit";
18
+ /**
19
+ * Returns true if a class declaration extends an identifier imported from
20
+ * @studiometa/js-toolkit, or extends a bare identifier named `Base`.
21
+ *
22
+ * This is a heuristic — we cannot do full type resolution in a lint rule.
23
+ */
197
24
  function isBaseSubclass(node, context) {
198
- if (node.type !== "ClassDeclaration" && node.type !== "ClassExpression") {
199
- return false;
200
- }
201
- if (!node.superClass) {
202
- return false;
203
- }
204
- const superName = node.superClass.type === "Identifier" ? node.superClass.name : null;
205
- if (!superName) {
206
- return false;
207
- }
208
- if (superName === "Base") {
209
- return true;
210
- }
211
- const sourceCode = context.sourceCode ?? context.getSourceCode?.();
212
- const ast = sourceCode?.ast;
213
- if (!ast) {
214
- return false;
215
- }
216
- for (const node2 of ast.body) {
217
- if (node2.type !== "ImportDeclaration") continue;
218
- if (node2.source.value !== TOOLKIT_PACKAGE) continue;
219
- for (const specifier of node2.specifiers) {
220
- if (specifier.type === "ImportSpecifier" && specifier.local.name === superName) {
221
- return true;
222
- }
223
- }
224
- }
225
- return false;
25
+ if (node.type !== "ClassDeclaration" && node.type !== "ClassExpression") return false;
26
+ if (!node.superClass) return false;
27
+ const superName = node.superClass.type === "Identifier" ? node.superClass.name : null;
28
+ if (!superName) return false;
29
+ if (superName === "Base") return true;
30
+ const ast = (context.sourceCode ?? context.getSourceCode?.())?.ast;
31
+ if (!ast) return false;
32
+ for (const node of ast.body) {
33
+ if (node.type !== "ImportDeclaration") continue;
34
+ if (node.source.value !== "@studiometa/js-toolkit") continue;
35
+ for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier" && specifier.local.name === superName) return true;
36
+ }
37
+ return false;
226
38
  }
39
+ /**
40
+ * Returns true if the string is camelCase (starts lowercase, no separators).
41
+ */
227
42
  function isCamelCase(value) {
228
- return /^[a-z][a-zA-Z0-9]*$/.test(value);
43
+ return /^[a-z][a-zA-Z0-9]*$/.test(value);
229
44
  }
45
+ /**
46
+ * Returns true if the string is PascalCase.
47
+ */
230
48
  function isPascalCase(value) {
231
- return /^[A-Z][a-zA-Z0-9]*$/.test(value);
49
+ return /^[A-Z][a-zA-Z0-9]*$/.test(value);
232
50
  }
51
+ /**
52
+ * Converts a kebab-case, snake_case or PascalCase string to camelCase.
53
+ */
233
54
  function toCamelCase(value) {
234
- return value.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase());
55
+ return value.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase());
235
56
  }
57
+ /**
58
+ * Converts a kebab-case, snake_case or camelCase string to PascalCase.
59
+ */
236
60
  function toPascalCase(value) {
237
- return value.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase());
61
+ return value.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase());
238
62
  }
63
+ /**
64
+ * Walks up ancestor nodes to find the nearest class declaration/expression.
65
+ */
239
66
  function findEnclosingClass(ancestors) {
240
- for (let i = ancestors.length - 1; i >= 0; i--) {
241
- const node = ancestors[i];
242
- if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
243
- return node;
244
- }
245
- }
246
- return null;
67
+ for (let i = ancestors.length - 1; i >= 0; i--) {
68
+ const node = ancestors[i];
69
+ if (node.type === "ClassDeclaration" || node.type === "ClassExpression") return node;
70
+ }
71
+ return null;
247
72
  }
248
-
249
- // src/rules/require-config.ts
250
- var requireConfig = createRule({
251
- meta: {
252
- type: "problem",
253
- docs: {
254
- description: "Require a static config property on classes extending Base"
255
- },
256
- messages: {
257
- missingConfig: "Classes extending Base must define a static config property.",
258
- missingName: "The static config must include a name property."
259
- }
260
- },
261
- createOnce(context) {
262
- return {
263
- ClassDeclaration(node) {
264
- check(node, context);
265
- },
266
- ClassExpression(node) {
267
- check(node, context);
268
- }
269
- };
270
- }
73
+ /**
74
+ * Returns the ancestors of a node, whichever accessor the runtime exposes.
75
+ */
76
+ function getAncestors(node, context) {
77
+ return context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
78
+ }
79
+ /**
80
+ * Returns true for the node types that open a new function scope.
81
+ */
82
+ function isFunctionNode(node) {
83
+ return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
84
+ }
85
+ /**
86
+ * Walks every descendant node, depth first, calling `visit` on each.
87
+ *
88
+ * Returning `false` from `visit` skips the subtree of that node. Used by rules
89
+ * which have to analyse a node the visitor will not hand them again, such as
90
+ * the body of a method resolved from its call site.
91
+ */
92
+ function walk(node, visit) {
93
+ if (!node || typeof node.type !== "string") return;
94
+ if (visit(node) === false) return;
95
+ for (const key of Object.keys(node)) {
96
+ if (key === "parent") continue;
97
+ const value = node[key];
98
+ if (Array.isArray(value)) {
99
+ for (const item of value) if (item && typeof item.type === "string") walk(item, visit);
100
+ } else if (value && typeof value.type === "string") walk(value, visit);
101
+ }
102
+ }
103
+ /**
104
+ * Returns the name of a class member key, for identifier and literal keys alike.
105
+ */
106
+ function getKeyName(node) {
107
+ const key = node?.key;
108
+ if (!key) return null;
109
+ if (key.type === "Identifier") return key.name;
110
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
111
+ return null;
112
+ }
113
+ /**
114
+ * Returns true if a class member carries one of the named decorators, written
115
+ * either bare (`@write`) or called (`@write()`).
116
+ */
117
+ function hasDecorator(node, names) {
118
+ for (const decorator of node?.decorators ?? []) {
119
+ const expression = decorator.expression;
120
+ if (!expression) continue;
121
+ if (expression.type === "Identifier" && names.has(expression.name)) return true;
122
+ if (expression.type === "CallExpression" && expression.callee?.type === "Identifier" && names.has(expression.callee.name)) return true;
123
+ }
124
+ return false;
125
+ }
126
+ /**
127
+ * Unwraps the type-argument application a v4 component writes on its
128
+ * superclass, as in `extends withRaf(Base)<Props>`.
129
+ */
130
+ function unwrapTypeArguments(node) {
131
+ return node?.type === "TSInstantiationExpression" ? node.expression ?? node : node;
132
+ }
133
+ /**
134
+ * The instance surface only a v4 component has.
135
+ *
136
+ * Reaching for one of these off `this` is the signal that a class extending
137
+ * something unresolvable is a component rather than an ordinary subclass. The
138
+ * lifecycle and service hook names are deliberately **not** in this set: they
139
+ * are the very thing a rule like `no-write-in-read-phase` keys on, so counting
140
+ * them as proof would make the test circular and let `class Store extends
141
+ * Error { ticked() {} }` pass for a component.
142
+ */
143
+ const COMPONENT_MEMBERS = /* @__PURE__ */ new Set([
144
+ "$el",
145
+ "$refs",
146
+ "$options",
147
+ "$id",
148
+ "$config",
149
+ "$isMounted",
150
+ "$emit",
151
+ "$on",
152
+ "$off",
153
+ "$query",
154
+ "$closest",
155
+ "$watchChildren",
156
+ "$provide",
157
+ "$inject",
158
+ "$injectSync",
159
+ "$read",
160
+ "$write",
161
+ "$mount",
162
+ "$unmount",
163
+ "$destroy",
164
+ "$services"
165
+ ]);
166
+ /**
167
+ * Returns true if the class body reaches for the framework surface off `this`.
168
+ */
169
+ function usesComponentSurface(node) {
170
+ let found = false;
171
+ walk(node.body, (inner) => {
172
+ if (found) return false;
173
+ if (inner !== node.body && (inner.type === "ClassDeclaration" || inner.type === "ClassExpression")) return false;
174
+ if (inner.type === "MemberExpression" && !inner.computed && inner.object?.type === "ThisExpression" && COMPONENT_MEMBERS.has(inner.property?.name)) {
175
+ found = true;
176
+ return false;
177
+ }
178
+ });
179
+ return found;
180
+ }
181
+ /**
182
+ * Returns true if a class looks like a v4 component.
183
+ *
184
+ * A component always extends something — `Base`, a `with<Service>(…)` mixin,
185
+ * or a local abstract component — so a superclass is necessary. It is nowhere
186
+ * near sufficient: `class Store extends Error` would read as a component, and
187
+ * `prefer-instance-scheduler` would then tell it to call a `this.$read()` it
188
+ * does not have, which is wrong advice rather than mere noise.
189
+ *
190
+ * Restricting to a list of known bases is not the answer either. `extends
191
+ * AbstractCarouselChild`, `extends Indexable` and `extends AbstractPrefetch`
192
+ * are the common shape in `@studiometa/ui`, and a single-file linter cannot
193
+ * follow the chain to `Base`.
194
+ *
195
+ * So a superclass **plus** a second signal in the same class body: a
196
+ * `@component({ … })` decorator, a `static config`, or use of the framework
197
+ * surface off `this`. That keeps a subclass of a local abstract component —
198
+ * which reads `this.$el` or emits — while rejecting a class that merely
199
+ * extends something.
200
+ *
201
+ * Requiring the superclass is what keeps `Base` itself out. It declares
202
+ * `static config` and defines the whole surface, but it extends nothing, and
203
+ * the class defining `$read()` is not a class that should be told to call it.
204
+ *
205
+ * A component using none of the three anywhere in its body is missed, which
206
+ * is the right way to be wrong: a false negative rather than advice that does
207
+ * not apply.
208
+ */
209
+ function isComponentClass(node) {
210
+ if (node?.type !== "ClassDeclaration" && node?.type !== "ClassExpression") return false;
211
+ if (!unwrapTypeArguments(node.superClass)) return false;
212
+ return hasDecorator(node, /* @__PURE__ */ new Set(["component"])) || Boolean(findStaticConfig(node)) || usesComponentSurface(node);
213
+ }
214
+ /** The object literal passed to a class's `@component({ … })` decorator. */
215
+ function findDecoratorConfig(node) {
216
+ for (const decorator of node?.decorators ?? []) {
217
+ const expression = decorator.expression;
218
+ if (expression?.type !== "CallExpression") continue;
219
+ if (expression.callee?.type !== "Identifier") continue;
220
+ if (expression.callee.name !== "component") continue;
221
+ const argument = expression.arguments?.[0];
222
+ if (argument?.type === "ObjectExpression") return argument;
223
+ }
224
+ return null;
225
+ }
226
+ /** The `static config = { … }` object of a class. */
227
+ function findStaticConfig(node) {
228
+ for (const member of node?.body?.body ?? []) {
229
+ if (member.type !== "PropertyDefinition") continue;
230
+ if (member.static !== true) continue;
231
+ if (member.key?.name !== "config") continue;
232
+ if (member.value?.type === "ObjectExpression") return member.value;
233
+ }
234
+ return null;
235
+ }
236
+ /**
237
+ * Finds the `static config = { … }` object of a class, or the object literal
238
+ * passed to its `@component({ … })` decorator. v4 accepts both spellings.
239
+ */
240
+ function findComponentConfig(node) {
241
+ return findDecoratorConfig(node) ?? findStaticConfig(node);
242
+ }
243
+ /**
244
+ * Returns the value of a named property of an object literal.
245
+ */
246
+ function getProperty(node, name) {
247
+ for (const property of node?.properties ?? []) {
248
+ if (property.type !== "Property") continue;
249
+ if (getKeyName(property) === name) return property.value ?? null;
250
+ }
251
+ return null;
252
+ }
253
+ //#endregion
254
+ //#region src/rules/require-config.ts
255
+ const requireConfig = createRule({
256
+ meta: {
257
+ type: "problem",
258
+ docs: { description: "Require a static config property on classes extending Base" },
259
+ messages: {
260
+ missingConfig: "Classes extending Base must define a static config property.",
261
+ missingName: "The static config must include a name property."
262
+ }
263
+ },
264
+ createOnce(context) {
265
+ return {
266
+ ClassDeclaration(node) {
267
+ check$9(node, context);
268
+ },
269
+ ClassExpression(node) {
270
+ check$9(node, context);
271
+ }
272
+ };
273
+ }
271
274
  });
272
- function check(node, context) {
273
- if (!isBaseSubclass(node, context)) return;
274
- const body = node.body?.body ?? [];
275
- const configProp = body.find(
276
- (member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config"
277
- );
278
- if (!configProp) {
279
- context.report({ node, messageId: "missingConfig" });
280
- return;
281
- }
282
- const value = configProp.value;
283
- if (!value || value.type !== "ObjectExpression") return;
284
- const hasName = value.properties?.some(
285
- (prop) => prop.type === "Property" && prop.key?.name === "name" && prop.value?.value
286
- );
287
- if (!hasName) {
288
- context.report({ node: configProp, messageId: "missingName" });
289
- }
275
+ function check$9(node, context) {
276
+ if (!isBaseSubclass(node, context)) return;
277
+ const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
278
+ if (!configProp) {
279
+ context.report({
280
+ node,
281
+ messageId: "missingConfig"
282
+ });
283
+ return;
284
+ }
285
+ const value = configProp.value;
286
+ if (!value || value.type !== "ObjectExpression") return;
287
+ if (!value.properties?.some((prop) => prop.type === "Property" && prop.key?.name === "name" && prop.value?.value)) context.report({
288
+ node: configProp,
289
+ messageId: "missingName"
290
+ });
290
291
  }
291
-
292
- // src/rules/require-config-name-pascal-case.ts
293
- var requireConfigNamePascalCase = createRule({
294
- meta: {
295
- type: "problem",
296
- fixable: "code",
297
- docs: {
298
- description: "Require config.name to be PascalCase on classes extending Base"
299
- },
300
- messages: {
301
- notPascalCase: 'config.name "{{name}}" must be PascalCase.'
302
- }
303
- },
304
- createOnce(context) {
305
- return {
306
- ClassDeclaration(node) {
307
- check2(node, context);
308
- },
309
- ClassExpression(node) {
310
- check2(node, context);
311
- }
312
- };
313
- }
292
+ //#endregion
293
+ //#region src/rules/require-config-name-pascal-case.ts
294
+ const requireConfigNamePascalCase = createRule({
295
+ meta: {
296
+ type: "problem",
297
+ fixable: "code",
298
+ docs: { description: "Require config.name to be PascalCase on classes extending Base" },
299
+ messages: { notPascalCase: "config.name \"{{name}}\" must be PascalCase." }
300
+ },
301
+ createOnce(context) {
302
+ return {
303
+ ClassDeclaration(node) {
304
+ check$8(node, context);
305
+ },
306
+ ClassExpression(node) {
307
+ check$8(node, context);
308
+ }
309
+ };
310
+ }
314
311
  });
315
- function check2(node, context) {
316
- if (!isBaseSubclass(node, context)) return;
317
- const body = node.body?.body ?? [];
318
- const configProp = body.find(
319
- (member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config"
320
- );
321
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
322
- const nameProp = configProp.value.properties?.find(
323
- (prop) => prop.type === "Property" && prop.key?.name === "name"
324
- );
325
- if (!nameProp) return;
326
- const nameValue = nameProp.value?.value;
327
- if (typeof nameValue !== "string") return;
328
- if (!isPascalCase(nameValue)) {
329
- context.report({
330
- node: nameProp.value,
331
- messageId: "notPascalCase",
332
- data: { name: nameValue },
333
- fix: (fixer) => fixer.replaceText(nameProp.value, `'${toPascalCase(nameValue)}'`)
334
- });
335
- }
312
+ function check$8(node, context) {
313
+ if (!isBaseSubclass(node, context)) return;
314
+ const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
315
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
316
+ const nameProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "name");
317
+ if (!nameProp) return;
318
+ const nameValue = nameProp.value?.value;
319
+ if (typeof nameValue !== "string") return;
320
+ if (!isPascalCase(nameValue)) context.report({
321
+ node: nameProp.value,
322
+ messageId: "notPascalCase",
323
+ data: { name: nameValue },
324
+ fix: (fixer) => fixer.replaceText(nameProp.value, `'${toPascalCase(nameValue)}'`)
325
+ });
336
326
  }
337
-
338
- // src/rules/refs-camel-case.ts
339
- var refsCamelCase = createRule({
340
- meta: {
341
- type: "problem",
342
- fixable: "code",
343
- docs: {
344
- description: "Require ref names in static config to be camelCase"
345
- },
346
- messages: {
347
- notCamelCase: 'Ref name "{{name}}" must be camelCase.'
348
- }
349
- },
350
- createOnce(context) {
351
- return {
352
- ClassDeclaration(node) {
353
- check3(node, context);
354
- },
355
- ClassExpression(node) {
356
- check3(node, context);
357
- }
358
- };
359
- }
327
+ //#endregion
328
+ //#region src/rules/refs-camel-case.ts
329
+ const refsCamelCase = createRule({
330
+ meta: {
331
+ type: "problem",
332
+ fixable: "code",
333
+ docs: { description: "Require ref names in static config to be camelCase" },
334
+ messages: { notCamelCase: "Ref name \"{{name}}\" must be camelCase." }
335
+ },
336
+ createOnce(context) {
337
+ return {
338
+ ClassDeclaration(node) {
339
+ check$7(node, context);
340
+ },
341
+ ClassExpression(node) {
342
+ check$7(node, context);
343
+ }
344
+ };
345
+ }
360
346
  });
361
- function check3(node, context) {
362
- if (!isBaseSubclass(node, context)) return;
363
- const body = node.body?.body ?? [];
364
- const configProp = body.find(
365
- (member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config"
366
- );
367
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
368
- const refsProp = configProp.value.properties?.find(
369
- (prop) => prop.type === "Property" && prop.key?.name === "refs"
370
- );
371
- if (!refsProp?.value || refsProp.value.type !== "ArrayExpression") return;
372
- for (const element of refsProp.value.elements ?? []) {
373
- if (!element || element.type !== "Literal") continue;
374
- const raw = element.value;
375
- if (typeof raw !== "string") continue;
376
- const name = raw.endsWith("[]") ? raw.slice(0, -2) : raw;
377
- if (!isCamelCase(name)) {
378
- const fixed = toCamelCase(name) + (raw.endsWith("[]") ? "[]" : "");
379
- context.report({
380
- node: element,
381
- messageId: "notCamelCase",
382
- data: { name },
383
- fix: (fixer) => fixer.replaceText(element, `'${fixed}'`)
384
- });
385
- }
386
- }
347
+ function check$7(node, context) {
348
+ if (!isBaseSubclass(node, context)) return;
349
+ const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
350
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
351
+ const refsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "refs");
352
+ if (!refsProp?.value || refsProp.value.type !== "ArrayExpression") return;
353
+ for (const element of refsProp.value.elements ?? []) {
354
+ if (!element || element.type !== "Literal") continue;
355
+ const raw = element.value;
356
+ if (typeof raw !== "string") continue;
357
+ const name = raw.endsWith("[]") ? raw.slice(0, -2) : raw;
358
+ if (!isCamelCase(name)) {
359
+ const fixed = toCamelCase(name) + (raw.endsWith("[]") ? "[]" : "");
360
+ context.report({
361
+ node: element,
362
+ messageId: "notCamelCase",
363
+ data: { name },
364
+ fix: (fixer) => fixer.replaceText(element, `'${fixed}'`)
365
+ });
366
+ }
367
+ }
387
368
  }
388
-
389
- // src/rules/options-camel-case.ts
390
- var optionsCamelCase = createRule({
391
- meta: {
392
- type: "problem",
393
- fixable: "code",
394
- docs: {
395
- description: "Require option keys in static config to be camelCase"
396
- },
397
- messages: {
398
- notCamelCase: 'Option key "{{name}}" must be camelCase.'
399
- }
400
- },
401
- createOnce(context) {
402
- return {
403
- ClassDeclaration(node) {
404
- check4(node, context);
405
- },
406
- ClassExpression(node) {
407
- check4(node, context);
408
- }
409
- };
410
- }
369
+ //#endregion
370
+ //#region src/rules/options-camel-case.ts
371
+ const optionsCamelCase = createRule({
372
+ meta: {
373
+ type: "problem",
374
+ fixable: "code",
375
+ docs: { description: "Require option keys in static config to be camelCase" },
376
+ messages: { notCamelCase: "Option key \"{{name}}\" must be camelCase." }
377
+ },
378
+ createOnce(context) {
379
+ return {
380
+ ClassDeclaration(node) {
381
+ check$6(node, context);
382
+ },
383
+ ClassExpression(node) {
384
+ check$6(node, context);
385
+ }
386
+ };
387
+ }
411
388
  });
412
- function check4(node, context) {
413
- if (!isBaseSubclass(node, context)) return;
414
- const body = node.body?.body ?? [];
415
- const configProp = body.find(
416
- (member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config"
417
- );
418
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
419
- const optionsProp = configProp.value.properties?.find(
420
- (prop) => prop.type === "Property" && prop.key?.name === "options"
421
- );
422
- if (!optionsProp?.value || optionsProp.value.type !== "ObjectExpression") return;
423
- for (const prop of optionsProp.value.properties ?? []) {
424
- if (prop.type !== "Property") continue;
425
- const name = prop.key?.name ?? prop.key?.value;
426
- if (typeof name !== "string") continue;
427
- if (!isCamelCase(name)) {
428
- const fixed = toCamelCase(name);
429
- const fixedText = prop.key.type === "Literal" ? `'${fixed}'` : fixed;
430
- context.report({
431
- node: prop.key,
432
- messageId: "notCamelCase",
433
- data: { name },
434
- fix: (fixer) => fixer.replaceText(prop.key, fixedText)
435
- });
436
- }
437
- }
389
+ function check$6(node, context) {
390
+ if (!isBaseSubclass(node, context)) return;
391
+ const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
392
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
393
+ const optionsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "options");
394
+ if (!optionsProp?.value || optionsProp.value.type !== "ObjectExpression") return;
395
+ for (const prop of optionsProp.value.properties ?? []) {
396
+ if (prop.type !== "Property") continue;
397
+ const name = prop.key?.name ?? prop.key?.value;
398
+ if (typeof name !== "string") continue;
399
+ if (!isCamelCase(name)) {
400
+ const fixed = toCamelCase(name);
401
+ const fixedText = prop.key.type === "Literal" ? `'${fixed}'` : fixed;
402
+ context.report({
403
+ node: prop.key,
404
+ messageId: "notCamelCase",
405
+ data: { name },
406
+ fix: (fixer) => fixer.replaceText(prop.key, fixedText)
407
+ });
408
+ }
409
+ }
438
410
  }
439
-
440
- // src/rules/async-lifecycle-methods.ts
441
- var asyncLifecycleMethods = createRule({
442
- meta: {
443
- type: "problem",
444
- fixable: "code",
445
- docs: {
446
- description: "Require lifecycle methods on Base subclasses to be async"
447
- },
448
- messages: {
449
- notAsync: 'Lifecycle method "{{name}}" must be declared as async.'
450
- }
451
- },
452
- createOnce(context) {
453
- return {
454
- MethodDefinition(node) {
455
- const name = node.key?.name;
456
- if (!name || !LIFECYCLE_METHODS.has(name)) return;
457
- if (node.value?.async) return;
458
- const ancestors = context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
459
- const enclosingClass = findEnclosingClass(ancestors);
460
- if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
461
- context.report({
462
- node,
463
- messageId: "notAsync",
464
- data: { name },
465
- fix: (fixer) => fixer.insertTextBefore(node.key, "async ")
466
- });
467
- }
468
- };
469
- }
411
+ //#endregion
412
+ //#region src/rules/async-lifecycle-methods.ts
413
+ const asyncLifecycleMethods = createRule({
414
+ meta: {
415
+ type: "problem",
416
+ fixable: "code",
417
+ docs: { description: "Require lifecycle methods on Base subclasses to be async" },
418
+ messages: { notAsync: "Lifecycle method \"{{name}}\" must be declared as async." }
419
+ },
420
+ createOnce(context) {
421
+ return { MethodDefinition(node) {
422
+ const name = node.key?.name;
423
+ if (!name || !LIFECYCLE_METHODS.has(name)) return;
424
+ if (node.value?.async) return;
425
+ const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
426
+ if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
427
+ context.report({
428
+ node,
429
+ messageId: "notAsync",
430
+ data: { name },
431
+ fix: (fixer) => fixer.insertTextBefore(node.key, "async ")
432
+ });
433
+ } };
434
+ }
470
435
  });
471
-
472
- // src/rules/on-handler-naming.ts
473
- var ON_HANDLER_RE = /^on[A-Z][a-zA-Z0-9]*$/;
474
- var onHandlerNaming = createRule({
475
- meta: {
476
- type: "problem",
477
- docs: {
478
- description: "Require event handler methods to follow the onXxxYyy naming convention"
479
- },
480
- messages: {
481
- invalidName: 'Event handler "{{name}}" must follow the onXxxYyy camelCase convention (e.g. onClickButton).'
482
- }
483
- },
484
- createOnce(context) {
485
- return {
486
- MethodDefinition(node) {
487
- if (node.kind === "get" || node.kind === "set") return;
488
- const name = node.key?.name;
489
- if (!name || !/^on[A-Z]/.test(name)) return;
490
- if (ON_HANDLER_RE.test(name)) return;
491
- const ancestors = context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
492
- const enclosingClass = findEnclosingClass(ancestors);
493
- if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
494
- context.report({ node, messageId: "invalidName", data: { name } });
495
- }
496
- };
497
- }
436
+ //#endregion
437
+ //#region src/rules/on-handler-naming.ts
438
+ const ON_HANDLER_RE = /^on[A-Z][a-zA-Z0-9]*$/;
439
+ const onHandlerNaming = createRule({
440
+ meta: {
441
+ type: "problem",
442
+ docs: { description: "Require event handler methods to follow the onXxxYyy naming convention" },
443
+ messages: { invalidName: "Event handler \"{{name}}\" must follow the onXxxYyy camelCase convention (e.g. onClickButton)." }
444
+ },
445
+ createOnce(context) {
446
+ return { MethodDefinition(node) {
447
+ if (node.kind === "get" || node.kind === "set") return;
448
+ const name = node.key?.name;
449
+ if (!name || !/^on[A-Z]/.test(name)) return;
450
+ if (ON_HANDLER_RE.test(name)) return;
451
+ const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
452
+ if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
453
+ context.report({
454
+ node,
455
+ messageId: "invalidName",
456
+ data: { name }
457
+ });
458
+ } };
459
+ }
498
460
  });
499
-
500
- // src/rules/on-global-handler-prefix.ts
501
- var GLOBAL_ONLY_EVENTS = /* @__PURE__ */ new Set(["Resize"]);
502
- var onGlobalHandlerPrefix = createRule({
503
- meta: {
504
- type: "suggestion",
505
- docs: {
506
- description: "Require global event handlers (resize, scroll, etc.) to be prefixed with onWindow or onDocument"
507
- },
508
- messages: {
509
- missingPrefix: '"{{name}}" looks like a global event handler. Use "onWindow{{event}}" or "onDocument{{event}}" to bind to window/document events.'
510
- }
511
- },
512
- createOnce(context) {
513
- return {
514
- MethodDefinition(node) {
515
- const name = node.key?.name;
516
- if (!name || !name.startsWith("on")) return;
517
- const suffix = name.slice(2);
518
- const event = GLOBAL_ONLY_EVENTS.has(suffix) ? suffix : null;
519
- if (!event) return;
520
- if (name.startsWith("onWindow") || name.startsWith("onDocument")) return;
521
- const ancestors = context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
522
- const enclosingClass = findEnclosingClass(ancestors);
523
- if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
524
- context.report({
525
- node,
526
- messageId: "missingPrefix",
527
- data: { name, event }
528
- });
529
- }
530
- };
531
- }
461
+ //#endregion
462
+ //#region src/rules/on-global-handler-prefix.ts
463
+ const GLOBAL_ONLY_EVENTS = /* @__PURE__ */ new Set(["Resize"]);
464
+ const onGlobalHandlerPrefix = createRule({
465
+ meta: {
466
+ type: "suggestion",
467
+ docs: { description: "Require global event handlers (resize, scroll, etc.) to be prefixed with onWindow or onDocument" },
468
+ messages: { missingPrefix: "\"{{name}}\" looks like a global event handler. Use \"onWindow{{event}}\" or \"onDocument{{event}}\" to bind to window/document events." }
469
+ },
470
+ createOnce(context) {
471
+ return { MethodDefinition(node) {
472
+ const name = node.key?.name;
473
+ if (!name || !name.startsWith("on")) return;
474
+ const suffix = name.slice(2);
475
+ const event = GLOBAL_ONLY_EVENTS.has(suffix) ? suffix : null;
476
+ if (!event) return;
477
+ if (name.startsWith("onWindow") || name.startsWith("onDocument")) return;
478
+ const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
479
+ if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
480
+ context.report({
481
+ node,
482
+ messageId: "missingPrefix",
483
+ data: {
484
+ name,
485
+ event
486
+ }
487
+ });
488
+ } };
489
+ }
532
490
  });
533
-
534
- // src/rules/no-deprecated-properties.ts
535
- var DEPRECATED = /* @__PURE__ */ new Map([
536
- ["$parent", "$closest()"],
537
- ["$root", "$closest()"],
538
- ["$children", "$query()"]
491
+ //#endregion
492
+ //#region src/rules/no-deprecated-properties.ts
493
+ /** Deprecated in v3, still present at runtime. */
494
+ const V3_DEPRECATED = /* @__PURE__ */ new Map([
495
+ ["$parent", "$closest()"],
496
+ ["$root", "$closest()"],
497
+ ["$children", "$query()"]
539
498
  ]);
540
- var noDeprecatedProperties = createRule({
541
- meta: {
542
- type: "problem",
543
- docs: {
544
- description: "Disallow deprecated Base properties ($parent, $children, $root)"
545
- },
546
- messages: {
547
- deprecated: '"{{name}}" is deprecated. Use {{replacement}} instead.'
548
- }
549
- },
550
- createOnce(context) {
551
- return {
552
- MemberExpression(node) {
553
- const prop = node.property?.name;
554
- if (!prop || !DEPRECATED.has(prop)) return;
555
- if (node.object?.type !== "ThisExpression") return;
556
- const ancestors = context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
557
- const enclosingClass = findEnclosingClass(ancestors);
558
- if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
559
- const replacement = DEPRECATED.get(prop);
560
- context.report({
561
- node,
562
- messageId: "deprecated",
563
- data: { name: prop, replacement }
564
- });
565
- }
566
- };
567
- }
499
+ /** Removed in v4. Reaching for any of these is a runtime error, not a warning. */
500
+ const V4_REMOVED = /* @__PURE__ */ new Map([
501
+ ["$parent", "$closest()"],
502
+ ["$root", "$closest()"],
503
+ ["$children", "$watchChildren()"],
504
+ ["$update", "nothing — $refs and $options read the DOM on every access"],
505
+ ["$log", "console.log()"],
506
+ ["$terminate", "$unmount()"]
507
+ ]);
508
+ /** Methods v4 no longer calls. Defining one is dead code. */
509
+ const V4_REMOVED_METHODS = /* @__PURE__ */ new Map([["updated", "$watchChildren(), or an option<Name>Changed() hook"], ["terminated", "unmounted()"]]);
510
+ /** `$services` survives, but its two switches do not. */
511
+ const V4_REMOVED_SERVICE_METHODS = /* @__PURE__ */ new Map([["enable", "this.$services.<hook>.start()"], ["disable", "this.$services.<hook>.stop()"]]);
512
+ const noDeprecatedProperties = createRule({
513
+ meta: {
514
+ type: "problem",
515
+ docs: { description: "Disallow deprecated Base properties, and the members v4 removed" },
516
+ schema: [{
517
+ type: "object",
518
+ properties: { version: { enum: ["v3", "v4"] } },
519
+ additionalProperties: false
520
+ }],
521
+ messages: {
522
+ deprecated: "\"{{name}}\" is deprecated. Use {{replacement}} instead.",
523
+ removed: "\"{{name}}\" is removed in v4. Use {{replacement}} instead.",
524
+ removedMethod: "\"{{name}}()\" is removed in v4, so this method is never called. Use {{replacement}} instead.",
525
+ removedConfig: "`config.emits` is removed in v4. Declare the events in the `$emits` type of the component instead — nothing of it stays in the bundle."
526
+ }
527
+ },
528
+ createOnce(context) {
529
+ /**
530
+ * `createOnce` runs once for the whole run, while `context` tracks the file
531
+ * being linted. Options are therefore read per file, at report time.
532
+ */
533
+ const isV4Mode = () => context.options?.[0]?.version === "v4";
534
+ /**
535
+ * v3 resolves a component class by its `Base` superclass; v4 also writes
536
+ * `extends with<Service>(…)` and `@component({ … })`.
537
+ */
538
+ function isComponent(node) {
539
+ return isV4Mode() ? isComponentClass(node) : isBaseSubclass(node, context);
540
+ }
541
+ function enclosingComponent(node) {
542
+ const enclosing = findEnclosingClass(getAncestors(node, context));
543
+ return enclosing && isComponent(enclosing) ? enclosing : null;
544
+ }
545
+ return {
546
+ MemberExpression(node) {
547
+ const name = node.property?.name;
548
+ if (!name) return;
549
+ const isV4 = isV4Mode();
550
+ if (isV4 && V4_REMOVED_SERVICE_METHODS.has(name) && node.object?.type === "MemberExpression" && node.object.object?.type === "ThisExpression" && node.object.property?.name === "$services") {
551
+ if (!enclosingComponent(node)) return;
552
+ context.report({
553
+ node,
554
+ messageId: "removed",
555
+ data: {
556
+ name: `$services.${name}`,
557
+ replacement: V4_REMOVED_SERVICE_METHODS.get(name)
558
+ }
559
+ });
560
+ return;
561
+ }
562
+ const members = isV4 ? V4_REMOVED : V3_DEPRECATED;
563
+ if (!members.has(name)) return;
564
+ if (node.object?.type !== "ThisExpression") return;
565
+ if (!enclosingComponent(node)) return;
566
+ context.report({
567
+ node,
568
+ messageId: isV4 ? "removed" : "deprecated",
569
+ data: {
570
+ name,
571
+ replacement: members.get(name)
572
+ }
573
+ });
574
+ },
575
+ MethodDefinition(node) {
576
+ if (!isV4Mode()) return;
577
+ const name = getKeyName(node);
578
+ if (!name || !V4_REMOVED_METHODS.has(name)) return;
579
+ if (node.static === true) return;
580
+ if (!enclosingComponent(node)) return;
581
+ context.report({
582
+ node: node.key ?? node,
583
+ messageId: "removedMethod",
584
+ data: {
585
+ name,
586
+ replacement: V4_REMOVED_METHODS.get(name)
587
+ }
588
+ });
589
+ },
590
+ ClassDeclaration(node) {
591
+ checkConfig(node);
592
+ },
593
+ ClassExpression(node) {
594
+ checkConfig(node);
595
+ }
596
+ };
597
+ function checkConfig(node) {
598
+ if (!isV4Mode() || !isComponent(node)) return;
599
+ const emits = getProperty(findComponentConfig(node), "emits");
600
+ if (!emits) return;
601
+ context.report({
602
+ node: emits,
603
+ messageId: "removedConfig"
604
+ });
605
+ }
606
+ }
568
607
  });
569
-
570
- // src/rules/no-dispatch-event.ts
571
- var noDispatchEvent = createRule({
572
- meta: {
573
- type: "suggestion",
574
- docs: {
575
- description: "Disallow dispatchEvent() in Base subclasses \u2014 use this.$emit() instead"
576
- },
577
- messages: {
578
- useEmit: "Use this.$emit() instead of dispatchEvent() to emit events in a Base component."
579
- }
580
- },
581
- createOnce(context) {
582
- return {
583
- CallExpression(node) {
584
- const callee = node.callee;
585
- const isDispatchEvent = (
586
- // this.dispatchEvent(...)
587
- callee.type === "MemberExpression" && callee.object?.type === "ThisExpression" && callee.property?.name === "dispatchEvent" || // this.$el.dispatchEvent(...) or el.dispatchEvent(...)
588
- callee.type === "MemberExpression" && callee.property?.name === "dispatchEvent"
589
- );
590
- if (!isDispatchEvent) return;
591
- const ancestors = context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
592
- const enclosingClass = findEnclosingClass(ancestors);
593
- if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
594
- context.report({ node, messageId: "useEmit" });
595
- }
596
- };
597
- }
608
+ //#endregion
609
+ //#region src/rules/no-dispatch-event.ts
610
+ const noDispatchEvent = createRule({
611
+ meta: {
612
+ type: "suggestion",
613
+ docs: { description: "Disallow dispatchEvent() in Base subclasses — use this.$emit() instead" },
614
+ messages: { useEmit: "Use this.$emit() instead of dispatchEvent() to emit events in a Base component." }
615
+ },
616
+ createOnce(context) {
617
+ return { CallExpression(node) {
618
+ const callee = node.callee;
619
+ if (!(callee.type === "MemberExpression" && callee.object?.type === "ThisExpression" && callee.property?.name === "dispatchEvent" || callee.type === "MemberExpression" && callee.property?.name === "dispatchEvent")) return;
620
+ const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
621
+ if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
622
+ context.report({
623
+ node,
624
+ messageId: "useEmit"
625
+ });
626
+ } };
627
+ }
598
628
  });
599
-
600
- // src/rules/no-shadow-dom.ts
601
- var noShadowDom = createRule({
602
- meta: {
603
- type: "problem",
604
- docs: {
605
- description: "Disallow attachShadow() in Base subclasses \u2014 js-toolkit uses Light DOM only"
606
- },
607
- messages: {
608
- noShadow: "Do not use attachShadow() in a Base component. @studiometa/js-toolkit uses Light DOM only."
609
- }
610
- },
611
- createOnce(context) {
612
- return {
613
- CallExpression(node) {
614
- const callee = node.callee;
615
- if (callee.type !== "MemberExpression" || callee.property?.name !== "attachShadow") {
616
- return;
617
- }
618
- const ancestors = context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
619
- const enclosingClass = findEnclosingClass(ancestors);
620
- if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
621
- context.report({ node, messageId: "noShadow" });
622
- }
623
- };
624
- }
629
+ //#endregion
630
+ //#region src/rules/no-shadow-dom.ts
631
+ const noShadowDom = createRule({
632
+ meta: {
633
+ type: "problem",
634
+ docs: { description: "Disallow attachShadow() in Base subclasses — js-toolkit uses Light DOM only" },
635
+ messages: { noShadow: "Do not use attachShadow() in a Base component. @studiometa/js-toolkit uses Light DOM only." }
636
+ },
637
+ createOnce(context) {
638
+ return { CallExpression(node) {
639
+ const callee = node.callee;
640
+ if (callee.type !== "MemberExpression" || callee.property?.name !== "attachShadow") return;
641
+ const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
642
+ if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
643
+ context.report({
644
+ node,
645
+ messageId: "noShadow"
646
+ });
647
+ } };
648
+ }
625
649
  });
626
-
627
- // src/rules/no-create-app.ts
628
- var noCreateApp = createRule({
629
- meta: {
630
- type: "suggestion",
631
- docs: {
632
- description: "Disallow createApp() \u2014 use registerComponent() instead"
633
- },
634
- messages: {
635
- useRegister: "createApp() is deprecated. Use registerComponent() from @studiometa/js-toolkit instead."
636
- }
637
- },
638
- createOnce(context) {
639
- return {
640
- CallExpression(node) {
641
- const callee = node.callee;
642
- if (callee.type === "Identifier" && callee.name === "createApp") {
643
- context.report({ node, messageId: "useRegister" });
644
- }
645
- }
646
- };
647
- }
650
+ //#endregion
651
+ //#region src/rules/no-create-app.ts
652
+ const noCreateApp = createRule({
653
+ meta: {
654
+ type: "suggestion",
655
+ docs: { description: "Disallow createApp() — use registerComponent() instead" },
656
+ messages: { useRegister: "createApp() is deprecated. Use registerComponent() from @studiometa/js-toolkit instead." }
657
+ },
658
+ createOnce(context) {
659
+ return { CallExpression(node) {
660
+ const callee = node.callee;
661
+ if (callee.type === "Identifier" && callee.name === "createApp") context.report({
662
+ node,
663
+ messageId: "useRegister"
664
+ });
665
+ } };
666
+ }
648
667
  });
649
-
650
- // src/rules/no-event-listener-methods.ts
651
- var FORBIDDEN = /* @__PURE__ */ new Set(["addEventListener", "removeEventListener"]);
652
- var noEventListenerMethods = createRule({
653
- meta: {
654
- type: "problem",
655
- docs: {
656
- description: "Disallow addEventListener/removeEventListener in Base subclasses \u2014 use on* methods instead"
657
- },
658
- messages: {
659
- useOnMethod: '"{{method}}()" should not be used in a Base component. Define an "on<Target><Event>()" method instead \u2014 the framework handles binding and cleanup automatically.'
660
- }
661
- },
662
- createOnce(context) {
663
- return {
664
- CallExpression(node) {
665
- const callee = node.callee;
666
- if (callee.type !== "MemberExpression") return;
667
- const method = callee.property?.name;
668
- if (!method || !FORBIDDEN.has(method)) return;
669
- let root = callee.object;
670
- while (root?.type === "MemberExpression") root = root.object;
671
- const isThis = root?.type === "ThisExpression";
672
- const isGlobal = root?.type === "Identifier" && (root.name === "document" || root.name === "window");
673
- if (!isThis && !isGlobal) return;
674
- const ancestors = context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? [];
675
- const enclosingClass = findEnclosingClass(ancestors);
676
- if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
677
- context.report({ node, messageId: "useOnMethod", data: { method } });
678
- }
679
- };
680
- }
668
+ //#endregion
669
+ //#region src/rules/no-event-listener-methods.ts
670
+ const FORBIDDEN = /* @__PURE__ */ new Set(["addEventListener", "removeEventListener"]);
671
+ const noEventListenerMethods = createRule({
672
+ meta: {
673
+ type: "problem",
674
+ docs: { description: "Disallow addEventListener/removeEventListener in Base subclasses — use on* methods instead" },
675
+ messages: { useOnMethod: "\"{{method}}()\" should not be used in a Base component. Define an \"on<Target><Event>()\" method instead — the framework handles binding and cleanup automatically." }
676
+ },
677
+ createOnce(context) {
678
+ return { CallExpression(node) {
679
+ const callee = node.callee;
680
+ if (callee.type !== "MemberExpression") return;
681
+ const method = callee.property?.name;
682
+ if (!method || !FORBIDDEN.has(method)) return;
683
+ let root = callee.object;
684
+ while (root?.type === "MemberExpression") root = root.object;
685
+ const isThis = root?.type === "ThisExpression";
686
+ const isGlobal = root?.type === "Identifier" && (root.name === "document" || root.name === "window");
687
+ if (!isThis && !isGlobal) return;
688
+ const enclosingClass = findEnclosingClass(context.getAncestors ? context.getAncestors() : context.sourceCode?.getAncestors?.(node) ?? []);
689
+ if (!enclosingClass || !isBaseSubclass(enclosingClass, context)) return;
690
+ context.report({
691
+ node,
692
+ messageId: "useOnMethod",
693
+ data: { method }
694
+ });
695
+ } };
696
+ }
681
697
  });
682
-
683
- // src/rules/refs-plural-multiple.ts
684
- var refsPluralMultiple = createRule({
685
- meta: {
686
- type: "problem",
687
- docs: {
688
- description: "Require multiple-ref names (using [] suffix) to be pluralized"
689
- },
690
- messages: {
691
- notPlural: 'Multiple ref "{{name}}" must be pluralized (e.g. "{{name}}s[]").'
692
- }
693
- },
694
- createOnce(context) {
695
- return {
696
- ClassDeclaration(node) {
697
- check5(node, context);
698
- },
699
- ClassExpression(node) {
700
- check5(node, context);
701
- }
702
- };
703
- }
698
+ //#endregion
699
+ //#region src/rules/refs-plural-multiple.ts
700
+ const refsPluralMultiple = createRule({
701
+ meta: {
702
+ type: "problem",
703
+ docs: { description: "Require multiple-ref names (using [] suffix) to be pluralized" },
704
+ messages: { notPlural: "Multiple ref \"{{name}}\" must be pluralized (e.g. \"{{name}}s[]\")." }
705
+ },
706
+ createOnce(context) {
707
+ return {
708
+ ClassDeclaration(node) {
709
+ check$5(node, context);
710
+ },
711
+ ClassExpression(node) {
712
+ check$5(node, context);
713
+ }
714
+ };
715
+ }
704
716
  });
705
- function check5(node, context) {
706
- if (!isBaseSubclass(node, context)) return;
707
- const body = node.body?.body ?? [];
708
- const configProp = body.find(
709
- (member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config"
710
- );
711
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
712
- const refsProp = configProp.value.properties?.find(
713
- (prop) => prop.type === "Property" && prop.key?.name === "refs"
714
- );
715
- if (!refsProp?.value || refsProp.value.type !== "ArrayExpression") return;
716
- for (const element of refsProp.value.elements ?? []) {
717
- if (!element || element.type !== "Literal") continue;
718
- const raw = element.value;
719
- if (typeof raw !== "string") continue;
720
- if (!raw.endsWith("[]")) continue;
721
- const name = raw.slice(0, -2);
722
- if (!name.endsWith("s")) {
723
- context.report({
724
- node: element,
725
- messageId: "notPlural",
726
- data: { name }
727
- });
728
- }
729
- }
717
+ function check$5(node, context) {
718
+ if (!isBaseSubclass(node, context)) return;
719
+ const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
720
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
721
+ const refsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "refs");
722
+ if (!refsProp?.value || refsProp.value.type !== "ArrayExpression") return;
723
+ for (const element of refsProp.value.elements ?? []) {
724
+ if (!element || element.type !== "Literal") continue;
725
+ const raw = element.value;
726
+ if (typeof raw !== "string") continue;
727
+ if (!raw.endsWith("[]")) continue;
728
+ const name = raw.slice(0, -2);
729
+ if (!name.endsWith("s")) context.report({
730
+ node: element,
731
+ messageId: "notPlural",
732
+ data: { name }
733
+ });
734
+ }
730
735
  }
731
-
732
- // src/rules/no-deep-utils-import.ts
733
- var UTILS_PREFIX = "@studiometa/js-toolkit/utils/";
734
- var PUBLIC_SUBPATH_RE = /^[^/.]+$/;
735
- var noDeepUtilsImport = createRule({
736
- meta: {
737
- type: "problem",
738
- docs: {
739
- description: "Disallow importing internal module paths from @studiometa/js-toolkit/utils \u2014 use the public per-symbol subpath"
740
- },
741
- messages: {
742
- noDeepImport: 'Import from "{{source}}" reaches into the package layout. Use the public subpath "@studiometa/js-toolkit/utils/<name>" for a single util, or "@studiometa/js-toolkit/utils" for several.'
743
- }
744
- },
745
- createOnce(context) {
746
- return {
747
- ImportDeclaration(node) {
748
- const source = node.source.value;
749
- if (!source.startsWith(UTILS_PREFIX)) return;
750
- if (PUBLIC_SUBPATH_RE.test(source.slice(UTILS_PREFIX.length))) return;
751
- context.report({ node, messageId: "noDeepImport", data: { source } });
752
- }
753
- };
754
- }
736
+ //#endregion
737
+ //#region src/rules/no-deep-utils-import.ts
738
+ const UTILS_PREFIX = "@studiometa/js-toolkit/utils/";
739
+ /**
740
+ * A published per-symbol subpath: exactly one segment after `utils/`, and no dot in it.
741
+ *
742
+ * Since v3.9.0 every util is exported at `@studiometa/js-toolkit/utils/<name>`. Those subpaths are
743
+ * public API and the cheapest way to import a util — they resolve to the module declaring the symbol,
744
+ * so a consumer served by a CDN downloads that module instead of the whole `utils` barrel. Anything
745
+ * deeper (`utils/math/damp.js`) reaches into the package layout, which is not part of the API and
746
+ * changes without notice.
747
+ *
748
+ * Every key of the `exports` map is a symbol name, so none of them contains a dot; rejecting the dot
749
+ * outright keeps a file path out (`utils/damp.ts`, `utils/foo.css`) whatever its extension.
750
+ */
751
+ const PUBLIC_SUBPATH_RE = /^[^/.]+$/;
752
+ const noDeepUtilsImport = createRule({
753
+ meta: {
754
+ type: "problem",
755
+ docs: { description: "Disallow importing internal module paths from @studiometa/js-toolkit/utils — use the public per-symbol subpath" },
756
+ messages: { noDeepImport: "Import from \"{{source}}\" reaches into the package layout. Use the public subpath \"@studiometa/js-toolkit/utils/<name>\" for a single util, or \"@studiometa/js-toolkit/utils\" for several." }
757
+ },
758
+ createOnce(context) {
759
+ return { ImportDeclaration(node) {
760
+ const source = node.source.value;
761
+ if (!source.startsWith(UTILS_PREFIX)) return;
762
+ if (PUBLIC_SUBPATH_RE.test(source.slice(29))) return;
763
+ context.report({
764
+ node,
765
+ messageId: "noDeepImport",
766
+ data: { source }
767
+ });
768
+ } };
769
+ }
755
770
  });
756
-
757
- // src/rules/no-internal-barrel-import.ts
758
- var RELATIVE_BARREL_RE = /^\.{1,2}\/.*\/index\.js$/;
759
- var noInternalBarrelImport = createRule({
760
- meta: {
761
- type: "problem",
762
- docs: {
763
- description: "Disallow importing an internal barrel inside the package \u2014 import the declaring module instead"
764
- },
765
- messages: {
766
- noBarrelImport: 'Import from "{{source}}" pulls in every symbol the barrel names. Import the module declaring the symbol instead, so a consumer served by a CDN only downloads what it uses.'
767
- }
768
- },
769
- createOnce(context) {
770
- return {
771
- ImportDeclaration(node) {
772
- const source = node.source.value;
773
- if (!RELATIVE_BARREL_RE.test(source)) return;
774
- context.report({ node, messageId: "noBarrelImport", data: { source } });
775
- }
776
- };
777
- }
771
+ //#endregion
772
+ //#region src/rules/no-internal-barrel-import.ts
773
+ /** A relative import of a barrel: `'./utils/index.js'`, `'../Base/index.js'`. */
774
+ const RELATIVE_BARREL_RE = /^\.{1,2}\/.*\/index\.js$/;
775
+ const noInternalBarrelImport = createRule({
776
+ meta: {
777
+ type: "problem",
778
+ docs: { description: "Disallow importing an internal barrel inside the package — import the declaring module instead" },
779
+ messages: { noBarrelImport: "Import from \"{{source}}\" pulls in every symbol the barrel names. Import the module declaring the symbol instead, so a consumer served by a CDN only downloads what it uses." }
780
+ },
781
+ createOnce(context) {
782
+ return { ImportDeclaration(node) {
783
+ const source = node.source.value;
784
+ if (!RELATIVE_BARREL_RE.test(source)) return;
785
+ context.report({
786
+ node,
787
+ messageId: "noBarrelImport",
788
+ data: { source }
789
+ });
790
+ } };
791
+ }
778
792
  });
779
-
780
- // src/rules/refs-no-bracket-access.ts
781
- function isThisRefs(node) {
782
- return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$refs";
793
+ //#endregion
794
+ //#region src/rules/refs-no-bracket-access.ts
795
+ function isThisRefs$1(node) {
796
+ return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$refs";
783
797
  }
784
- var refsNoBracketAccess = createRule({
785
- meta: {
786
- type: "problem",
787
- fixable: "code",
788
- docs: {
789
- description: "Disallow bracket access with [] suffix on this.$refs"
790
- },
791
- messages: {
792
- noBracketAccess: `Use "this.$refs.{{fixed}}" instead of "this.$refs['{{raw}}']".`
793
- }
794
- },
795
- createOnce(context) {
796
- return {
797
- MemberExpression(node) {
798
- if (!isThisRefs(node.object)) return;
799
- if (!node.computed) return;
800
- const prop = node.property;
801
- if (prop.type !== "Literal" || typeof prop.value !== "string") return;
802
- const raw = prop.value;
803
- if (!raw.endsWith("[]")) return;
804
- const stripped = raw.slice(0, -2);
805
- const fixed = toCamelCase(stripped);
806
- context.report({
807
- node,
808
- messageId: "noBracketAccess",
809
- data: { raw, fixed },
810
- fix(fixer) {
811
- const src = context.sourceCode ?? context.getSourceCode?.();
812
- const objText = src.getText(node.object);
813
- return fixer.replaceText(node, `${objText}.${fixed}`);
814
- }
815
- });
816
- }
817
- };
818
- }
798
+ const refsNoBracketAccess = createRule({
799
+ meta: {
800
+ type: "problem",
801
+ fixable: "code",
802
+ docs: { description: "Disallow bracket access with [] suffix on this.$refs" },
803
+ messages: { noBracketAccess: "Use \"this.$refs.{{fixed}}\" instead of \"this.$refs['{{raw}}']\"." }
804
+ },
805
+ createOnce(context) {
806
+ return { MemberExpression(node) {
807
+ if (!isThisRefs$1(node.object)) return;
808
+ if (!node.computed) return;
809
+ const prop = node.property;
810
+ if (prop.type !== "Literal" || typeof prop.value !== "string") return;
811
+ const raw = prop.value;
812
+ if (!raw.endsWith("[]")) return;
813
+ const fixed = toCamelCase(raw.slice(0, -2));
814
+ context.report({
815
+ node,
816
+ messageId: "noBracketAccess",
817
+ data: {
818
+ raw,
819
+ fixed
820
+ },
821
+ fix(fixer) {
822
+ const objText = (context.sourceCode ?? context.getSourceCode?.()).getText(node.object);
823
+ return fixer.replaceText(node, `${objText}.${fixed}`);
824
+ }
825
+ });
826
+ } };
827
+ }
819
828
  });
820
-
821
- // src/rules/no-redundant-with-mount-when-in-view.ts
822
- var noRedundantWithMountWhenInView = createRule({
823
- meta: {
824
- type: "suggestion",
825
- docs: {
826
- description: "Disallow wrapping withMountWhenInView inside withScrolledInView (redundant)"
827
- },
828
- messages: {
829
- redundant: "withScrolledInView already includes withMountWhenInView internally. Remove the inner withMountWhenInView call."
830
- }
831
- },
832
- createOnce(context) {
833
- return {
834
- "ClassDeclaration, ClassExpression"(node) {
835
- check6(node, context);
836
- }
837
- };
838
- }
829
+ //#endregion
830
+ //#region src/rules/no-redundant-with-mount-when-in-view.ts
831
+ const noRedundantWithMountWhenInView = createRule({
832
+ meta: {
833
+ type: "suggestion",
834
+ docs: { description: "Disallow wrapping withMountWhenInView inside withScrolledInView (redundant)" },
835
+ messages: { redundant: "withScrolledInView already includes withMountWhenInView internally. Remove the inner withMountWhenInView call." }
836
+ },
837
+ createOnce(context) {
838
+ return { "ClassDeclaration, ClassExpression"(node) {
839
+ check$4(node, context);
840
+ } };
841
+ }
839
842
  });
840
- function check6(node, context) {
841
- const superClass = node.superClass;
842
- if (!superClass || superClass.type !== "CallExpression") return;
843
- if (superClass.callee?.name !== "withScrolledInView") return;
844
- const arg = superClass.arguments?.[0];
845
- if (!arg || arg.type !== "CallExpression") return;
846
- if (arg.callee?.name !== "withMountWhenInView") return;
847
- context.report({ node: superClass, messageId: "redundant" });
843
+ function check$4(node, context) {
844
+ const superClass = node.superClass;
845
+ if (!superClass || superClass.type !== "CallExpression") return;
846
+ if (superClass.callee?.name !== "withScrolledInView") return;
847
+ const arg = superClass.arguments?.[0];
848
+ if (!arg || arg.type !== "CallExpression") return;
849
+ if (arg.callee?.name !== "withMountWhenInView") return;
850
+ context.report({
851
+ node: superClass,
852
+ messageId: "redundant"
853
+ });
848
854
  }
849
-
850
- // src/rules/prefer-ref-over-query-selector.ts
851
- var QUERY_METHODS = /* @__PURE__ */ new Set(["querySelector", "querySelectorAll"]);
855
+ //#endregion
856
+ //#region src/rules/prefer-ref-over-query-selector.ts
857
+ const QUERY_METHODS = /* @__PURE__ */ new Set(["querySelector", "querySelectorAll"]);
852
858
  function isThisEl(node) {
853
- return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$el";
859
+ return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$el";
854
860
  }
855
- var preferRefOverQuerySelector = createRule({
856
- meta: {
857
- type: "suggestion",
858
- docs: {
859
- description: "Prefer this.$refs over this.$el.querySelector() in Base subclasses"
860
- },
861
- messages: {
862
- preferRef: 'Avoid "this.$el.{{method}}()". Declare a ref in static config and use "this.$refs" instead.'
863
- }
864
- },
865
- createOnce(context) {
866
- return {
867
- CallExpression(node) {
868
- const callee = node.callee;
869
- if (callee?.type !== "MemberExpression") return;
870
- if (!isThisEl(callee.object)) return;
871
- const method = callee.property?.name;
872
- if (!QUERY_METHODS.has(method)) return;
873
- const ancestors = context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
874
- const cls = findEnclosingClass(ancestors);
875
- if (!cls || !isBaseSubclass(cls, context)) return;
876
- context.report({ node, messageId: "preferRef", data: { method } });
877
- }
878
- };
879
- }
861
+ const preferRefOverQuerySelector = createRule({
862
+ meta: {
863
+ type: "suggestion",
864
+ docs: { description: "Prefer this.$refs over this.$el.querySelector() in Base subclasses" },
865
+ messages: { preferRef: "Avoid \"this.$el.{{method}}()\". Declare a ref in static config and use \"this.$refs\" instead." }
866
+ },
867
+ createOnce(context) {
868
+ return { CallExpression(node) {
869
+ const callee = node.callee;
870
+ if (callee?.type !== "MemberExpression") return;
871
+ if (!isThisEl(callee.object)) return;
872
+ const method = callee.property?.name;
873
+ if (!QUERY_METHODS.has(method)) return;
874
+ const cls = findEnclosingClass(context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? []);
875
+ if (!cls || !isBaseSubclass(cls, context)) return;
876
+ context.report({
877
+ node,
878
+ messageId: "preferRef",
879
+ data: { method }
880
+ });
881
+ } };
882
+ }
880
883
  });
881
-
882
- // src/rules/require-refs-declared-in-config.ts
884
+ //#endregion
885
+ //#region src/rules/require-refs-declared-in-config.ts
883
886
  function collectDeclaredRefs(classNode) {
884
- const body = classNode.body?.body ?? [];
885
- const configProp = body.find(
886
- (m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config"
887
- );
888
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
889
- const refsProp = configProp.value.properties?.find(
890
- (p) => p.type === "Property" && p.key?.name === "refs"
891
- );
892
- if (!refsProp?.value || refsProp.value.type !== "ArrayExpression") return /* @__PURE__ */ new Set();
893
- const names = /* @__PURE__ */ new Set();
894
- for (const el of refsProp.value.elements ?? []) {
895
- if (!el || el.type !== "Literal" || typeof el.value !== "string") continue;
896
- const raw = el.value;
897
- const stripped = raw.endsWith("[]") ? raw.slice(0, -2) : raw;
898
- names.add(toCamelCase(stripped));
899
- }
900
- return names;
887
+ const configProp = (classNode.body?.body ?? []).find((m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config");
888
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
889
+ const refsProp = configProp.value.properties?.find((p) => p.type === "Property" && p.key?.name === "refs");
890
+ if (!refsProp?.value || refsProp.value.type !== "ArrayExpression") return /* @__PURE__ */ new Set();
891
+ const names = /* @__PURE__ */ new Set();
892
+ for (const el of refsProp.value.elements ?? []) {
893
+ if (!el || el.type !== "Literal" || typeof el.value !== "string") continue;
894
+ const raw = el.value;
895
+ const stripped = raw.endsWith("[]") ? raw.slice(0, -2) : raw;
896
+ names.add(toCamelCase(stripped));
897
+ }
898
+ return names;
901
899
  }
902
- function isThisRefs2(node) {
903
- return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$refs";
900
+ function isThisRefs(node) {
901
+ return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$refs";
904
902
  }
905
- var requireRefsDeclaredInConfig = createRule({
906
- meta: {
907
- type: "problem",
908
- docs: {
909
- description: "Require all this.$refs accesses to be declared in static config.refs"
910
- },
911
- messages: {
912
- undeclared: 'Ref "{{name}}" is not declared in static config.refs.'
913
- }
914
- },
915
- createOnce(context) {
916
- return {
917
- "ClassDeclaration, ClassExpression"(classNode) {
918
- if (!isBaseSubclass(classNode, context)) return;
919
- const declared = collectDeclaredRefs(classNode);
920
- walkNode(classNode.body, (node) => {
921
- if (node.type !== "MemberExpression") return;
922
- if (!isThisRefs2(node.object)) return;
923
- let refName = null;
924
- if (!node.computed && node.property?.type === "Identifier") {
925
- refName = node.property.name;
926
- } else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") {
927
- const raw = node.property.value;
928
- const stripped = raw.endsWith("[]") ? raw.slice(0, -2) : raw;
929
- refName = toCamelCase(stripped);
930
- }
931
- if (refName && !declared.has(refName)) {
932
- context.report({
933
- node,
934
- messageId: "undeclared",
935
- data: { name: refName }
936
- });
937
- }
938
- });
939
- }
940
- };
941
- }
903
+ const requireRefsDeclaredInConfig = createRule({
904
+ meta: {
905
+ type: "problem",
906
+ docs: { description: "Require all this.$refs accesses to be declared in static config.refs" },
907
+ messages: { undeclared: "Ref \"{{name}}\" is not declared in static config.refs." }
908
+ },
909
+ createOnce(context) {
910
+ return { "ClassDeclaration, ClassExpression"(classNode) {
911
+ if (!isBaseSubclass(classNode, context)) return;
912
+ const declared = collectDeclaredRefs(classNode);
913
+ walkNode$3(classNode.body, (node) => {
914
+ if (node.type !== "MemberExpression") return;
915
+ if (!isThisRefs(node.object)) return;
916
+ let refName = null;
917
+ if (!node.computed && node.property?.type === "Identifier") refName = node.property.name;
918
+ else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") {
919
+ const raw = node.property.value;
920
+ refName = toCamelCase(raw.endsWith("[]") ? raw.slice(0, -2) : raw);
921
+ }
922
+ if (refName && !declared.has(refName)) context.report({
923
+ node,
924
+ messageId: "undeclared",
925
+ data: { name: refName }
926
+ });
927
+ });
928
+ } };
929
+ }
942
930
  });
943
- function walkNode(node, visit) {
944
- if (!node || typeof node !== "object") return;
945
- visit(node);
946
- for (const key of Object.keys(node)) {
947
- if (key === "parent") continue;
948
- const child = node[key];
949
- if (Array.isArray(child)) {
950
- for (const item of child) walkNode(item, visit);
951
- } else if (child && typeof child === "object" && child.type) {
952
- walkNode(child, visit);
953
- }
954
- }
931
+ function walkNode$3(node, visit) {
932
+ if (!node || typeof node !== "object") return;
933
+ visit(node);
934
+ for (const key of Object.keys(node)) {
935
+ if (key === "parent") continue;
936
+ const child = node[key];
937
+ if (Array.isArray(child)) for (const item of child) walkNode$3(item, visit);
938
+ else if (child && typeof child === "object" && child.type) walkNode$3(child, visit);
939
+ }
955
940
  }
956
-
957
- // src/rules/no-manual-intersection-observer.ts
958
- var noManualIntersectionObserver = createRule({
959
- meta: {
960
- type: "suggestion",
961
- docs: {
962
- description: "Disallow manual IntersectionObserver inside Base subclasses; use withIntersectionObserver or withMountWhenInView instead"
963
- },
964
- messages: {
965
- noManual: 'Avoid manual "new IntersectionObserver()". Use "withIntersectionObserver" or "withMountWhenInView" decorators instead.'
966
- }
967
- },
968
- createOnce(context) {
969
- return {
970
- NewExpression(node) {
971
- if (node.callee?.name !== "IntersectionObserver") return;
972
- const ancestors = context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
973
- const cls = findEnclosingClass(ancestors);
974
- if (!cls || !isBaseSubclass(cls, context)) return;
975
- context.report({ node, messageId: "noManual" });
976
- }
977
- };
978
- }
941
+ //#endregion
942
+ //#region src/rules/no-manual-intersection-observer.ts
943
+ const noManualIntersectionObserver = createRule({
944
+ meta: {
945
+ type: "suggestion",
946
+ docs: { description: "Disallow manual IntersectionObserver inside Base subclasses; use withIntersectionObserver or withMountWhenInView instead" },
947
+ messages: { noManual: "Avoid manual \"new IntersectionObserver()\". Use \"withIntersectionObserver\" or \"withMountWhenInView\" decorators instead." }
948
+ },
949
+ createOnce(context) {
950
+ return { NewExpression(node) {
951
+ if (node.callee?.name !== "IntersectionObserver") return;
952
+ const cls = findEnclosingClass(context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? []);
953
+ if (!cls || !isBaseSubclass(cls, context)) return;
954
+ context.report({
955
+ node,
956
+ messageId: "noManual"
957
+ });
958
+ } };
959
+ }
979
960
  });
980
-
981
- // src/rules/no-manual-mutation-observer.ts
982
- var noManualMutationObserver = createRule({
983
- meta: {
984
- type: "suggestion",
985
- docs: {
986
- description: "Disallow manual MutationObserver inside Base subclasses; use withMutation decorator instead"
987
- },
988
- messages: {
989
- noManual: 'Avoid manual "new MutationObserver()". Use the "withMutation" decorator instead.'
990
- }
991
- },
992
- createOnce(context) {
993
- return {
994
- NewExpression(node) {
995
- if (node.callee?.name !== "MutationObserver") return;
996
- const ancestors = context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
997
- const cls = findEnclosingClass(ancestors);
998
- if (!cls || !isBaseSubclass(cls, context)) return;
999
- context.report({ node, messageId: "noManual" });
1000
- }
1001
- };
1002
- }
961
+ //#endregion
962
+ //#region src/rules/no-manual-mutation-observer.ts
963
+ const noManualMutationObserver = createRule({
964
+ meta: {
965
+ type: "suggestion",
966
+ docs: { description: "Disallow manual MutationObserver inside Base subclasses; use withMutation decorator instead" },
967
+ messages: { noManual: "Avoid manual \"new MutationObserver()\". Use the \"withMutation\" decorator instead." }
968
+ },
969
+ createOnce(context) {
970
+ return { NewExpression(node) {
971
+ if (node.callee?.name !== "MutationObserver") return;
972
+ const cls = findEnclosingClass(context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? []);
973
+ if (!cls || !isBaseSubclass(cls, context)) return;
974
+ context.report({
975
+ node,
976
+ messageId: "noManual"
977
+ });
978
+ } };
979
+ }
1003
980
  });
1004
-
1005
- // src/rules/emits-kebab-case.ts
981
+ //#endregion
982
+ //#region src/rules/emits-kebab-case.ts
983
+ /**
984
+ * Returns true if the string is kebab-case:
985
+ * all lowercase letters, digits and hyphens, no underscores or uppercase.
986
+ */
1006
987
  function isKebabCase(value) {
1007
- return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
988
+ return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
1008
989
  }
990
+ /**
991
+ * Converts a camelCase, PascalCase or snake_case string to kebab-case.
992
+ */
1009
993
  function toKebabCase(value) {
1010
- return value.replace(/([A-Z])/g, (_, c) => `-${c.toLowerCase()}`).replace(/_/g, "-").replace(/^-/, "").toLowerCase();
994
+ return value.replace(/([A-Z])/g, (_, c) => `-${c.toLowerCase()}`).replace(/_/g, "-").replace(/^-/, "").toLowerCase();
1011
995
  }
1012
- function check7(node, context) {
1013
- if (!isBaseSubclass(node, context)) return;
1014
- const body = node.body?.body ?? [];
1015
- const configProp = body.find(
1016
- (member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config"
1017
- );
1018
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
1019
- const emitsProp = configProp.value.properties?.find(
1020
- (prop) => prop.type === "Property" && prop.key?.name === "emits"
1021
- );
1022
- if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return;
1023
- for (const element of emitsProp.value.elements ?? []) {
1024
- if (!element || element.type !== "Literal") continue;
1025
- const name = element.value;
1026
- if (typeof name !== "string") continue;
1027
- if (!isKebabCase(name)) {
1028
- const fixed = toKebabCase(name);
1029
- context.report({
1030
- node: element,
1031
- messageId: "notKebabCase",
1032
- data: { name },
1033
- fix: (fixer) => fixer.replaceText(element, `'${fixed}'`)
1034
- });
1035
- }
1036
- }
996
+ function check$3(node, context) {
997
+ if (!isBaseSubclass(node, context)) return;
998
+ const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
999
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
1000
+ const emitsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "emits");
1001
+ if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return;
1002
+ for (const element of emitsProp.value.elements ?? []) {
1003
+ if (!element || element.type !== "Literal") continue;
1004
+ const name = element.value;
1005
+ if (typeof name !== "string") continue;
1006
+ if (!isKebabCase(name)) {
1007
+ const fixed = toKebabCase(name);
1008
+ context.report({
1009
+ node: element,
1010
+ messageId: "notKebabCase",
1011
+ data: { name },
1012
+ fix: (fixer) => fixer.replaceText(element, `'${fixed}'`)
1013
+ });
1014
+ }
1015
+ }
1037
1016
  }
1038
- var emitsKebabCase = createRule({
1039
- meta: {
1040
- type: "problem",
1041
- fixable: "code",
1042
- docs: {
1043
- description: "Require emit names in static config to be kebab-case"
1044
- },
1045
- messages: {
1046
- notKebabCase: 'Emit name "{{name}}" must be kebab-case (e.g. "content-change").'
1047
- }
1048
- },
1049
- createOnce(context) {
1050
- return {
1051
- ClassDeclaration(node) {
1052
- check7(node, context);
1053
- },
1054
- ClassExpression(node) {
1055
- check7(node, context);
1056
- }
1057
- };
1058
- }
1017
+ const emitsKebabCase = createRule({
1018
+ meta: {
1019
+ type: "problem",
1020
+ fixable: "code",
1021
+ docs: { description: "Require emit names in static config to be kebab-case" },
1022
+ messages: { notKebabCase: "Emit name \"{{name}}\" must be kebab-case (e.g. \"content-change\")." }
1023
+ },
1024
+ createOnce(context) {
1025
+ return {
1026
+ ClassDeclaration(node) {
1027
+ check$3(node, context);
1028
+ },
1029
+ ClassExpression(node) {
1030
+ check$3(node, context);
1031
+ }
1032
+ };
1033
+ }
1059
1034
  });
1060
-
1061
- // src/rules/emits-multi-word.ts
1062
- function check8(node, context) {
1063
- if (!isBaseSubclass(node, context)) return;
1064
- const body = node.body?.body ?? [];
1065
- const configProp = body.find(
1066
- (member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config"
1067
- );
1068
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
1069
- const emitsProp = configProp.value.properties?.find(
1070
- (prop) => prop.type === "Property" && prop.key?.name === "emits"
1071
- );
1072
- if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return;
1073
- for (const element of emitsProp.value.elements ?? []) {
1074
- if (!element || element.type !== "Literal") continue;
1075
- const name = element.value;
1076
- if (typeof name !== "string") continue;
1077
- if (!name.includes("-")) {
1078
- context.report({
1079
- node: element,
1080
- messageId: "singleWord",
1081
- data: { name }
1082
- });
1083
- }
1084
- }
1035
+ //#endregion
1036
+ //#region src/rules/emits-multi-word.ts
1037
+ function check$2(node, context) {
1038
+ if (!isBaseSubclass(node, context)) return;
1039
+ const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
1040
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
1041
+ const emitsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "emits");
1042
+ if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return;
1043
+ for (const element of emitsProp.value.elements ?? []) {
1044
+ if (!element || element.type !== "Literal") continue;
1045
+ const name = element.value;
1046
+ if (typeof name !== "string") continue;
1047
+ if (!name.includes("-")) context.report({
1048
+ node: element,
1049
+ messageId: "singleWord",
1050
+ data: { name }
1051
+ });
1052
+ }
1085
1053
  }
1086
- var emitsMultiWord = createRule({
1087
- meta: {
1088
- type: "problem",
1089
- docs: {
1090
- description: "Require emit names in static config to be multi-word to avoid collision with native DOM events"
1091
- },
1092
- messages: {
1093
- singleWord: 'Emit name "{{name}}" must be multi-word (e.g. "item-click" instead of "click") to avoid collisions with native DOM events.'
1094
- }
1095
- },
1096
- createOnce(context) {
1097
- return {
1098
- ClassDeclaration(node) {
1099
- check8(node, context);
1100
- },
1101
- ClassExpression(node) {
1102
- check8(node, context);
1103
- }
1104
- };
1105
- }
1054
+ const emitsMultiWord = createRule({
1055
+ meta: {
1056
+ type: "problem",
1057
+ docs: { description: "Require emit names in static config to be multi-word to avoid collision with native DOM events" },
1058
+ messages: { singleWord: "Emit name \"{{name}}\" must be multi-word (e.g. \"item-click\" instead of \"click\") to avoid collisions with native DOM events." }
1059
+ },
1060
+ createOnce(context) {
1061
+ return {
1062
+ ClassDeclaration(node) {
1063
+ check$2(node, context);
1064
+ },
1065
+ ClassExpression(node) {
1066
+ check$2(node, context);
1067
+ }
1068
+ };
1069
+ }
1106
1070
  });
1107
-
1108
- // src/rules/components-pascal-case.ts
1109
- function check9(node, context) {
1110
- if (!isBaseSubclass(node, context)) return;
1111
- const body = node.body?.body ?? [];
1112
- const configProp = body.find(
1113
- (member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config"
1114
- );
1115
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
1116
- const componentsProp = configProp.value.properties?.find(
1117
- (prop) => prop.type === "Property" && prop.key?.name === "components"
1118
- );
1119
- if (!componentsProp?.value || componentsProp.value.type !== "ObjectExpression") return;
1120
- for (const prop of componentsProp.value.properties ?? []) {
1121
- if (prop.type !== "Property") continue;
1122
- const name = prop.key?.name ?? prop.key?.value;
1123
- if (typeof name !== "string") continue;
1124
- if (!isPascalCase(name)) {
1125
- const fixed = toPascalCase(name);
1126
- const fixedText = prop.key.type === "Literal" ? `'${fixed}'` : fixed;
1127
- context.report({
1128
- node: prop.key,
1129
- messageId: "notPascalCase",
1130
- data: { name },
1131
- fix: (fixer) => fixer.replaceText(prop.key, fixedText)
1132
- });
1133
- }
1134
- }
1071
+ //#endregion
1072
+ //#region src/rules/components-pascal-case.ts
1073
+ function check$1(node, context) {
1074
+ if (!isBaseSubclass(node, context)) return;
1075
+ const configProp = (node.body?.body ?? []).find((member) => member.type === "PropertyDefinition" && member.static === true && member.key?.name === "config");
1076
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return;
1077
+ const componentsProp = configProp.value.properties?.find((prop) => prop.type === "Property" && prop.key?.name === "components");
1078
+ if (!componentsProp?.value || componentsProp.value.type !== "ObjectExpression") return;
1079
+ for (const prop of componentsProp.value.properties ?? []) {
1080
+ if (prop.type !== "Property") continue;
1081
+ const name = prop.key?.name ?? prop.key?.value;
1082
+ if (typeof name !== "string") continue;
1083
+ if (!isPascalCase(name)) {
1084
+ const fixed = toPascalCase(name);
1085
+ const fixedText = prop.key.type === "Literal" ? `'${fixed}'` : fixed;
1086
+ context.report({
1087
+ node: prop.key,
1088
+ messageId: "notPascalCase",
1089
+ data: { name },
1090
+ fix: (fixer) => fixer.replaceText(prop.key, fixedText)
1091
+ });
1092
+ }
1093
+ }
1135
1094
  }
1136
- var componentsPascalCase = createRule({
1137
- meta: {
1138
- type: "problem",
1139
- fixable: "code",
1140
- docs: {
1141
- description: "Require component keys in static config to be PascalCase"
1142
- },
1143
- messages: {
1144
- notPascalCase: 'Component key "{{name}}" must be PascalCase (e.g. "MyComponent").'
1145
- }
1146
- },
1147
- createOnce(context) {
1148
- return {
1149
- ClassDeclaration(node) {
1150
- check9(node, context);
1151
- },
1152
- ClassExpression(node) {
1153
- check9(node, context);
1154
- }
1155
- };
1156
- }
1095
+ const componentsPascalCase = createRule({
1096
+ meta: {
1097
+ type: "problem",
1098
+ fixable: "code",
1099
+ docs: { description: "Require component keys in static config to be PascalCase" },
1100
+ messages: { notPascalCase: "Component key \"{{name}}\" must be PascalCase (e.g. \"MyComponent\")." }
1101
+ },
1102
+ createOnce(context) {
1103
+ return {
1104
+ ClassDeclaration(node) {
1105
+ check$1(node, context);
1106
+ },
1107
+ ClassExpression(node) {
1108
+ check$1(node, context);
1109
+ }
1110
+ };
1111
+ }
1157
1112
  });
1158
-
1159
- // src/rules/require-emit-declared-in-config.ts
1113
+ //#endregion
1114
+ //#region src/rules/require-emit-declared-in-config.ts
1160
1115
  function collectDeclaredEmits(classNode) {
1161
- const body = classNode.body?.body ?? [];
1162
- const configProp = body.find(
1163
- (m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config"
1164
- );
1165
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1166
- const emitsProp = configProp.value.properties?.find(
1167
- (p) => p.type === "Property" && p.key?.name === "emits"
1168
- );
1169
- if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return /* @__PURE__ */ new Set();
1170
- const names = /* @__PURE__ */ new Set();
1171
- for (const el of emitsProp.value.elements ?? []) {
1172
- if (el?.type === "Literal" && typeof el.value === "string") {
1173
- names.add(el.value);
1174
- }
1175
- }
1176
- return names;
1116
+ const configProp = (classNode.body?.body ?? []).find((m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config");
1117
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1118
+ const emitsProp = configProp.value.properties?.find((p) => p.type === "Property" && p.key?.name === "emits");
1119
+ if (!emitsProp?.value || emitsProp.value.type !== "ArrayExpression") return /* @__PURE__ */ new Set();
1120
+ const names = /* @__PURE__ */ new Set();
1121
+ for (const el of emitsProp.value.elements ?? []) if (el?.type === "Literal" && typeof el.value === "string") names.add(el.value);
1122
+ return names;
1177
1123
  }
1124
+ /**
1125
+ * Returns true if `node` looks like `this.$emit`.
1126
+ */
1178
1127
  function isThisEmit(node) {
1179
- return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$emit";
1128
+ return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$emit";
1180
1129
  }
1181
- var requireEmitDeclaredInConfig = createRule({
1182
- meta: {
1183
- type: "problem",
1184
- docs: {
1185
- description: "Require all this.$emit() calls to use event names declared in static config.emits"
1186
- },
1187
- messages: {
1188
- undeclared: 'Emit "{{name}}" is not declared in static config.emits.'
1189
- }
1190
- },
1191
- createOnce(context) {
1192
- return {
1193
- "ClassDeclaration, ClassExpression"(classNode) {
1194
- if (!isBaseSubclass(classNode, context)) return;
1195
- const declared = collectDeclaredEmits(classNode);
1196
- walkNode2(classNode.body, (node) => {
1197
- if (node.type !== "CallExpression") return;
1198
- if (!isThisEmit(node.callee)) return;
1199
- const firstArg = node.arguments?.[0];
1200
- if (!firstArg || firstArg.type !== "Literal") return;
1201
- const name = firstArg.value;
1202
- if (typeof name !== "string") return;
1203
- if (!declared.has(name)) {
1204
- context.report({
1205
- node: firstArg,
1206
- messageId: "undeclared",
1207
- data: { name }
1208
- });
1209
- }
1210
- });
1211
- }
1212
- };
1213
- }
1130
+ const requireEmitDeclaredInConfig = createRule({
1131
+ meta: {
1132
+ type: "problem",
1133
+ docs: { description: "Require all this.$emit() calls to use event names declared in static config.emits" },
1134
+ messages: { undeclared: "Emit \"{{name}}\" is not declared in static config.emits." }
1135
+ },
1136
+ createOnce(context) {
1137
+ return { "ClassDeclaration, ClassExpression"(classNode) {
1138
+ if (!isBaseSubclass(classNode, context)) return;
1139
+ const declared = collectDeclaredEmits(classNode);
1140
+ walkNode$2(classNode.body, (node) => {
1141
+ if (node.type !== "CallExpression") return;
1142
+ if (!isThisEmit(node.callee)) return;
1143
+ const firstArg = node.arguments?.[0];
1144
+ if (!firstArg || firstArg.type !== "Literal") return;
1145
+ const name = firstArg.value;
1146
+ if (typeof name !== "string") return;
1147
+ if (!declared.has(name)) context.report({
1148
+ node: firstArg,
1149
+ messageId: "undeclared",
1150
+ data: { name }
1151
+ });
1152
+ });
1153
+ } };
1154
+ }
1214
1155
  });
1215
- function walkNode2(node, visit) {
1216
- if (!node || typeof node !== "object") return;
1217
- visit(node);
1218
- for (const key of Object.keys(node)) {
1219
- if (key === "parent") continue;
1220
- const child = node[key];
1221
- if (Array.isArray(child)) {
1222
- for (const item of child) walkNode2(item, visit);
1223
- } else if (child && typeof child === "object" && child.type) {
1224
- walkNode2(child, visit);
1225
- }
1226
- }
1156
+ function walkNode$2(node, visit) {
1157
+ if (!node || typeof node !== "object") return;
1158
+ visit(node);
1159
+ for (const key of Object.keys(node)) {
1160
+ if (key === "parent") continue;
1161
+ const child = node[key];
1162
+ if (Array.isArray(child)) for (const item of child) walkNode$2(item, visit);
1163
+ else if (child && typeof child === "object" && child.type) walkNode$2(child, visit);
1164
+ }
1227
1165
  }
1228
-
1229
- // src/rules/require-children-declared-in-config.ts
1166
+ //#endregion
1167
+ //#region src/rules/require-children-declared-in-config.ts
1230
1168
  function collectDeclaredComponents(classNode) {
1231
- const body = classNode.body?.body ?? [];
1232
- const configProp = body.find(
1233
- (m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config"
1234
- );
1235
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1236
- const componentsProp = configProp.value.properties?.find(
1237
- (p) => p.type === "Property" && p.key?.name === "components"
1238
- );
1239
- if (!componentsProp?.value || componentsProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1240
- const names = /* @__PURE__ */ new Set();
1241
- for (const prop of componentsProp.value.properties ?? []) {
1242
- if (prop.type !== "Property") continue;
1243
- const name = prop.key?.name ?? prop.key?.value;
1244
- if (typeof name === "string") {
1245
- names.add(name);
1246
- }
1247
- }
1248
- return names;
1169
+ const configProp = (classNode.body?.body ?? []).find((m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config");
1170
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1171
+ const componentsProp = configProp.value.properties?.find((p) => p.type === "Property" && p.key?.name === "components");
1172
+ if (!componentsProp?.value || componentsProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1173
+ const names = /* @__PURE__ */ new Set();
1174
+ for (const prop of componentsProp.value.properties ?? []) {
1175
+ if (prop.type !== "Property") continue;
1176
+ const name = prop.key?.name ?? prop.key?.value;
1177
+ if (typeof name === "string") names.add(name);
1178
+ }
1179
+ return names;
1249
1180
  }
1181
+ /**
1182
+ * Returns true if `node` looks like `this.$children`.
1183
+ */
1250
1184
  function isThisChildren(node) {
1251
- return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$children";
1185
+ return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$children";
1252
1186
  }
1253
- var requireChildrenDeclaredInConfig = createRule({
1254
- meta: {
1255
- type: "problem",
1256
- docs: {
1257
- description: "Require all this.$children accesses to use component names declared in static config.components"
1258
- },
1259
- messages: {
1260
- undeclared: 'Component "{{name}}" is not declared in static config.components.'
1261
- }
1262
- },
1263
- createOnce(context) {
1264
- return {
1265
- "ClassDeclaration, ClassExpression"(classNode) {
1266
- if (!isBaseSubclass(classNode, context)) return;
1267
- const declared = collectDeclaredComponents(classNode);
1268
- walkNode3(classNode.body, (node) => {
1269
- if (node.type !== "MemberExpression") return;
1270
- if (!isThisChildren(node.object)) return;
1271
- let name = null;
1272
- if (!node.computed && node.property?.type === "Identifier") {
1273
- name = node.property.name;
1274
- } else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") {
1275
- name = node.property.value;
1276
- }
1277
- if (name && !declared.has(name)) {
1278
- context.report({
1279
- node,
1280
- messageId: "undeclared",
1281
- data: { name }
1282
- });
1283
- }
1284
- });
1285
- }
1286
- };
1287
- }
1187
+ const requireChildrenDeclaredInConfig = createRule({
1188
+ meta: {
1189
+ type: "problem",
1190
+ docs: { description: "Require all this.$children accesses to use component names declared in static config.components" },
1191
+ messages: { undeclared: "Component \"{{name}}\" is not declared in static config.components." }
1192
+ },
1193
+ createOnce(context) {
1194
+ return { "ClassDeclaration, ClassExpression"(classNode) {
1195
+ if (!isBaseSubclass(classNode, context)) return;
1196
+ const declared = collectDeclaredComponents(classNode);
1197
+ walkNode$1(classNode.body, (node) => {
1198
+ if (node.type !== "MemberExpression") return;
1199
+ if (!isThisChildren(node.object)) return;
1200
+ let name = null;
1201
+ if (!node.computed && node.property?.type === "Identifier") name = node.property.name;
1202
+ else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") name = node.property.value;
1203
+ if (name && !declared.has(name)) context.report({
1204
+ node,
1205
+ messageId: "undeclared",
1206
+ data: { name }
1207
+ });
1208
+ });
1209
+ } };
1210
+ }
1288
1211
  });
1289
- function walkNode3(node, visit) {
1290
- if (!node || typeof node !== "object") return;
1291
- visit(node);
1292
- for (const key of Object.keys(node)) {
1293
- if (key === "parent") continue;
1294
- const child = node[key];
1295
- if (Array.isArray(child)) {
1296
- for (const item of child) walkNode3(item, visit);
1297
- } else if (child && typeof child === "object" && child.type) {
1298
- walkNode3(child, visit);
1299
- }
1300
- }
1212
+ function walkNode$1(node, visit) {
1213
+ if (!node || typeof node !== "object") return;
1214
+ visit(node);
1215
+ for (const key of Object.keys(node)) {
1216
+ if (key === "parent") continue;
1217
+ const child = node[key];
1218
+ if (Array.isArray(child)) for (const item of child) walkNode$1(item, visit);
1219
+ else if (child && typeof child === "object" && child.type) walkNode$1(child, visit);
1220
+ }
1301
1221
  }
1302
-
1303
- // src/rules/require-options-declared-in-config.ts
1222
+ //#endregion
1223
+ //#region src/rules/require-options-declared-in-config.ts
1304
1224
  function collectDeclaredOptions(classNode) {
1305
- const body = classNode.body?.body ?? [];
1306
- const configProp = body.find(
1307
- (m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config"
1308
- );
1309
- if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1310
- const optionsProp = configProp.value.properties?.find(
1311
- (p) => p.type === "Property" && p.key?.name === "options"
1312
- );
1313
- if (!optionsProp?.value || optionsProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1314
- const names = /* @__PURE__ */ new Set();
1315
- for (const prop of optionsProp.value.properties ?? []) {
1316
- if (prop.type !== "Property") continue;
1317
- const name = prop.key?.name ?? prop.key?.value;
1318
- if (typeof name === "string") {
1319
- names.add(toCamelCase(name));
1320
- }
1321
- }
1322
- return names;
1225
+ const configProp = (classNode.body?.body ?? []).find((m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config");
1226
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1227
+ const optionsProp = configProp.value.properties?.find((p) => p.type === "Property" && p.key?.name === "options");
1228
+ if (!optionsProp?.value || optionsProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
1229
+ const names = /* @__PURE__ */ new Set();
1230
+ for (const prop of optionsProp.value.properties ?? []) {
1231
+ if (prop.type !== "Property") continue;
1232
+ const name = prop.key?.name ?? prop.key?.value;
1233
+ if (typeof name === "string") names.add(toCamelCase(name));
1234
+ }
1235
+ return names;
1323
1236
  }
1237
+ /**
1238
+ * Returns true if `node` looks like `this.$options`.
1239
+ */
1324
1240
  function isThisOptions(node) {
1325
- return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$options";
1241
+ return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$options";
1326
1242
  }
1327
- var requireOptionsDeclaredInConfig = createRule({
1328
- meta: {
1329
- type: "problem",
1330
- docs: {
1331
- description: "Require all this.$options accesses to use option names declared in static config.options"
1332
- },
1333
- messages: {
1334
- undeclared: 'Option "{{name}}" is not declared in static config.options.'
1335
- }
1336
- },
1337
- createOnce(context) {
1338
- return {
1339
- "ClassDeclaration, ClassExpression"(classNode) {
1340
- if (!isBaseSubclass(classNode, context)) return;
1341
- const declared = collectDeclaredOptions(classNode);
1342
- walkNode4(classNode.body, (node) => {
1343
- if (node.type !== "MemberExpression") return;
1344
- if (!isThisOptions(node.object)) return;
1345
- let name = null;
1346
- if (!node.computed && node.property?.type === "Identifier") {
1347
- name = node.property.name;
1348
- } else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") {
1349
- name = toCamelCase(node.property.value);
1350
- }
1351
- if (name && !declared.has(name)) {
1352
- context.report({
1353
- node,
1354
- messageId: "undeclared",
1355
- data: { name }
1356
- });
1357
- }
1358
- });
1359
- }
1360
- };
1361
- }
1243
+ const requireOptionsDeclaredInConfig = createRule({
1244
+ meta: {
1245
+ type: "problem",
1246
+ docs: { description: "Require all this.$options accesses to use option names declared in static config.options" },
1247
+ messages: { undeclared: "Option \"{{name}}\" is not declared in static config.options." }
1248
+ },
1249
+ createOnce(context) {
1250
+ return { "ClassDeclaration, ClassExpression"(classNode) {
1251
+ if (!isBaseSubclass(classNode, context)) return;
1252
+ const declared = collectDeclaredOptions(classNode);
1253
+ walkNode(classNode.body, (node) => {
1254
+ if (node.type !== "MemberExpression") return;
1255
+ if (!isThisOptions(node.object)) return;
1256
+ let name = null;
1257
+ if (!node.computed && node.property?.type === "Identifier") name = node.property.name;
1258
+ else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") name = toCamelCase(node.property.value);
1259
+ if (name && !declared.has(name)) context.report({
1260
+ node,
1261
+ messageId: "undeclared",
1262
+ data: { name }
1263
+ });
1264
+ });
1265
+ } };
1266
+ }
1362
1267
  });
1363
- function walkNode4(node, visit) {
1364
- if (!node || typeof node !== "object") return;
1365
- visit(node);
1366
- for (const key of Object.keys(node)) {
1367
- if (key === "parent") continue;
1368
- const child = node[key];
1369
- if (Array.isArray(child)) {
1370
- for (const item of child) walkNode4(item, visit);
1371
- } else if (child && typeof child === "object" && child.type) {
1372
- walkNode4(child, visit);
1373
- }
1374
- }
1268
+ function walkNode(node, visit) {
1269
+ if (!node || typeof node !== "object") return;
1270
+ visit(node);
1271
+ for (const key of Object.keys(node)) {
1272
+ if (key === "parent") continue;
1273
+ const child = node[key];
1274
+ if (Array.isArray(child)) for (const item of child) walkNode(item, visit);
1275
+ else if (child && typeof child === "object" && child.type) walkNode(child, visit);
1276
+ }
1375
1277
  }
1376
-
1377
- // src/rules/prefer-destructured-lookups.ts
1378
- var TRACKED_PROPERTIES = /* @__PURE__ */ new Set(["$refs", "$options", "$children"]);
1278
+ //#endregion
1279
+ //#region src/rules/prefer-destructured-lookups.ts
1280
+ const TRACKED_PROPERTIES = /* @__PURE__ */ new Set([
1281
+ "$refs",
1282
+ "$options",
1283
+ "$children"
1284
+ ]);
1379
1285
  function isTrackedLookup(node) {
1380
- if (node.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.type === "ThisExpression" && node.object.property?.type === "Identifier" && TRACKED_PROPERTIES.has(node.object.property.name) && node.property?.type === "Identifier" && !node.computed) {
1381
- return { prop: node.object.property.name, name: node.property.name };
1382
- }
1383
- return null;
1286
+ if (node.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.type === "ThisExpression" && node.object.property?.type === "Identifier" && TRACKED_PROPERTIES.has(node.object.property.name) && node.property?.type === "Identifier" && !node.computed) return {
1287
+ prop: node.object.property.name,
1288
+ name: node.property.name
1289
+ };
1290
+ return null;
1384
1291
  }
1385
1292
  function findEnclosingMethod(ancestors) {
1386
- for (let i = ancestors.length - 1; i >= 0; i--) {
1387
- const node = ancestors[i];
1388
- if (node.type === "MethodDefinition") return node;
1389
- }
1390
- return null;
1293
+ for (let i = ancestors.length - 1; i >= 0; i--) {
1294
+ const node = ancestors[i];
1295
+ if (node.type === "MethodDefinition") return node;
1296
+ }
1297
+ return null;
1391
1298
  }
1392
- var preferDestructuredLookups = createRule({
1393
- meta: {
1394
- type: "suggestion",
1395
- docs: {
1396
- description: "Prefer destructuring this.$refs, this.$options and this.$children into local variables when accessed multiple times in the same method"
1397
- },
1398
- messages: {
1399
- preferDestructured: '"this.{{prop}}.{{name}}" is accessed {{count}} times. Destructure it into a local variable to avoid repeated lookups.'
1400
- }
1401
- },
1402
- createOnce(context) {
1403
- const methodLookups = /* @__PURE__ */ new Map();
1404
- return {
1405
- MemberExpression(node) {
1406
- const match = isTrackedLookup(node);
1407
- if (!match) return;
1408
- const ancestors = context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
1409
- const method = findEnclosingMethod(ancestors);
1410
- if (!method) return;
1411
- const cls = findEnclosingClass(ancestors);
1412
- if (!cls || !isBaseSubclass(cls, context)) return;
1413
- if (!methodLookups.has(method)) methodLookups.set(method, /* @__PURE__ */ new Map());
1414
- const map = methodLookups.get(method);
1415
- const key = `${match.prop}.${match.name}`;
1416
- const list = map.get(key) ?? [];
1417
- list.push(node);
1418
- map.set(key, list);
1419
- },
1420
- "MethodDefinition:exit"(node) {
1421
- const map = methodLookups.get(node);
1422
- if (!map) return;
1423
- methodLookups.delete(node);
1424
- for (const [key, nodes] of map) {
1425
- if (nodes.length < 2) continue;
1426
- const [prop, name] = key.split(".");
1427
- context.report({
1428
- node: nodes[0],
1429
- messageId: "preferDestructured",
1430
- data: { prop, name, count: String(nodes.length) }
1431
- });
1432
- }
1433
- }
1434
- };
1435
- }
1299
+ const preferDestructuredLookups = createRule({
1300
+ meta: {
1301
+ type: "suggestion",
1302
+ docs: { description: "Prefer destructuring this.$refs, this.$options and this.$children into local variables when accessed multiple times in the same method" },
1303
+ messages: { preferDestructured: "\"this.{{prop}}.{{name}}\" is accessed {{count}} times. Destructure it into a local variable to avoid repeated lookups." }
1304
+ },
1305
+ createOnce(context) {
1306
+ const methodLookups = /* @__PURE__ */ new Map();
1307
+ return {
1308
+ MemberExpression(node) {
1309
+ const match = isTrackedLookup(node);
1310
+ if (!match) return;
1311
+ const ancestors = context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
1312
+ const method = findEnclosingMethod(ancestors);
1313
+ if (!method) return;
1314
+ const cls = findEnclosingClass(ancestors);
1315
+ if (!cls || !isBaseSubclass(cls, context)) return;
1316
+ if (!methodLookups.has(method)) methodLookups.set(method, /* @__PURE__ */ new Map());
1317
+ const map = methodLookups.get(method);
1318
+ const key = `${match.prop}.${match.name}`;
1319
+ const list = map.get(key) ?? [];
1320
+ list.push(node);
1321
+ map.set(key, list);
1322
+ },
1323
+ "MethodDefinition:exit"(node) {
1324
+ const map = methodLookups.get(node);
1325
+ if (!map) return;
1326
+ methodLookups.delete(node);
1327
+ for (const [key, nodes] of map) {
1328
+ if (nodes.length < 2) continue;
1329
+ const [prop, name] = key.split(".");
1330
+ context.report({
1331
+ node: nodes[0],
1332
+ messageId: "preferDestructured",
1333
+ data: {
1334
+ prop,
1335
+ name,
1336
+ count: String(nodes.length)
1337
+ }
1338
+ });
1339
+ }
1340
+ }
1341
+ };
1342
+ }
1436
1343
  });
1437
-
1438
- // src/rules/no-dollar-prefix.ts
1439
- var noDollarPrefix = createRule({
1440
- meta: {
1441
- type: "problem",
1442
- docs: {
1443
- description: 'Disallow user-defined methods and properties prefixed with "$" in Base subclasses \u2014 "$" is reserved for js-toolkit framework members'
1444
- },
1445
- messages: {
1446
- noDollarPrefix: '"${{name}}" uses the "$" prefix, which is reserved for js-toolkit framework members. Rename it without the "$" prefix.'
1447
- }
1448
- },
1449
- createOnce(context) {
1450
- return {
1451
- ClassDeclaration(node) {
1452
- check10(node, context);
1453
- },
1454
- ClassExpression(node) {
1455
- check10(node, context);
1456
- }
1457
- };
1458
- }
1344
+ //#endregion
1345
+ //#region src/rules/no-dollar-prefix.ts
1346
+ const noDollarPrefix = createRule({
1347
+ meta: {
1348
+ type: "problem",
1349
+ docs: { description: "Disallow user-defined methods and properties prefixed with \"$\" in Base subclasses — \"$\" is reserved for js-toolkit framework members" },
1350
+ messages: { noDollarPrefix: "\"${{name}}\" uses the \"$\" prefix, which is reserved for js-toolkit framework members. Rename it without the \"$\" prefix." }
1351
+ },
1352
+ createOnce(context) {
1353
+ return {
1354
+ ClassDeclaration(node) {
1355
+ check(node, context);
1356
+ },
1357
+ ClassExpression(node) {
1358
+ check(node, context);
1359
+ }
1360
+ };
1361
+ }
1459
1362
  });
1460
- function check10(node, context) {
1461
- if (!isBaseSubclass(node, context)) return;
1462
- for (const member of node.body?.body ?? []) {
1463
- if (member.static) continue;
1464
- if (member.type !== "MethodDefinition" && member.type !== "PropertyDefinition") continue;
1465
- const name = member.key?.name ?? "";
1466
- if (name.startsWith("$")) {
1467
- context.report({
1468
- node: member.key,
1469
- messageId: "noDollarPrefix",
1470
- data: { name: name.slice(1) }
1471
- });
1472
- }
1473
- }
1363
+ function check(node, context) {
1364
+ if (!isBaseSubclass(node, context)) return;
1365
+ for (const member of node.body?.body ?? []) {
1366
+ if (member.static) continue;
1367
+ if (member.type !== "MethodDefinition" && member.type !== "PropertyDefinition") continue;
1368
+ const name = member.key?.name ?? "";
1369
+ if (name.startsWith("$")) context.report({
1370
+ node: member.key,
1371
+ messageId: "noDollarPrefix",
1372
+ data: { name: name.slice(1) }
1373
+ });
1374
+ }
1474
1375
  }
1475
-
1476
- // src/rules/require-destroyed-cleanup.ts
1477
- var SETUP_CALLS = /* @__PURE__ */ new Set(["setTimeout", "setInterval", "requestAnimationFrame"]);
1478
- var requireDestroyedCleanup = createRule({
1479
- meta: {
1480
- type: "suggestion",
1481
- docs: {
1482
- description: "Require a destroyed() method in Base subclasses that use setTimeout, setInterval or requestAnimationFrame"
1483
- },
1484
- messages: {
1485
- requireDestroyed: "This component uses {{calls}} but has no destroyed() method. Add destroyed() to clear timers and avoid memory leaks."
1486
- }
1487
- },
1488
- createOnce(context) {
1489
- const classTimers = /* @__PURE__ */ new Map();
1490
- const classHasDestroyed = /* @__PURE__ */ new Map();
1491
- return {
1492
- ClassDeclaration(node) {
1493
- classTimers.set(node, /* @__PURE__ */ new Set());
1494
- classHasDestroyed.set(node, false);
1495
- },
1496
- ClassExpression(node) {
1497
- classTimers.set(node, /* @__PURE__ */ new Set());
1498
- classHasDestroyed.set(node, false);
1499
- },
1500
- MethodDefinition(node) {
1501
- if (node.key?.name === "destroyed") {
1502
- const sourceCode = context.sourceCode ?? context.getSourceCode?.();
1503
- const ancestors = context.getAncestors?.() ?? sourceCode?.getAncestors?.(node) ?? [];
1504
- for (let i = ancestors.length - 1; i >= 0; i--) {
1505
- const a = ancestors[i];
1506
- if (a.type === "ClassDeclaration" || a.type === "ClassExpression") {
1507
- classHasDestroyed.set(a, true);
1508
- break;
1509
- }
1510
- }
1511
- }
1512
- },
1513
- CallExpression(node) {
1514
- if (node.callee?.type !== "Identifier") return;
1515
- if (!SETUP_CALLS.has(node.callee.name)) return;
1516
- const sourceCode = context.sourceCode ?? context.getSourceCode?.();
1517
- const ancestors = context.getAncestors?.() ?? sourceCode?.getAncestors?.(node) ?? [];
1518
- for (let i = ancestors.length - 1; i >= 0; i--) {
1519
- const a = ancestors[i];
1520
- if (a.type === "ClassDeclaration" || a.type === "ClassExpression") {
1521
- if (!isBaseSubclass(a, context)) break;
1522
- classTimers.get(a)?.add(node.callee.name);
1523
- break;
1524
- }
1525
- }
1526
- },
1527
- "ClassDeclaration:exit"(node) {
1528
- reportIfNeeded(node, context, classTimers, classHasDestroyed);
1529
- },
1530
- "ClassExpression:exit"(node) {
1531
- reportIfNeeded(node, context, classTimers, classHasDestroyed);
1532
- }
1533
- };
1534
- }
1376
+ //#endregion
1377
+ //#region src/rules/require-destroyed-cleanup.ts
1378
+ const SETUP_CALLS = /* @__PURE__ */ new Set([
1379
+ "setTimeout",
1380
+ "setInterval",
1381
+ "requestAnimationFrame"
1382
+ ]);
1383
+ const requireDestroyedCleanup = createRule({
1384
+ meta: {
1385
+ type: "suggestion",
1386
+ docs: { description: "Require a destroyed() method in Base subclasses that use setTimeout, setInterval or requestAnimationFrame" },
1387
+ messages: { requireDestroyed: "This component uses {{calls}} but has no destroyed() method. Add destroyed() to clear timers and avoid memory leaks." }
1388
+ },
1389
+ createOnce(context) {
1390
+ const classTimers = /* @__PURE__ */ new Map();
1391
+ const classHasDestroyed = /* @__PURE__ */ new Map();
1392
+ return {
1393
+ ClassDeclaration(node) {
1394
+ classTimers.set(node, /* @__PURE__ */ new Set());
1395
+ classHasDestroyed.set(node, false);
1396
+ },
1397
+ ClassExpression(node) {
1398
+ classTimers.set(node, /* @__PURE__ */ new Set());
1399
+ classHasDestroyed.set(node, false);
1400
+ },
1401
+ MethodDefinition(node) {
1402
+ if (node.key?.name === "destroyed") {
1403
+ const sourceCode = context.sourceCode ?? context.getSourceCode?.();
1404
+ const ancestors = context.getAncestors?.() ?? sourceCode?.getAncestors?.(node) ?? [];
1405
+ for (let i = ancestors.length - 1; i >= 0; i--) {
1406
+ const a = ancestors[i];
1407
+ if (a.type === "ClassDeclaration" || a.type === "ClassExpression") {
1408
+ classHasDestroyed.set(a, true);
1409
+ break;
1410
+ }
1411
+ }
1412
+ }
1413
+ },
1414
+ CallExpression(node) {
1415
+ if (node.callee?.type !== "Identifier") return;
1416
+ if (!SETUP_CALLS.has(node.callee.name)) return;
1417
+ const sourceCode = context.sourceCode ?? context.getSourceCode?.();
1418
+ const ancestors = context.getAncestors?.() ?? sourceCode?.getAncestors?.(node) ?? [];
1419
+ for (let i = ancestors.length - 1; i >= 0; i--) {
1420
+ const a = ancestors[i];
1421
+ if (a.type === "ClassDeclaration" || a.type === "ClassExpression") {
1422
+ if (!isBaseSubclass(a, context)) break;
1423
+ classTimers.get(a)?.add(node.callee.name);
1424
+ break;
1425
+ }
1426
+ }
1427
+ },
1428
+ "ClassDeclaration:exit"(node) {
1429
+ reportIfNeeded(node, context, classTimers, classHasDestroyed);
1430
+ },
1431
+ "ClassExpression:exit"(node) {
1432
+ reportIfNeeded(node, context, classTimers, classHasDestroyed);
1433
+ }
1434
+ };
1435
+ }
1535
1436
  });
1536
1437
  function reportIfNeeded(node, context, classTimers, classHasDestroyed) {
1537
- const timers = classTimers.get(node);
1538
- const hasDestroyed = classHasDestroyed.get(node);
1539
- classTimers.delete(node);
1540
- classHasDestroyed.delete(node);
1541
- if (!timers || timers.size === 0) return;
1542
- if (hasDestroyed) return;
1543
- context.report({
1544
- node: node.id ?? node,
1545
- messageId: "requireDestroyed",
1546
- data: { calls: [...timers].join(", ") }
1547
- });
1438
+ const timers = classTimers.get(node);
1439
+ const hasDestroyed = classHasDestroyed.get(node);
1440
+ classTimers.delete(node);
1441
+ classHasDestroyed.delete(node);
1442
+ if (!timers || timers.size === 0) return;
1443
+ if (hasDestroyed) return;
1444
+ context.report({
1445
+ node: node.id ?? node,
1446
+ messageId: "requireDestroyed",
1447
+ data: { calls: [...timers].join(", ") }
1448
+ });
1548
1449
  }
1549
-
1550
- // src/index.ts
1551
- var PLUGIN_NAME = "js-toolkit";
1552
- var rules = {
1553
- "require-config": requireConfig,
1554
- "require-config-name-pascal-case": requireConfigNamePascalCase,
1555
- "refs-camel-case": refsCamelCase,
1556
- "refs-plural-multiple": refsPluralMultiple,
1557
- "options-camel-case": optionsCamelCase,
1558
- "async-lifecycle-methods": asyncLifecycleMethods,
1559
- "on-handler-naming": onHandlerNaming,
1560
- "on-global-handler-prefix": onGlobalHandlerPrefix,
1561
- "no-deprecated-properties": noDeprecatedProperties,
1562
- "no-dispatch-event": noDispatchEvent,
1563
- "no-shadow-dom": noShadowDom,
1564
- "no-create-app": noCreateApp,
1565
- "no-event-listener-methods": noEventListenerMethods,
1566
- "no-deep-utils-import": noDeepUtilsImport,
1567
- "no-internal-barrel-import": noInternalBarrelImport,
1568
- "refs-no-bracket-access": refsNoBracketAccess,
1569
- "no-redundant-with-mount-when-in-view": noRedundantWithMountWhenInView,
1570
- "prefer-ref-over-query-selector": preferRefOverQuerySelector,
1571
- "require-refs-declared-in-config": requireRefsDeclaredInConfig,
1572
- "no-manual-intersection-observer": noManualIntersectionObserver,
1573
- "no-manual-mutation-observer": noManualMutationObserver,
1574
- "emits-kebab-case": emitsKebabCase,
1575
- "emits-multi-word": emitsMultiWord,
1576
- "components-pascal-case": componentsPascalCase,
1577
- "require-emit-declared-in-config": requireEmitDeclaredInConfig,
1578
- "require-children-declared-in-config": requireChildrenDeclaredInConfig,
1579
- "require-options-declared-in-config": requireOptionsDeclaredInConfig,
1580
- "prefer-destructured-lookups": preferDestructuredLookups,
1581
- "no-dollar-prefix": noDollarPrefix,
1582
- "require-destroyed-cleanup": requireDestroyedCleanup
1450
+ //#endregion
1451
+ //#region src/rules/no-write-in-read-phase.ts
1452
+ /**
1453
+ * Hooks whose service fans its subscribers out inside `defaultScheduler.read()`.
1454
+ *
1455
+ * `raf.ts`, `scroll.ts` and `scroll-progress.ts` all coalesce into the read
1456
+ * phase. The other services — resize, pointer, key, mutation, in-view — emit
1457
+ * straight from their listener or observer, in no phase at all, so their hooks
1458
+ * are not read-phase contexts and are deliberately absent.
1459
+ */
1460
+ const READ_PHASE_HOOKS = /* @__PURE__ */ new Set([
1461
+ "ticked",
1462
+ "scrolled",
1463
+ "scrolledInView"
1464
+ ]);
1465
+ /** Services whose `subscribe()` callback runs in the read phase. */
1466
+ const READ_PHASE_SERVICES = /* @__PURE__ */ new Set([
1467
+ "useRaf",
1468
+ "useScroll",
1469
+ "useWindowScroll",
1470
+ "useScrollProgress"
1471
+ ]);
1472
+ /** A method carrying either of these already names its own phase. */
1473
+ const PHASE_DECORATORS = /* @__PURE__ */ new Set(["read", "write"]);
1474
+ /** Properties whose assignment mutates the DOM. */
1475
+ const WRITE_PROPERTIES = /* @__PURE__ */ new Set([
1476
+ "textContent",
1477
+ "innerHTML",
1478
+ "className",
1479
+ "scrollTop",
1480
+ "scrollLeft",
1481
+ "value"
1482
+ ]);
1483
+ /** Methods which mutate the DOM. */
1484
+ const WRITE_METHODS = /* @__PURE__ */ new Set([
1485
+ "setProperty",
1486
+ "setAttribute",
1487
+ "removeAttribute",
1488
+ "toggleAttribute",
1489
+ "append",
1490
+ "prepend",
1491
+ "remove",
1492
+ "replaceChildren",
1493
+ "insertAdjacentHTML",
1494
+ "scrollTo",
1495
+ "scrollIntoView",
1496
+ "animate"
1497
+ ]);
1498
+ /** Globals a component may reach the DOM through. */
1499
+ const DOM_GLOBALS = /* @__PURE__ */ new Set(["document", "window"]);
1500
+ /**
1501
+ * Splits a member expression into the expression it starts from and the chain
1502
+ * of property names read off it. A computed property contributes `null`.
1503
+ */
1504
+ function memberPath(node) {
1505
+ const path = [];
1506
+ let current = node;
1507
+ while (current?.type === "MemberExpression") {
1508
+ path.unshift(current.computed ? null : current.property?.name ?? null);
1509
+ current = current.object;
1510
+ }
1511
+ return {
1512
+ base: current ?? null,
1513
+ path
1514
+ };
1515
+ }
1516
+ /**
1517
+ * Returns true if an expression reaches the DOM from one of the roots a v4
1518
+ * component owns: `this.$el`, `this.$refs.*`, `document`, `window`, or a local
1519
+ * initialised from one of those.
1520
+ *
1521
+ * Anchoring the receiver is what makes a type-free rule accurate enough to
1522
+ * turn on: without it, every `foo.value = 1` in the file would be a candidate.
1523
+ */
1524
+ function isAnchored(node, aliases) {
1525
+ if (!node) return false;
1526
+ if (node.type === "CallExpression") return isAnchored(node.callee, aliases);
1527
+ const { base, path } = memberPath(node);
1528
+ if (!base) return false;
1529
+ if (base.type === "ThisExpression") return path[0] === "$el" || path[0] === "$refs";
1530
+ if (base.type === "Identifier") return DOM_GLOBALS.has(base.name) || aliases.has(base.name);
1531
+ return false;
1532
+ }
1533
+ /**
1534
+ * Names the DOM write an assignment performs, or null if it is not one.
1535
+ */
1536
+ function assignmentWrite(node, aliases) {
1537
+ const left = node.left;
1538
+ if (left?.type !== "MemberExpression") return null;
1539
+ if (!isAnchored(left, aliases)) return null;
1540
+ const { path } = memberPath(left);
1541
+ const last = path[path.length - 1];
1542
+ if (path.includes("style")) return "style";
1543
+ if (typeof last === "string" && WRITE_PROPERTIES.has(last)) return last;
1544
+ return null;
1545
+ }
1546
+ /**
1547
+ * Names the DOM write a call performs, or null if it is not one.
1548
+ */
1549
+ function callWrite(node, aliases) {
1550
+ const callee = node.callee;
1551
+ if (callee?.type !== "MemberExpression") return null;
1552
+ if (!isAnchored(callee, aliases)) return null;
1553
+ const { path } = memberPath(callee);
1554
+ const last = path[path.length - 1];
1555
+ if (path.includes("classList")) return "classList";
1556
+ if (typeof last === "string" && WRITE_METHODS.has(last)) return `${last}()`;
1557
+ return null;
1558
+ }
1559
+ /**
1560
+ * Collects, in one pass over the file, the names a member path may be anchored
1561
+ * to and the names holding a `smoothTo()` value.
1562
+ *
1563
+ * A pre-pass rather than incremental collection, so that a field declared after
1564
+ * the method using it is still resolved.
1565
+ */
1566
+ function collectNames(program) {
1567
+ const aliases = /* @__PURE__ */ new Set();
1568
+ const smooth = /* @__PURE__ */ new Set();
1569
+ const candidates = [];
1570
+ walk(program, (node) => {
1571
+ let name = null;
1572
+ let init = null;
1573
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
1574
+ name = node.id.name;
1575
+ init = node.init;
1576
+ } else if (node.type === "PropertyDefinition") {
1577
+ name = getKeyName(node);
1578
+ init = node.value;
1579
+ }
1580
+ if (!name || !init) return;
1581
+ if (init.type === "CallExpression" && init.callee?.type === "Identifier") {
1582
+ if (init.callee.name === "smoothTo") smooth.add(name);
1583
+ }
1584
+ candidates.push({
1585
+ name,
1586
+ init
1587
+ });
1588
+ });
1589
+ let changed = true;
1590
+ while (changed) {
1591
+ changed = false;
1592
+ for (const { name, init } of candidates) {
1593
+ if (aliases.has(name)) continue;
1594
+ if (isAnchored(init, aliases)) {
1595
+ aliases.add(name);
1596
+ changed = true;
1597
+ }
1598
+ }
1599
+ }
1600
+ return {
1601
+ aliases,
1602
+ smooth
1603
+ };
1604
+ }
1605
+ /**
1606
+ * Returns true if `object` is something whose `subscribe()` callback runs in
1607
+ * the read phase.
1608
+ */
1609
+ function isReadPhaseSubscribable(object, smooth) {
1610
+ if (!object) return false;
1611
+ if (object.type === "CallExpression" && object.callee?.type === "Identifier") return READ_PHASE_SERVICES.has(object.callee.name);
1612
+ if (object.type === "MemberExpression" && object.object?.type === "ThisExpression" && !object.computed) return smooth.has(object.property?.name);
1613
+ if (object.type === "Identifier") return smooth.has(object.name);
1614
+ return false;
1615
+ }
1616
+ /** Returns true for `this.$read(fn)` and `defaultScheduler.read(fn)`. */
1617
+ function isReadScheduling(callee) {
1618
+ const name = callee?.property?.name;
1619
+ if (name === "$read" && callee.object?.type === "ThisExpression") return true;
1620
+ return name === "read" && callee.object?.type === "Identifier" && callee.object.name === "defaultScheduler";
1621
+ }
1622
+ const noWriteInReadPhase = createRule({
1623
+ meta: {
1624
+ type: "problem",
1625
+ docs: { description: "Disallow DOM writes in a read-phase callback — return a render function instead" },
1626
+ messages: {
1627
+ writeInReadPhase: "This writes the DOM ({{write}}) in the read phase, which forces layout for every component measuring in the same frame. Return a render function, or move the write into this.$write().",
1628
+ writeInHelper: "This writes the DOM ({{write}}) in the read phase, through {{helper}}(). Return a render function, or mark {{helper}}() with @write."
1629
+ }
1630
+ },
1631
+ createOnce(context) {
1632
+ let aliases = /* @__PURE__ */ new Set();
1633
+ let smooth = /* @__PURE__ */ new Set();
1634
+ /** One frame per function scope. Nesting resets the armed flag by design. */
1635
+ const scopes = [];
1636
+ const classes = [];
1637
+ const reported = /* @__PURE__ */ new Set();
1638
+ const isArmed = () => scopes[scopes.length - 1] === true;
1639
+ /**
1640
+ * Decides whether entering this function enters a read-phase context.
1641
+ */
1642
+ function isReadPhaseEntry(node) {
1643
+ const ancestors = getAncestors(node, context);
1644
+ const parent = ancestors[ancestors.length - 1];
1645
+ if (!parent) return false;
1646
+ if (parent.type === "MethodDefinition" && parent.value === node) {
1647
+ const name = getKeyName(parent);
1648
+ if (!name || !READ_PHASE_HOOKS.has(name)) return false;
1649
+ const enclosing = findEnclosingClass(ancestors);
1650
+ return Boolean(enclosing && isComponentClass(enclosing));
1651
+ }
1652
+ if (parent.type !== "CallExpression") return false;
1653
+ const callee = parent.callee;
1654
+ if (callee?.type !== "MemberExpression") return false;
1655
+ if (isReadScheduling(callee) && parent.arguments?.includes(node)) return true;
1656
+ if (callee.property?.name === "subscribe" && parent.arguments?.[0] === node) return isReadPhaseSubscribable(callee.object, smooth);
1657
+ return false;
1658
+ }
1659
+ /**
1660
+ * Resolves `this.<name>(…)` one level and reports the writes of that method.
1661
+ *
1662
+ * One level only, and never through a method naming its own phase with
1663
+ * `@read` or `@write` — which is how `Cursor`'s `@write render()` stays
1664
+ * correct while still being called from a frame subscriber.
1665
+ */
1666
+ function descend(node) {
1667
+ const callee = node.callee;
1668
+ if (callee?.type !== "MemberExpression") return;
1669
+ if (callee.computed || callee.object?.type !== "ThisExpression") return;
1670
+ const name = callee.property?.name;
1671
+ if (typeof name !== "string" || name.startsWith("$")) return;
1672
+ const enclosing = classes[classes.length - 1];
1673
+ if (!enclosing) return;
1674
+ const method = (enclosing.body?.body ?? []).find((member) => member.type === "MethodDefinition" && member.kind === "method" && getKeyName(member) === name);
1675
+ if (!method || hasDecorator(method, PHASE_DECORATORS)) return;
1676
+ walk(method.value?.body, (inner) => {
1677
+ if (isFunctionNode(inner)) return false;
1678
+ const write = inner.type === "AssignmentExpression" ? assignmentWrite(inner, aliases) : inner.type === "CallExpression" ? callWrite(inner, aliases) : null;
1679
+ if (write && !reported.has(inner)) {
1680
+ reported.add(inner);
1681
+ context.report({
1682
+ node: inner,
1683
+ messageId: "writeInHelper",
1684
+ data: {
1685
+ write,
1686
+ helper: name
1687
+ }
1688
+ });
1689
+ }
1690
+ });
1691
+ }
1692
+ function report(node, write) {
1693
+ if (reported.has(node)) return;
1694
+ reported.add(node);
1695
+ context.report({
1696
+ node,
1697
+ messageId: "writeInReadPhase",
1698
+ data: { write }
1699
+ });
1700
+ }
1701
+ return {
1702
+ Program(node) {
1703
+ const collected = collectNames(node);
1704
+ aliases = collected.aliases;
1705
+ smooth = collected.smooth;
1706
+ scopes.length = 0;
1707
+ classes.length = 0;
1708
+ reported.clear();
1709
+ },
1710
+ ClassDeclaration(node) {
1711
+ classes.push(node);
1712
+ },
1713
+ "ClassDeclaration:exit"() {
1714
+ classes.pop();
1715
+ },
1716
+ ClassExpression(node) {
1717
+ classes.push(node);
1718
+ },
1719
+ "ClassExpression:exit"() {
1720
+ classes.pop();
1721
+ },
1722
+ FunctionDeclaration(node) {
1723
+ scopes.push(isReadPhaseEntry(node));
1724
+ },
1725
+ "FunctionDeclaration:exit"() {
1726
+ scopes.pop();
1727
+ },
1728
+ FunctionExpression(node) {
1729
+ scopes.push(isReadPhaseEntry(node));
1730
+ },
1731
+ "FunctionExpression:exit"() {
1732
+ scopes.pop();
1733
+ },
1734
+ ArrowFunctionExpression(node) {
1735
+ scopes.push(isReadPhaseEntry(node));
1736
+ },
1737
+ "ArrowFunctionExpression:exit"() {
1738
+ scopes.pop();
1739
+ },
1740
+ AssignmentExpression(node) {
1741
+ if (!isArmed()) return;
1742
+ const write = assignmentWrite(node, aliases);
1743
+ if (write) report(node, write);
1744
+ },
1745
+ CallExpression(node) {
1746
+ if (!isArmed()) return;
1747
+ const write = callWrite(node, aliases);
1748
+ if (write) {
1749
+ report(node, write);
1750
+ return;
1751
+ }
1752
+ descend(node);
1753
+ }
1754
+ };
1755
+ }
1756
+ });
1757
+ //#endregion
1758
+ //#region src/rules/no-options-assignment.ts
1759
+ /**
1760
+ * Returns true if an expression is `this.$options`, or a path reaching into it.
1761
+ */
1762
+ function isOptionsPath(node) {
1763
+ let current = node;
1764
+ while (current?.type === "MemberExpression") {
1765
+ if (current.object?.type === "ThisExpression" && !current.computed && current.property?.name === "$options") return true;
1766
+ current = current.object;
1767
+ }
1768
+ return false;
1769
+ }
1770
+ const noOptionsAssignment = createRule({
1771
+ meta: {
1772
+ type: "problem",
1773
+ docs: { description: "Disallow writing to this.$options — it is a read-only view of the attributes" },
1774
+ messages: {
1775
+ assignment: "`$options` is a read-only view of the element attributes: every property is a getter with no setter, so this throws at runtime. Write the attribute instead, with setAttribute().",
1776
+ viewAssignment: "`$options` is defined non-writable, so replacing the view throws at runtime — and everything else in the framework would go on reading the original. Write the attributes instead, with setAttribute().",
1777
+ objectAssign: "Object.assign() onto `$options` writes properties which are getters with no setter, so this throws at runtime. Write the attributes instead, with setAttribute()."
1778
+ }
1779
+ },
1780
+ createOnce(context) {
1781
+ function inComponent(node) {
1782
+ const enclosing = findEnclosingClass(getAncestors(node, context));
1783
+ return Boolean(enclosing && isComponentClass(enclosing));
1784
+ }
1785
+ return {
1786
+ AssignmentExpression(node) {
1787
+ const left = node.left;
1788
+ if (left?.type !== "MemberExpression") return;
1789
+ if (!isOptionsPath(left)) return;
1790
+ if (!inComponent(node)) return;
1791
+ const isWholeView = left.object?.type === "ThisExpression" && left.property?.name === "$options";
1792
+ context.report({
1793
+ node,
1794
+ messageId: isWholeView ? "viewAssignment" : "assignment"
1795
+ });
1796
+ },
1797
+ CallExpression(node) {
1798
+ const callee = node.callee;
1799
+ if (callee?.type !== "MemberExpression") return;
1800
+ if (callee.object?.type !== "Identifier" || callee.object.name !== "Object") return;
1801
+ if (callee.property?.name !== "assign") return;
1802
+ const target = node.arguments?.[0];
1803
+ if (!target || target.type !== "MemberExpression") return;
1804
+ if (!isOptionsPath(target)) return;
1805
+ if (!inComponent(node)) return;
1806
+ context.report({
1807
+ node,
1808
+ messageId: "objectAssign"
1809
+ });
1810
+ }
1811
+ };
1812
+ }
1813
+ });
1814
+ //#endregion
1815
+ //#region src/rules/prefer-instance-scheduler.ts
1816
+ /** The phases a component owns an instance-scoped equivalent of. */
1817
+ const PHASES = /* @__PURE__ */ new Map([["read", "$read"], ["write", "$write"]]);
1818
+ const preferInstanceScheduler = createRule({
1819
+ meta: {
1820
+ type: "suggestion",
1821
+ docs: { description: "Prefer this.$read()/this.$write() over defaultScheduler inside a component" },
1822
+ messages: { preferInstance: "Use `this.{{instance}}()` rather than `defaultScheduler.{{phase}}()` inside a component. The instance form ties the task to this component, so destroying it cancels the task instead of running it against a detached element." }
1823
+ },
1824
+ createOnce(context) {
1825
+ return { CallExpression(node) {
1826
+ const callee = node.callee;
1827
+ if (callee?.type !== "MemberExpression" || callee.computed) return;
1828
+ if (callee.object?.type !== "Identifier" || callee.object.name !== "defaultScheduler") return;
1829
+ const phase = callee.property?.name;
1830
+ const instance = PHASES.get(phase);
1831
+ if (!instance) return;
1832
+ const enclosing = findEnclosingClass(getAncestors(node, context));
1833
+ if (!enclosing || !isComponentClass(enclosing)) return;
1834
+ context.report({
1835
+ node,
1836
+ messageId: "preferInstance",
1837
+ data: {
1838
+ phase,
1839
+ instance
1840
+ }
1841
+ });
1842
+ } };
1843
+ }
1844
+ });
1845
+ //#endregion
1846
+ //#region src/rules/option-default-factory.ts
1847
+ const optionDefaultFactory = createRule({
1848
+ meta: {
1849
+ type: "problem",
1850
+ docs: { description: "Require a factory for an option default which is an object or an array" },
1851
+ messages: { literalDefault: "Option \"{{name}}\" declares a literal {{kind}} default, which every instance of this component then shares — one instance mutating it mutates it for all of them. Declare it as a factory: `default: () => ({{example}})`." }
1852
+ },
1853
+ createOnce(context) {
1854
+ function check(node) {
1855
+ const options = getProperty(findComponentConfig(node), "options");
1856
+ if (options?.type !== "ObjectExpression") return;
1857
+ for (const property of options.properties ?? []) {
1858
+ if (property.type !== "Property") continue;
1859
+ const name = getKeyName(property);
1860
+ if (!name || property.value?.type !== "ObjectExpression") continue;
1861
+ const fallback = getProperty(property.value, "default");
1862
+ if (!fallback) continue;
1863
+ if (fallback.type === "ObjectExpression") context.report({
1864
+ node: fallback,
1865
+ messageId: "literalDefault",
1866
+ data: {
1867
+ name,
1868
+ kind: "object",
1869
+ example: "{}"
1870
+ }
1871
+ });
1872
+ else if (fallback.type === "ArrayExpression") context.report({
1873
+ node: fallback,
1874
+ messageId: "literalDefault",
1875
+ data: {
1876
+ name,
1877
+ kind: "array",
1878
+ example: "[]"
1879
+ }
1880
+ });
1881
+ }
1882
+ }
1883
+ return {
1884
+ ClassDeclaration: check,
1885
+ ClassExpression: check
1886
+ };
1887
+ }
1888
+ });
1889
+ //#endregion
1890
+ //#region src/rules/no-conflicting-negated-option.ts
1891
+ /**
1892
+ * Returns true if an option declaration can hold a boolean, in any of the
1893
+ * spellings v4 accepts: the shorthand `Boolean`, a union `[Boolean, String]`,
1894
+ * or the long form `{ type: Boolean }`.
1895
+ */
1896
+ function isBooleanOption(value) {
1897
+ if (!value) return false;
1898
+ if (value.type === "Identifier") return value.name === "Boolean";
1899
+ if (value.type === "ArrayExpression") return (value.elements ?? []).some((element) => element?.type === "Identifier" && element.name === "Boolean");
1900
+ if (value.type === "ObjectExpression") return isBooleanOption(getProperty(value, "type"));
1901
+ return false;
1902
+ }
1903
+ const noConflictingNegatedOption = createRule({
1904
+ meta: {
1905
+ type: "problem",
1906
+ docs: { description: "Disallow declaring both a boolean option and its negated spelling" },
1907
+ messages: { conflict: "Option \"{{negated}}\" claims `data-option-no-{{attribute}}`, which is also how the boolean option \"{{name}}\" is turned off. One attribute would mean two things. Rename one of them." }
1908
+ },
1909
+ createOnce(context) {
1910
+ function check(node) {
1911
+ const options = getProperty(findComponentConfig(node), "options");
1912
+ if (options?.type !== "ObjectExpression") return;
1913
+ const declared = /* @__PURE__ */ new Map();
1914
+ for (const property of options.properties ?? []) {
1915
+ if (property.type !== "Property") continue;
1916
+ const name = getKeyName(property);
1917
+ if (name) declared.set(name, property);
1918
+ }
1919
+ for (const [name, property] of declared) {
1920
+ if (!isBooleanOption(property.value)) continue;
1921
+ const negated = `no${toPascalCase(name)}`;
1922
+ const conflicting = declared.get(negated);
1923
+ if (!conflicting) continue;
1924
+ context.report({
1925
+ node: conflicting.key ?? conflicting,
1926
+ messageId: "conflict",
1927
+ data: {
1928
+ name,
1929
+ negated,
1930
+ attribute: name.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
1931
+ }
1932
+ });
1933
+ }
1934
+ }
1935
+ return {
1936
+ ClassDeclaration: check,
1937
+ ClassExpression: check
1938
+ };
1939
+ }
1940
+ });
1941
+ //#endregion
1942
+ //#region src/rules/no-destroy-lifecycle.ts
1943
+ /**
1944
+ * v4 renamed `$destroy()` to `$unmount()` and `destroyed()` to `unmounted()`.
1945
+ *
1946
+ * There is no destroyed state in v4: `#isMounted` is one boolean with two
1947
+ * values, and the method documented itself as "the reversible inverse of
1948
+ * `$mount()`". `$mount()`/`$unmount()` and `mounted()`/`unmounted()` are
1949
+ * symmetric pairs where `$mount()`/`$destroy()` was not, which is the same
1950
+ * rename Vue 3 made to `beforeDestroy`/`destroyed`.
1951
+ *
1952
+ * ## Why this is its own rule and not an entry in `no-deprecated-properties`
1953
+ *
1954
+ * Three reasons, and the first is the one that decides it.
1955
+ *
1956
+ * **`no-deprecated-properties` only flags a member expression whose object is
1957
+ * `this`.** That guard is deliberate — `$parent` and `$root` are ordinary
1958
+ * enough words that flagging `someLibrary.$parent` would be noise. But most
1959
+ * `$destroy()` calls are `instance.$destroy()`: from a registry, from a test,
1960
+ * from application code holding an instance. Folding the rename in would
1961
+ * therefore miss the common case, and lifting the guard for one entry would
1962
+ * lift it for every entry, which is exactly the false positive that guard
1963
+ * exists to prevent.
1964
+ *
1965
+ * **`no-deprecated-properties` is not fixable, and should not become so.**
1966
+ * `$parent` → `$closest()` and `$children` → `$watchChildren()` are not safe
1967
+ * textual rewrites; they change the shape of the call and often the logic
1968
+ * around it. A rename is the one case where a fixer is exactly right, and a
1969
+ * rule that is fixable for one of its eleven reports is worse than two rules.
1970
+ *
1971
+ * **The two rules say different things.** `no-deprecated-properties` reports
1972
+ * v3 names that v4 dropped. `$destroy` was never a v3-only name: it is v4's
1973
+ * own, renamed while v4 is unreleased. This rule's whole lifetime is that
1974
+ * rename window, and it is meant to be deleted, not grown.
1975
+ *
1976
+ * ## What is flagged, and what is not
1977
+ *
1978
+ * The two halves are deliberately asymmetric, because the two names are.
1979
+ *
1980
+ * `$destroy` is flagged on **any** receiver — `this.$destroy()`,
1981
+ * `instance.$destroy()`, `super.$destroy()` — and on a `$destroy()` method
1982
+ * definition, which is the shape a service mixin overriding the framework's
1983
+ * teardown writes. The `$` prefix is the toolkit's own namespace, and the rule
1984
+ * ships in `configs.v4` only, which a project turns on when it is on v4. That
1985
+ * opt-in is the guard against rewriting an unrelated library's API.
1986
+ *
1987
+ * `destroyed` has no such prefix — `emitter.destroyed`, `record.destroyed`
1988
+ * and a `destroyed` boolean are all ordinary code — so that half is narrowed
1989
+ * to the two places where it can only mean the hook: a non-static
1990
+ * `destroyed()` method definition in a class that reads as a v4 component,
1991
+ * and a `super.destroyed()` call inside one. A plain `foo.destroyed()` call is
1992
+ * left alone.
1993
+ *
1994
+ * Computed access (`instance['$destroy']()`) is not flagged. It is rare, and a
1995
+ * fixer would have to guess the quote style of a string it did not write.
1996
+ */
1997
+ const noDestroyLifecycle = createRule({
1998
+ meta: {
1999
+ type: "problem",
2000
+ fixable: "code",
2001
+ docs: { description: "Require the v4 names `$unmount()` and `unmounted()` over `$destroy()` and `destroyed()`" },
2002
+ messages: {
2003
+ renamedMethod: "`$destroy()` is named `$unmount()` in v4.",
2004
+ renamedHook: "The `destroyed()` hook is named `unmounted()` in v4."
2005
+ }
2006
+ },
2007
+ createOnce(context) {
2008
+ function inComponent(node) {
2009
+ const enclosing = findEnclosingClass(getAncestors(node, context));
2010
+ return Boolean(enclosing) && isComponentClass(enclosing);
2011
+ }
2012
+ function rename(target, messageId, to) {
2013
+ context.report({
2014
+ node: target,
2015
+ messageId,
2016
+ fix: (fixer) => fixer.replaceText(target, to)
2017
+ });
2018
+ }
2019
+ return {
2020
+ MemberExpression(node) {
2021
+ if (node.computed) return;
2022
+ const name = node.property?.name;
2023
+ if (name === "$destroy") {
2024
+ rename(node.property, "renamedMethod", "$unmount");
2025
+ return;
2026
+ }
2027
+ if (name === "destroyed" && node.object?.type === "Super" && inComponent(node)) rename(node.property, "renamedHook", "unmounted");
2028
+ },
2029
+ MethodDefinition(node) {
2030
+ if (node.computed || !node.key) return;
2031
+ const name = getKeyName(node);
2032
+ if (name === "$destroy") {
2033
+ rename(node.key, "renamedMethod", "$unmount");
2034
+ return;
2035
+ }
2036
+ if (name !== "destroyed" || node.static === true) return;
2037
+ if (!inComponent(node)) return;
2038
+ rename(node.key, "renamedHook", "unmounted");
2039
+ }
2040
+ };
2041
+ }
2042
+ });
2043
+ //#endregion
2044
+ //#region src/index.ts
2045
+ const PLUGIN_NAME = "js-toolkit";
2046
+ const rules = {
2047
+ "require-config": requireConfig,
2048
+ "require-config-name-pascal-case": requireConfigNamePascalCase,
2049
+ "refs-camel-case": refsCamelCase,
2050
+ "refs-plural-multiple": refsPluralMultiple,
2051
+ "options-camel-case": optionsCamelCase,
2052
+ "async-lifecycle-methods": asyncLifecycleMethods,
2053
+ "on-handler-naming": onHandlerNaming,
2054
+ "on-global-handler-prefix": onGlobalHandlerPrefix,
2055
+ "no-deprecated-properties": noDeprecatedProperties,
2056
+ "no-dispatch-event": noDispatchEvent,
2057
+ "no-shadow-dom": noShadowDom,
2058
+ "no-create-app": noCreateApp,
2059
+ "no-event-listener-methods": noEventListenerMethods,
2060
+ "no-deep-utils-import": noDeepUtilsImport,
2061
+ "no-internal-barrel-import": noInternalBarrelImport,
2062
+ "refs-no-bracket-access": refsNoBracketAccess,
2063
+ "no-redundant-with-mount-when-in-view": noRedundantWithMountWhenInView,
2064
+ "prefer-ref-over-query-selector": preferRefOverQuerySelector,
2065
+ "require-refs-declared-in-config": requireRefsDeclaredInConfig,
2066
+ "no-manual-intersection-observer": noManualIntersectionObserver,
2067
+ "no-manual-mutation-observer": noManualMutationObserver,
2068
+ "emits-kebab-case": emitsKebabCase,
2069
+ "emits-multi-word": emitsMultiWord,
2070
+ "components-pascal-case": componentsPascalCase,
2071
+ "require-emit-declared-in-config": requireEmitDeclaredInConfig,
2072
+ "require-children-declared-in-config": requireChildrenDeclaredInConfig,
2073
+ "require-options-declared-in-config": requireOptionsDeclaredInConfig,
2074
+ "prefer-destructured-lookups": preferDestructuredLookups,
2075
+ "no-dollar-prefix": noDollarPrefix,
2076
+ "require-destroyed-cleanup": requireDestroyedCleanup,
2077
+ "no-write-in-read-phase": noWriteInReadPhase,
2078
+ "no-options-assignment": noOptionsAssignment,
2079
+ "prefer-instance-scheduler": preferInstanceScheduler,
2080
+ "option-default-factory": optionDefaultFactory,
2081
+ "no-conflicting-negated-option": noConflictingNegatedOption,
2082
+ "no-destroy-lifecycle": noDestroyLifecycle
1583
2083
  };
1584
- var recommendedRules = {
1585
- [`${PLUGIN_NAME}/require-config`]: "error",
1586
- [`${PLUGIN_NAME}/require-config-name-pascal-case`]: "error",
1587
- [`${PLUGIN_NAME}/refs-camel-case`]: "error",
1588
- [`${PLUGIN_NAME}/refs-plural-multiple`]: "error",
1589
- [`${PLUGIN_NAME}/options-camel-case`]: "error",
1590
- [`${PLUGIN_NAME}/async-lifecycle-methods`]: "error",
1591
- [`${PLUGIN_NAME}/on-handler-naming`]: "error",
1592
- [`${PLUGIN_NAME}/on-global-handler-prefix`]: "warn",
1593
- [`${PLUGIN_NAME}/no-deprecated-properties`]: "warn",
1594
- [`${PLUGIN_NAME}/no-dispatch-event`]: "warn",
1595
- [`${PLUGIN_NAME}/no-shadow-dom`]: "error",
1596
- [`${PLUGIN_NAME}/no-create-app`]: "warn",
1597
- [`${PLUGIN_NAME}/no-event-listener-methods`]: "error",
1598
- [`${PLUGIN_NAME}/no-deep-utils-import`]: "error",
1599
- [`${PLUGIN_NAME}/refs-no-bracket-access`]: "error",
1600
- [`${PLUGIN_NAME}/no-redundant-with-mount-when-in-view`]: "warn",
1601
- [`${PLUGIN_NAME}/prefer-ref-over-query-selector`]: "warn",
1602
- [`${PLUGIN_NAME}/require-refs-declared-in-config`]: "error",
1603
- [`${PLUGIN_NAME}/no-manual-intersection-observer`]: "warn",
1604
- [`${PLUGIN_NAME}/no-manual-mutation-observer`]: "warn",
1605
- [`${PLUGIN_NAME}/emits-kebab-case`]: "error",
1606
- [`${PLUGIN_NAME}/emits-multi-word`]: "error",
1607
- [`${PLUGIN_NAME}/components-pascal-case`]: "error",
1608
- [`${PLUGIN_NAME}/require-emit-declared-in-config`]: "error",
1609
- [`${PLUGIN_NAME}/require-children-declared-in-config`]: "error",
1610
- [`${PLUGIN_NAME}/require-options-declared-in-config`]: "error",
1611
- [`${PLUGIN_NAME}/prefer-destructured-lookups`]: "warn",
1612
- [`${PLUGIN_NAME}/no-dollar-prefix`]: "error",
1613
- [`${PLUGIN_NAME}/require-destroyed-cleanup`]: "warn"
2084
+ const recommendedRules = {
2085
+ [`${PLUGIN_NAME}/require-config`]: "error",
2086
+ [`${PLUGIN_NAME}/require-config-name-pascal-case`]: "error",
2087
+ [`${PLUGIN_NAME}/refs-camel-case`]: "error",
2088
+ [`${PLUGIN_NAME}/refs-plural-multiple`]: "error",
2089
+ [`${PLUGIN_NAME}/options-camel-case`]: "error",
2090
+ [`${PLUGIN_NAME}/async-lifecycle-methods`]: "error",
2091
+ [`${PLUGIN_NAME}/on-handler-naming`]: "error",
2092
+ [`${PLUGIN_NAME}/on-global-handler-prefix`]: "warn",
2093
+ [`${PLUGIN_NAME}/no-deprecated-properties`]: "warn",
2094
+ [`${PLUGIN_NAME}/no-dispatch-event`]: "warn",
2095
+ [`${PLUGIN_NAME}/no-shadow-dom`]: "error",
2096
+ [`${PLUGIN_NAME}/no-create-app`]: "warn",
2097
+ [`${PLUGIN_NAME}/no-event-listener-methods`]: "error",
2098
+ [`${PLUGIN_NAME}/no-deep-utils-import`]: "error",
2099
+ [`${PLUGIN_NAME}/refs-no-bracket-access`]: "error",
2100
+ [`${PLUGIN_NAME}/no-redundant-with-mount-when-in-view`]: "warn",
2101
+ [`${PLUGIN_NAME}/prefer-ref-over-query-selector`]: "warn",
2102
+ [`${PLUGIN_NAME}/require-refs-declared-in-config`]: "error",
2103
+ [`${PLUGIN_NAME}/no-manual-intersection-observer`]: "warn",
2104
+ [`${PLUGIN_NAME}/no-manual-mutation-observer`]: "warn",
2105
+ [`${PLUGIN_NAME}/emits-kebab-case`]: "error",
2106
+ [`${PLUGIN_NAME}/emits-multi-word`]: "error",
2107
+ [`${PLUGIN_NAME}/components-pascal-case`]: "error",
2108
+ [`${PLUGIN_NAME}/require-emit-declared-in-config`]: "error",
2109
+ [`${PLUGIN_NAME}/require-children-declared-in-config`]: "error",
2110
+ [`${PLUGIN_NAME}/require-options-declared-in-config`]: "error",
2111
+ [`${PLUGIN_NAME}/prefer-destructured-lookups`]: "warn",
2112
+ [`${PLUGIN_NAME}/no-dollar-prefix`]: "error",
2113
+ [`${PLUGIN_NAME}/require-destroyed-cleanup`]: "warn"
1614
2114
  };
1615
- var base = eslintCompatPlugin({
1616
- meta: {
1617
- name: "@studiometa/eslint-plugin-js-toolkit"
1618
- },
1619
- rules
2115
+ /**
2116
+ * v4 rules.
2117
+ *
2118
+ * v4 is not v3 with a bigger version number, so this is a separate set rather
2119
+ * than an extension of `recommended`. Rules policing something v4 removed —
2120
+ * `config.emits`, `$children`, the v3 decorator names — are absent, and
2121
+ * `async-lifecycle-methods` is absent because a v4 frame hook returns the
2122
+ * function which writes rather than a promise.
2123
+ */
2124
+ const v4Rules = {
2125
+ [`${PLUGIN_NAME}/no-write-in-read-phase`]: "error",
2126
+ [`${PLUGIN_NAME}/no-options-assignment`]: "error",
2127
+ [`${PLUGIN_NAME}/prefer-instance-scheduler`]: "warn",
2128
+ [`${PLUGIN_NAME}/option-default-factory`]: "error",
2129
+ [`${PLUGIN_NAME}/no-conflicting-negated-option`]: "error",
2130
+ [`${PLUGIN_NAME}/no-destroy-lifecycle`]: "error",
2131
+ [`${PLUGIN_NAME}/no-deprecated-properties`]: ["error", { version: "v4" }],
2132
+ [`${PLUGIN_NAME}/no-dispatch-event`]: "warn",
2133
+ [`${PLUGIN_NAME}/no-shadow-dom`]: "error",
2134
+ [`${PLUGIN_NAME}/no-event-listener-methods`]: "error",
2135
+ [`${PLUGIN_NAME}/no-manual-mutation-observer`]: "warn",
2136
+ [`${PLUGIN_NAME}/refs-no-bracket-access`]: "error",
2137
+ [`${PLUGIN_NAME}/prefer-ref-over-query-selector`]: "warn"
2138
+ };
2139
+ const base = eslintCompatPlugin({
2140
+ meta: { name: "@studiometa/eslint-plugin-js-toolkit" },
2141
+ rules
1620
2142
  });
1621
- var plugin = Object.assign(base, { configs: {} });
2143
+ const plugin = Object.assign(base, { configs: {} });
1622
2144
  plugin.configs["recommended"] = {
1623
- plugins: { [PLUGIN_NAME]: plugin },
1624
- rules: recommendedRules
2145
+ plugins: { [PLUGIN_NAME]: plugin },
2146
+ rules: recommendedRules
1625
2147
  };
1626
- var index_default = plugin;
1627
- export {
1628
- index_default as default,
1629
- plugin as jsToolkit
2148
+ plugin.configs["v4"] = {
2149
+ plugins: { [PLUGIN_NAME]: plugin },
2150
+ rules: v4Rules
1630
2151
  };
1631
- //# sourceMappingURL=index.js.map
2152
+ //#endregion
2153
+ export { plugin as default, plugin as jsToolkit };
2154
+
2155
+ //# sourceMappingURL=index.js.map