@studiometa/eslint-plugin-js-toolkit 0.1.0 → 3.6.0-beta.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.
package/dist/index.js ADDED
@@ -0,0 +1,1064 @@
1
+ // ../../node_modules/@oxlint/plugins/index.js
2
+ var EMPTY_VISITOR = {};
3
+ function eslintCompatPlugin(plugin2) {
4
+ if (typeof plugin2 != "object" || !plugin2) throw Error("Plugin must be an object");
5
+ let { rules: rules2 } = plugin2;
6
+ if (typeof rules2 != "object" || !rules2) throw Error("Plugin must have an object as `rules` property");
7
+ let afterHooksState = new AfterHooksState();
8
+ for (let ruleName in rules2) Object.hasOwn(rules2, ruleName) && convertRule(rules2[ruleName], afterHooksState);
9
+ return plugin2;
10
+ }
11
+ var AfterHooksState = class {
12
+ resetFunctions = [];
13
+ pendingStates = [];
14
+ pendingCount = 0;
15
+ lintFinishedCount = 0;
16
+ resetIsScheduled = false;
17
+ sourceCode = null;
18
+ resetMicrotask = this.resetMicrotaskImpl.bind(this);
19
+ /**
20
+ * Register a function to run `after` hook for a rule, and reset state.
21
+ * @param reset - Function to run `after` hook and reset state
22
+ * @returns Index of rule
23
+ */
24
+ registerResetFunction(reset) {
25
+ let { pendingStates } = this, index = pendingStates.length;
26
+ return pendingStates.push(0), this.resetFunctions.push(reset), index;
27
+ }
28
+ /**
29
+ * Register that a rule with `after` hook has completed linting a file.
30
+ * Called by `onCodePathEnd` CFG event handler which is added to visitor for rules with `after` hooks.
31
+ *
32
+ * If all rules with an `after` hook which needs to be run have completed linting the file, run all `after` hooks.
33
+ */
34
+ ruleFinished() {
35
+ this.lintFinishedCount++, this.lintFinishedCount === this.pendingCount && this.reset(false);
36
+ }
37
+ /**
38
+ * Call all reset functions where corresponding entry in `pendingStates` is `AFTER_HOOK_PENDING`.
39
+ * Should only be called when some `after` hooks are pending.
40
+ *
41
+ * @param ignoreErrors - `true` to catch and silently ignore any errors which occur in `after` hooks.
42
+ * `false` to throw them,
43
+ * @throws {unknown} If `ignoreErrors` is `false` and an error occurs in any `after` hooks.
44
+ */
45
+ reset(ignoreErrors) {
46
+ this.pendingCount;
47
+ let { resetFunctions, pendingStates } = this, hooksLen = pendingStates.length, hasError = false, error;
48
+ for (let i = 0; i < hooksLen; i++) if (pendingStates[i] !== 0) {
49
+ pendingStates[i] = 0;
50
+ try {
51
+ resetFunctions[i]();
52
+ } catch (e) {
53
+ hasError === false && (hasError = true, error = e);
54
+ }
55
+ }
56
+ if (this.pendingCount = 0, this.lintFinishedCount = 0, this.sourceCode = null, hasError === true && ignoreErrors === false) throw error;
57
+ }
58
+ /**
59
+ * Schedule a microtask to run `reset` functions.
60
+ */
61
+ scheduleReset() {
62
+ queueMicrotask(this.resetMicrotask), this.resetIsScheduled = true;
63
+ }
64
+ /**
65
+ * Function which is scheduled as the cleanup microtask.
66
+ * `scheduleReset` uses `resetMicrotask` which is this method bound to `this`.
67
+ */
68
+ resetMicrotaskImpl() {
69
+ this.resetIsScheduled = false, this.pendingCount !== 0 && this.reset(true);
70
+ }
71
+ };
72
+ function convertRule(rule, afterHooksState) {
73
+ if (typeof rule != "object" || !rule) throw Error("Rule must be an object");
74
+ if ("create" in rule) return;
75
+ let context = null, visitor, beforeHook, setupAfterHook;
76
+ rule.create = (eslintContext) => {
77
+ context === null && ({ context, visitor, beforeHook, setupAfterHook } = createContextAndVisitor(rule, afterHooksState));
78
+ let eslintFileContext = Object.getPrototypeOf(eslintContext);
79
+ if (setupAfterHook !== null) {
80
+ let { sourceCode } = eslintFileContext;
81
+ afterHooksState.sourceCode !== sourceCode && (afterHooksState.sourceCode = sourceCode, afterHooksState.pendingCount !== 0 && afterHooksState.reset(true));
82
+ }
83
+ return Object.defineProperties(context, {
84
+ id: { value: eslintContext.id },
85
+ options: { value: eslintContext.options },
86
+ report: { value: eslintContext.report }
87
+ }), Object.setPrototypeOf(context, eslintFileContext), beforeHook !== null && beforeHook() === false ? EMPTY_VISITOR : (setupAfterHook !== null && (setupAfterHook(eslintFileContext.sourceCode.ast), afterHooksState.resetIsScheduled === false && afterHooksState.scheduleReset()), visitor);
88
+ };
89
+ }
90
+ var FILE_CONTEXT = Object.freeze({
91
+ get filename() {
92
+ throw Error("Cannot access `context.filename` in `createOnce`");
93
+ },
94
+ getFilename() {
95
+ throw Error("Cannot call `context.getFilename` in `createOnce`");
96
+ },
97
+ get physicalFilename() {
98
+ throw Error("Cannot access `context.physicalFilename` in `createOnce`");
99
+ },
100
+ getPhysicalFilename() {
101
+ throw Error("Cannot call `context.getPhysicalFilename` in `createOnce`");
102
+ },
103
+ get cwd() {
104
+ throw Error("Cannot access `context.cwd` in `createOnce`");
105
+ },
106
+ getCwd() {
107
+ throw Error("Cannot call `context.getCwd` in `createOnce`");
108
+ },
109
+ get sourceCode() {
110
+ throw Error("Cannot access `context.sourceCode` in `createOnce`");
111
+ },
112
+ getSourceCode() {
113
+ throw Error("Cannot call `context.getSourceCode` in `createOnce`");
114
+ },
115
+ get languageOptions() {
116
+ throw Error("Cannot access `context.languageOptions` in `createOnce`");
117
+ },
118
+ get settings() {
119
+ throw Error("Cannot access `context.settings` in `createOnce`");
120
+ },
121
+ extend(extension) {
122
+ return Object.freeze(Object.assign(Object.create(this), extension));
123
+ },
124
+ get parserOptions() {
125
+ throw Error("Cannot access `context.parserOptions` in `createOnce`");
126
+ },
127
+ get parserPath() {
128
+ throw Error("Cannot access `context.parserPath` in `createOnce`");
129
+ }
130
+ });
131
+ function createContextAndVisitor(rule, afterHooksState) {
132
+ let { createOnce } = rule;
133
+ if (createOnce == null) throw Error("Rules must define either a `create` or `createOnce` method");
134
+ if (typeof createOnce != "function") throw Error("Rule `createOnce` property must be a function");
135
+ let context = Object.create(FILE_CONTEXT, {
136
+ id: {
137
+ value: null,
138
+ enumerable: true,
139
+ configurable: true
140
+ },
141
+ options: {
142
+ value: null,
143
+ enumerable: true,
144
+ configurable: true
145
+ },
146
+ report: {
147
+ value() {
148
+ throw Error("Cannot report errors in `createOnce`");
149
+ },
150
+ enumerable: true,
151
+ configurable: true
152
+ }
153
+ }), { before: beforeHook, after: afterHook, ...visitor } = createOnce.call(rule, context);
154
+ if (beforeHook === void 0) beforeHook = null;
155
+ else if (beforeHook !== null && typeof beforeHook != "function") throw Error("`before` property of visitor must be a function if defined");
156
+ let setupAfterHook = null;
157
+ if (afterHook != null) {
158
+ if (typeof afterHook != "function") throw Error("`after` property of visitor must be a function if defined");
159
+ let program = null, ruleIndex = afterHooksState.registerResetFunction(() => {
160
+ program = null, afterHook();
161
+ });
162
+ setupAfterHook = (ast) => {
163
+ program = ast, afterHooksState.pendingStates[ruleIndex] = 1, afterHooksState.pendingCount++;
164
+ };
165
+ let onCodePathEnd = visitor.onCodePathEnd;
166
+ visitor.onCodePathEnd = onCodePathEnd == null ? function(_codePath, node) {
167
+ node === program && afterHooksState.ruleFinished();
168
+ } : function(codePath, node) {
169
+ onCodePathEnd.call(this, codePath, node), node === program && afterHooksState.ruleFinished();
170
+ };
171
+ }
172
+ return {
173
+ context,
174
+ visitor,
175
+ beforeHook,
176
+ setupAfterHook
177
+ };
178
+ }
179
+
180
+ // src/utils/ast.ts
181
+ function createRule(rule) {
182
+ return rule;
183
+ }
184
+ var 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"
195
+ ]);
196
+ var TOOLKIT_PACKAGE = "@studiometa/js-toolkit";
197
+ 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;
226
+ }
227
+ function isCamelCase(value) {
228
+ return /^[a-z][a-zA-Z0-9]*$/.test(value);
229
+ }
230
+ function isPascalCase(value) {
231
+ return /^[A-Z][a-zA-Z0-9]*$/.test(value);
232
+ }
233
+ function toCamelCase(value) {
234
+ return value.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase());
235
+ }
236
+ function toPascalCase(value) {
237
+ return value.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase());
238
+ }
239
+ 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;
247
+ }
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
+ }
271
+ });
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
+ }
290
+ }
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
+ }
314
+ });
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
+ }
336
+ }
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
+ }
360
+ });
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
+ }
387
+ }
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
+ }
411
+ });
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
+ }
438
+ }
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
+ }
470
+ });
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
+ }
498
+ });
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
+ }
532
+ });
533
+
534
+ // src/rules/no-deprecated-properties.ts
535
+ var DEPRECATED = /* @__PURE__ */ new Map([
536
+ ["$parent", "$closest()"],
537
+ ["$root", "$closest()"],
538
+ ["$children", "$query()"]
539
+ ]);
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
+ }
568
+ });
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
+ }
598
+ });
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
+ }
625
+ });
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
+ }
648
+ });
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
+ }
681
+ });
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
+ }
704
+ });
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
+ }
730
+ }
731
+
732
+ // src/rules/no-deep-utils-import.ts
733
+ var DEEP_UTILS_RE = /^@studiometa\/js-toolkit\/utils\/.+/;
734
+ var noDeepUtilsImport = createRule({
735
+ meta: {
736
+ type: "problem",
737
+ fixable: "code",
738
+ docs: {
739
+ description: "Disallow deep imports from @studiometa/js-toolkit/utils/*"
740
+ },
741
+ messages: {
742
+ noDeepImport: 'Import from "{{source}}" should use the public entrypoint "@studiometa/js-toolkit/utils".'
743
+ }
744
+ },
745
+ createOnce(context) {
746
+ return {
747
+ ImportDeclaration(node) {
748
+ const source = node.source.value;
749
+ if (!DEEP_UTILS_RE.test(source)) return;
750
+ context.report({
751
+ node,
752
+ messageId: "noDeepImport",
753
+ data: { source },
754
+ fix(fixer) {
755
+ const specifiers = node.specifiers ?? [];
756
+ const parts = [];
757
+ for (const s of specifiers) {
758
+ if (s.type === "ImportDefaultSpecifier") {
759
+ parts.push(s.local.name);
760
+ } else if (s.type === "ImportSpecifier") {
761
+ const imported = s.imported.name;
762
+ const local = s.local.name;
763
+ parts.push(imported === local ? imported : `${imported} as ${local}`);
764
+ }
765
+ }
766
+ const named = parts.join(", ");
767
+ return fixer.replaceText(
768
+ node,
769
+ `import { ${named} } from '@studiometa/js-toolkit/utils';`
770
+ );
771
+ }
772
+ });
773
+ }
774
+ };
775
+ }
776
+ });
777
+
778
+ // src/rules/refs-no-bracket-access.ts
779
+ function isThisRefs(node) {
780
+ return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$refs";
781
+ }
782
+ var refsNoBracketAccess = createRule({
783
+ meta: {
784
+ type: "problem",
785
+ fixable: "code",
786
+ docs: {
787
+ description: "Disallow bracket access with [] suffix on this.$refs"
788
+ },
789
+ messages: {
790
+ noBracketAccess: `Use "this.$refs.{{fixed}}" instead of "this.$refs['{{raw}}']".`
791
+ }
792
+ },
793
+ createOnce(context) {
794
+ return {
795
+ MemberExpression(node) {
796
+ if (!isThisRefs(node.object)) return;
797
+ if (!node.computed) return;
798
+ const prop = node.property;
799
+ if (prop.type !== "Literal" || typeof prop.value !== "string") return;
800
+ const raw = prop.value;
801
+ if (!raw.endsWith("[]")) return;
802
+ const stripped = raw.slice(0, -2);
803
+ const fixed = toCamelCase(stripped);
804
+ context.report({
805
+ node,
806
+ messageId: "noBracketAccess",
807
+ data: { raw, fixed },
808
+ fix(fixer) {
809
+ const src = context.sourceCode ?? context.getSourceCode?.();
810
+ const objText = src.getText(node.object);
811
+ return fixer.replaceText(node, `${objText}.${fixed}`);
812
+ }
813
+ });
814
+ }
815
+ };
816
+ }
817
+ });
818
+
819
+ // src/rules/no-redundant-with-mount-when-in-view.ts
820
+ var noRedundantWithMountWhenInView = createRule({
821
+ meta: {
822
+ type: "suggestion",
823
+ docs: {
824
+ description: "Disallow wrapping withMountWhenInView inside withScrolledInView (redundant)"
825
+ },
826
+ messages: {
827
+ redundant: "withScrolledInView already includes withMountWhenInView internally. Remove the inner withMountWhenInView call."
828
+ }
829
+ },
830
+ createOnce(context) {
831
+ return {
832
+ "ClassDeclaration, ClassExpression"(node) {
833
+ check6(node, context);
834
+ }
835
+ };
836
+ }
837
+ });
838
+ function check6(node, context) {
839
+ const superClass = node.superClass;
840
+ if (!superClass || superClass.type !== "CallExpression") return;
841
+ if (superClass.callee?.name !== "withScrolledInView") return;
842
+ const arg = superClass.arguments?.[0];
843
+ if (!arg || arg.type !== "CallExpression") return;
844
+ if (arg.callee?.name !== "withMountWhenInView") return;
845
+ context.report({ node: superClass, messageId: "redundant" });
846
+ }
847
+
848
+ // src/rules/prefer-ref-over-query-selector.ts
849
+ var QUERY_METHODS = /* @__PURE__ */ new Set(["querySelector", "querySelectorAll"]);
850
+ function isThisEl(node) {
851
+ return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$el";
852
+ }
853
+ var preferRefOverQuerySelector = createRule({
854
+ meta: {
855
+ type: "suggestion",
856
+ docs: {
857
+ description: "Prefer this.$refs over this.$el.querySelector() in Base subclasses"
858
+ },
859
+ messages: {
860
+ preferRef: 'Avoid "this.$el.{{method}}()". Declare a ref in static config and use "this.$refs" instead.'
861
+ }
862
+ },
863
+ createOnce(context) {
864
+ return {
865
+ CallExpression(node) {
866
+ const callee = node.callee;
867
+ if (callee?.type !== "MemberExpression") return;
868
+ if (!isThisEl(callee.object)) return;
869
+ const method = callee.property?.name;
870
+ if (!QUERY_METHODS.has(method)) return;
871
+ const ancestors = context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
872
+ const cls = findEnclosingClass(ancestors);
873
+ if (!cls || !isBaseSubclass(cls, context)) return;
874
+ context.report({ node, messageId: "preferRef", data: { method } });
875
+ }
876
+ };
877
+ }
878
+ });
879
+
880
+ // src/rules/require-refs-declared-in-config.ts
881
+ function collectDeclaredRefs(classNode) {
882
+ const body = classNode.body?.body ?? [];
883
+ const configProp = body.find(
884
+ (m) => m.type === "PropertyDefinition" && m.static === true && m.key?.name === "config"
885
+ );
886
+ if (!configProp?.value || configProp.value.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
887
+ const refsProp = configProp.value.properties?.find(
888
+ (p) => p.type === "Property" && p.key?.name === "refs"
889
+ );
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;
899
+ }
900
+ function isThisRefs2(node) {
901
+ return node.type === "MemberExpression" && node.object?.type === "ThisExpression" && node.property?.name === "$refs";
902
+ }
903
+ var requireRefsDeclaredInConfig = createRule({
904
+ meta: {
905
+ type: "problem",
906
+ docs: {
907
+ description: "Require all this.$refs accesses to be declared in static config.refs"
908
+ },
909
+ messages: {
910
+ undeclared: 'Ref "{{name}}" is not declared in static config.refs.'
911
+ }
912
+ },
913
+ createOnce(context) {
914
+ return {
915
+ "ClassDeclaration, ClassExpression"(classNode) {
916
+ if (!isBaseSubclass(classNode, context)) return;
917
+ const declared = collectDeclaredRefs(classNode);
918
+ walkNode(classNode.body, (node) => {
919
+ if (node.type !== "MemberExpression") return;
920
+ if (!isThisRefs2(node.object)) return;
921
+ let refName = null;
922
+ if (!node.computed && node.property?.type === "Identifier") {
923
+ refName = node.property.name;
924
+ } else if (node.computed && node.property?.type === "Literal" && typeof node.property.value === "string") {
925
+ const raw = node.property.value;
926
+ const stripped = raw.endsWith("[]") ? raw.slice(0, -2) : raw;
927
+ refName = toCamelCase(stripped);
928
+ }
929
+ if (refName && !declared.has(refName)) {
930
+ context.report({
931
+ node,
932
+ messageId: "undeclared",
933
+ data: { name: refName }
934
+ });
935
+ }
936
+ });
937
+ }
938
+ };
939
+ }
940
+ });
941
+ function walkNode(node, visit) {
942
+ if (!node || typeof node !== "object") return;
943
+ visit(node);
944
+ for (const key of Object.keys(node)) {
945
+ if (key === "parent") continue;
946
+ const child = node[key];
947
+ if (Array.isArray(child)) {
948
+ for (const item of child) walkNode(item, visit);
949
+ } else if (child && typeof child === "object" && child.type) {
950
+ walkNode(child, visit);
951
+ }
952
+ }
953
+ }
954
+
955
+ // src/rules/no-manual-intersection-observer.ts
956
+ var noManualIntersectionObserver = createRule({
957
+ meta: {
958
+ type: "suggestion",
959
+ docs: {
960
+ description: "Disallow manual IntersectionObserver inside Base subclasses; use withIntersectionObserver or withMountWhenInView instead"
961
+ },
962
+ messages: {
963
+ noManual: 'Avoid manual "new IntersectionObserver()". Use "withIntersectionObserver" or "withMountWhenInView" decorators instead.'
964
+ }
965
+ },
966
+ createOnce(context) {
967
+ return {
968
+ NewExpression(node) {
969
+ if (node.callee?.name !== "IntersectionObserver") return;
970
+ const ancestors = context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
971
+ const cls = findEnclosingClass(ancestors);
972
+ if (!cls || !isBaseSubclass(cls, context)) return;
973
+ context.report({ node, messageId: "noManual" });
974
+ }
975
+ };
976
+ }
977
+ });
978
+
979
+ // src/rules/no-manual-mutation-observer.ts
980
+ var noManualMutationObserver = createRule({
981
+ meta: {
982
+ type: "suggestion",
983
+ docs: {
984
+ description: "Disallow manual MutationObserver inside Base subclasses; use withMutation decorator instead"
985
+ },
986
+ messages: {
987
+ noManual: 'Avoid manual "new MutationObserver()". Use the "withMutation" decorator instead.'
988
+ }
989
+ },
990
+ createOnce(context) {
991
+ return {
992
+ NewExpression(node) {
993
+ if (node.callee?.name !== "MutationObserver") return;
994
+ const ancestors = context.getAncestors?.() ?? context.sourceCode?.getAncestors?.(node) ?? [];
995
+ const cls = findEnclosingClass(ancestors);
996
+ if (!cls || !isBaseSubclass(cls, context)) return;
997
+ context.report({ node, messageId: "noManual" });
998
+ }
999
+ };
1000
+ }
1001
+ });
1002
+
1003
+ // src/index.ts
1004
+ var PLUGIN_NAME = "js-toolkit";
1005
+ var rules = {
1006
+ "require-config": requireConfig,
1007
+ "require-config-name-pascal-case": requireConfigNamePascalCase,
1008
+ "refs-camel-case": refsCamelCase,
1009
+ "refs-plural-multiple": refsPluralMultiple,
1010
+ "options-camel-case": optionsCamelCase,
1011
+ "async-lifecycle-methods": asyncLifecycleMethods,
1012
+ "on-handler-naming": onHandlerNaming,
1013
+ "on-global-handler-prefix": onGlobalHandlerPrefix,
1014
+ "no-deprecated-properties": noDeprecatedProperties,
1015
+ "no-dispatch-event": noDispatchEvent,
1016
+ "no-shadow-dom": noShadowDom,
1017
+ "no-create-app": noCreateApp,
1018
+ "no-event-listener-methods": noEventListenerMethods,
1019
+ "no-deep-utils-import": noDeepUtilsImport,
1020
+ "refs-no-bracket-access": refsNoBracketAccess,
1021
+ "no-redundant-with-mount-when-in-view": noRedundantWithMountWhenInView,
1022
+ "prefer-ref-over-query-selector": preferRefOverQuerySelector,
1023
+ "require-refs-declared-in-config": requireRefsDeclaredInConfig,
1024
+ "no-manual-intersection-observer": noManualIntersectionObserver,
1025
+ "no-manual-mutation-observer": noManualMutationObserver
1026
+ };
1027
+ var recommendedRules = {
1028
+ [`${PLUGIN_NAME}/require-config`]: "error",
1029
+ [`${PLUGIN_NAME}/require-config-name-pascal-case`]: "error",
1030
+ [`${PLUGIN_NAME}/refs-camel-case`]: "error",
1031
+ [`${PLUGIN_NAME}/refs-plural-multiple`]: "error",
1032
+ [`${PLUGIN_NAME}/options-camel-case`]: "error",
1033
+ [`${PLUGIN_NAME}/async-lifecycle-methods`]: "error",
1034
+ [`${PLUGIN_NAME}/on-handler-naming`]: "error",
1035
+ [`${PLUGIN_NAME}/on-global-handler-prefix`]: "warn",
1036
+ [`${PLUGIN_NAME}/no-deprecated-properties`]: "warn",
1037
+ [`${PLUGIN_NAME}/no-dispatch-event`]: "warn",
1038
+ [`${PLUGIN_NAME}/no-shadow-dom`]: "error",
1039
+ [`${PLUGIN_NAME}/no-create-app`]: "warn",
1040
+ [`${PLUGIN_NAME}/no-event-listener-methods`]: "error",
1041
+ [`${PLUGIN_NAME}/no-deep-utils-import`]: "error",
1042
+ [`${PLUGIN_NAME}/refs-no-bracket-access`]: "error",
1043
+ [`${PLUGIN_NAME}/no-redundant-with-mount-when-in-view`]: "warn",
1044
+ [`${PLUGIN_NAME}/prefer-ref-over-query-selector`]: "warn",
1045
+ [`${PLUGIN_NAME}/require-refs-declared-in-config`]: "error",
1046
+ [`${PLUGIN_NAME}/no-manual-intersection-observer`]: "warn",
1047
+ [`${PLUGIN_NAME}/no-manual-mutation-observer`]: "warn"
1048
+ };
1049
+ var base = eslintCompatPlugin({
1050
+ meta: {
1051
+ name: "@studiometa/eslint-plugin-js-toolkit"
1052
+ },
1053
+ rules
1054
+ });
1055
+ var plugin = Object.assign(base, { configs: {} });
1056
+ plugin.configs["recommended"] = {
1057
+ plugins: { [PLUGIN_NAME]: plugin },
1058
+ rules: recommendedRules
1059
+ };
1060
+ var index_default = plugin;
1061
+ export {
1062
+ index_default as default
1063
+ };
1064
+ //# sourceMappingURL=index.js.map