@coryrylan/tools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/dist/eslint/configs/browser.d.ts +12 -0
  4. package/dist/eslint/configs/browser.js +40 -0
  5. package/dist/eslint/configs/html.d.ts +10 -0
  6. package/dist/eslint/configs/html.js +38 -0
  7. package/dist/eslint/configs/json.d.ts +13 -0
  8. package/dist/eslint/configs/json.js +39 -0
  9. package/dist/eslint/configs/shared.d.ts +11 -0
  10. package/dist/eslint/configs/shared.js +19 -0
  11. package/dist/eslint/configs/tests.d.ts +8 -0
  12. package/dist/eslint/configs/tests.js +20 -0
  13. package/dist/eslint/configs/typescript.d.ts +18 -0
  14. package/dist/eslint/configs/typescript.js +104 -0
  15. package/dist/eslint/index.d.ts +27 -0
  16. package/dist/eslint/index.js +19 -0
  17. package/dist/eslint/plugin.d.ts +8 -0
  18. package/dist/eslint/plugin.js +36 -0
  19. package/dist/eslint/rules/consistent-error-messages.d.ts +10 -0
  20. package/dist/eslint/rules/consistent-error-messages.js +127 -0
  21. package/dist/eslint/rules/no-dead-code.d.ts +16 -0
  22. package/dist/eslint/rules/no-dead-code.js +110 -0
  23. package/dist/eslint/rules/no-deep-class-inheritance.d.ts +19 -0
  24. package/dist/eslint/rules/no-deep-class-inheritance.js +132 -0
  25. package/dist/eslint/rules/no-reexport-barrels.d.ts +10 -0
  26. package/dist/eslint/rules/no-reexport-barrels.js +55 -0
  27. package/dist/eslint/rules/no-single-consumer-abstraction.d.ts +3 -0
  28. package/dist/eslint/rules/no-single-consumer-abstraction.js +364 -0
  29. package/dist/eslint/rules/no-unjustified-disable.d.ts +11 -0
  30. package/dist/eslint/rules/no-unjustified-disable.js +75 -0
  31. package/dist/eslint/rules/no-unpinned-dependency-ranges.d.ts +46 -0
  32. package/dist/eslint/rules/no-unpinned-dependency-ranges.js +79 -0
  33. package/dist/eslint/rules/require-listener-cleanup.d.ts +8 -0
  34. package/dist/eslint/rules/require-listener-cleanup.js +202 -0
  35. package/dist/eslint/rules/require-observer-cleanup.d.ts +8 -0
  36. package/dist/eslint/rules/require-observer-cleanup.js +69 -0
  37. package/dist/eslint/rules/require-timer-cleanup.d.ts +10 -0
  38. package/dist/eslint/rules/require-timer-cleanup.js +142 -0
  39. package/dist/eslint/rules/utils.d.ts +45 -0
  40. package/dist/eslint/rules/utils.js +109 -0
  41. package/dist/prettier/index.d.ts +15 -0
  42. package/dist/prettier/index.js +26 -0
  43. package/dist/stylelint/index.d.ts +10 -0
  44. package/dist/stylelint/index.js +50 -0
  45. package/dist/vale/styles/config/vocabularies/Tools/accept.txt +80 -0
  46. package/dist/vale/styles/config/vocabularies/Tools/reject.txt +5 -0
  47. package/dist/vale/vale.ini +27 -0
  48. package/dist/vite/index.d.ts +37 -0
  49. package/dist/vite/index.js +61 -0
  50. package/dist/vite/plugins/dts.d.ts +26 -0
  51. package/dist/vite/plugins/dts.js +106 -0
  52. package/dist/vite/plugins/write-if-changed.d.ts +20 -0
  53. package/dist/vite/plugins/write-if-changed.js +33 -0
  54. package/dist/vitest/browser.d.ts +31 -0
  55. package/dist/vitest/browser.js +105 -0
  56. package/dist/vitest/index.d.ts +28 -0
  57. package/dist/vitest/index.js +34 -0
  58. package/package.json +200 -0
@@ -0,0 +1,202 @@
1
+ import { normalize, walk } from "./utils.js";
2
+ //#region src/eslint/rules/require-listener-cleanup.ts
3
+ var DEFAULT_LIFECYCLE_PAIRS = [["connectedCallback", "disconnectedCallback"]];
4
+ var rule = {
5
+ meta: {
6
+ type: "problem",
7
+ docs: {
8
+ description: "Ensures addEventListener calls in a setup lifecycle method have a matching removeEventListener in the paired teardown method, and forbids addEventListener in the constructor.",
9
+ recommended: true
10
+ },
11
+ schema: [{
12
+ type: "object",
13
+ properties: { lifecyclePairs: {
14
+ type: "array",
15
+ items: {
16
+ type: "array",
17
+ items: [{ type: "string" }, { type: "string" }],
18
+ minItems: 2,
19
+ maxItems: 2
20
+ }
21
+ } },
22
+ additionalProperties: false
23
+ }],
24
+ messages: {
25
+ "missing-cleanup": "`addEventListener` on `{{target}}` for `{{event}}` is called in {{setup}}() but no matching `removeEventListener` is called in {{teardown}}(). Attach the listener via a class field and remove the same reference in {{teardown}}() to prevent a listener leak when {{setup}} runs again.",
26
+ "missing-teardown": "{{setup}}() attaches event listeners but the class has no {{teardown}}(). Add a {{teardown}}() that removes each listener with its original reference to prevent a leak when {{setup}} runs again.",
27
+ "listener-in-constructor": "`addEventListener` on `{{target}}` for `{{event}}` is called in the constructor. The constructor runs once per instance, so this listener cannot be paired with a teardown call and leaks if the instance is torn down and recreated. Move the call into a setup lifecycle method and pair it with a matching removeEventListener in the corresponding teardown method."
28
+ }
29
+ },
30
+ create(context) {
31
+ const pairs = getLifecyclePairs(context);
32
+ const handleClass = (node) => {
33
+ checkClass({
34
+ context,
35
+ classNode: node
36
+ }, pairs);
37
+ };
38
+ return {
39
+ ClassDeclaration: handleClass,
40
+ ClassExpression: handleClass
41
+ };
42
+ }
43
+ };
44
+ function checkClass(env, pairs) {
45
+ checkConstructor(env);
46
+ for (const pair of pairs) checkPair(env, pair);
47
+ }
48
+ function checkConstructor(env) {
49
+ const ctor = findConstructor(env.classNode);
50
+ if (!ctor) return;
51
+ for (const add of collectListenerCalls(env, ctor, "addEventListener")) env.context.report({
52
+ node: add.node,
53
+ messageId: "listener-in-constructor",
54
+ data: {
55
+ target: add.target,
56
+ event: add.event
57
+ }
58
+ });
59
+ }
60
+ function checkPair(env, pair) {
61
+ const [setupName, teardownName] = pair;
62
+ const setup = findMethod(env.classNode, setupName);
63
+ if (!setup) return;
64
+ const addCalls = collectListenerCalls(env, setup, "addEventListener").filter((add) => !isSelfCleaningListenerCall(add.node));
65
+ if (addCalls.length === 0) return;
66
+ const teardown = findMethod(env.classNode, teardownName);
67
+ if (!teardown) {
68
+ env.context.report({
69
+ node: setup,
70
+ messageId: "missing-teardown",
71
+ data: {
72
+ setup: setupName,
73
+ teardown: teardownName
74
+ }
75
+ });
76
+ return;
77
+ }
78
+ reportUnmatched(env, {
79
+ addCalls,
80
+ removeCalls: collectListenerCalls(env, teardown, "removeEventListener"),
81
+ setupName,
82
+ teardownName
83
+ });
84
+ }
85
+ /**
86
+ * An `addEventListener` call whose options argument statically guarantees the
87
+ * listener detaches itself needs no paired `removeEventListener`:
88
+ * `{ once: true }` self-removes after the first invocation, and `{ signal }`
89
+ * delegates teardown to whatever aborts that `AbortSignal` - which commonly
90
+ * lives outside the setup/teardown pair this rule pairs up, e.g. a shared
91
+ * `AbortController` aborted from a different lifecycle hook entirely.
92
+ * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options
93
+ *
94
+ * Only a statically-analyzable options object exempts the call: an options
95
+ * value that isn't an object literal (a boolean, an identifier, a spread-only
96
+ * object) can't be proven self-cleaning here, so it keeps requiring a paired
97
+ * `removeEventListener` rather than risk a false negative.
98
+ */
99
+ function isSelfCleaningListenerCall(node) {
100
+ const optionsArg = node.arguments[2];
101
+ if (!optionsArg || optionsArg.type !== "ObjectExpression") return false;
102
+ return optionsArg.properties.some((property) => isAbortSignalProperty(property) || isOnceTrueProperty(property));
103
+ }
104
+ function isAbortSignalProperty(property) {
105
+ return getStaticPropertyName(property) === "signal";
106
+ }
107
+ function isOnceTrueProperty(property) {
108
+ if (property.type !== "Property" || getStaticPropertyName(property) !== "once") return false;
109
+ return property.value.type === "Literal" && property.value.value === true;
110
+ }
111
+ function getStaticPropertyName(property) {
112
+ if (property.type !== "Property" || property.computed) return null;
113
+ if (property.key.type === "Identifier") return property.key.name;
114
+ return property.key.type === "Literal" && typeof property.key.value === "string" ? property.key.value : null;
115
+ }
116
+ function reportUnmatched(env, check) {
117
+ for (const add of check.addCalls) if (!check.removeCalls.some((remove) => remove.target === add.target && remove.event === add.event)) env.context.report({
118
+ node: add.node,
119
+ messageId: "missing-cleanup",
120
+ data: {
121
+ target: add.target,
122
+ event: add.event,
123
+ setup: check.setupName,
124
+ teardown: check.teardownName
125
+ }
126
+ });
127
+ }
128
+ function findConstructor(classNode) {
129
+ for (const member of classNode.body.body) if (member.type === "MethodDefinition" && member.kind === "constructor") return member;
130
+ return null;
131
+ }
132
+ function findMethod(classNode, name) {
133
+ for (const member of classNode.body.body) if (isNamedMethod(member, name)) return member;
134
+ return null;
135
+ }
136
+ function isNamedMethod(member, name) {
137
+ return member.type === "MethodDefinition" && !member.static && member.key.type === "Identifier" && member.key.name === name;
138
+ }
139
+ /**
140
+ * Collects `methodName` calls in `methodNode`'s own body, plus (depth-1 only)
141
+ * calls made from any private helper method that `methodNode` calls directly
142
+ * - the common pattern of a lifecycle method delegating to `#setup()`/`#teardown()`.
143
+ */
144
+ function collectListenerCalls(env, methodNode, methodName) {
145
+ const results = collectInBody(env.context, methodNode.value.body, methodName);
146
+ const visited = /* @__PURE__ */ new Set([methodNode]);
147
+ walk(methodNode.value.body, (node) => {
148
+ if (node.type !== "CallExpression") return;
149
+ const resolved = resolvePrivateMethodCall(node, env.classNode);
150
+ if (!resolved || visited.has(resolved)) return;
151
+ visited.add(resolved);
152
+ results.push(...collectInBody(env.context, resolved.value.body, methodName));
153
+ });
154
+ return results;
155
+ }
156
+ function collectInBody(context, body, methodName) {
157
+ const results = [];
158
+ walk(body, (node) => {
159
+ if (node.type !== "CallExpression") return;
160
+ const call = matchListenerCall(context, node, methodName);
161
+ if (call) results.push(call);
162
+ });
163
+ return results;
164
+ }
165
+ function matchListenerCall(context, node, methodName) {
166
+ const callee = node.callee;
167
+ if (callee.type !== "MemberExpression") return null;
168
+ const property = callee.property;
169
+ if (property.type !== "Identifier" || property.name !== methodName) return null;
170
+ const eventArg = node.arguments[0];
171
+ if (!eventArg) return null;
172
+ return {
173
+ node,
174
+ target: normalize(context.sourceCode.getText(callee.object)),
175
+ event: normalize(context.sourceCode.getText(eventArg))
176
+ };
177
+ }
178
+ function resolvePrivateMethodCall(callNode, classNode) {
179
+ const callee = callNode.callee;
180
+ if (callee.type !== "MemberExpression") return null;
181
+ if (callee.object.type !== "ThisExpression" || callee.property.type !== "PrivateIdentifier") return null;
182
+ const name = callee.property.name;
183
+ for (const member of classNode.body.body) if (isPrivateMethod(member, name)) return member;
184
+ return null;
185
+ }
186
+ function isPrivateMethod(member, name) {
187
+ return member.type === "MethodDefinition" && !member.static && member.kind === "method" && member.key.type === "PrivateIdentifier" && member.key.name === name;
188
+ }
189
+ function getLifecyclePairs(context) {
190
+ const raw = context.options[0];
191
+ if (!hasLifecyclePairsField(raw) || !Array.isArray(raw.lifecyclePairs)) return DEFAULT_LIFECYCLE_PAIRS;
192
+ const pairs = raw.lifecyclePairs.filter(isLifecyclePair);
193
+ return pairs.length > 0 ? pairs : DEFAULT_LIFECYCLE_PAIRS;
194
+ }
195
+ function hasLifecyclePairsField(value) {
196
+ return typeof value === "object" && value !== null;
197
+ }
198
+ function isLifecyclePair(value) {
199
+ return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "string";
200
+ }
201
+ //#endregion
202
+ export { rule as default };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * ESLint rule that flags observer instances (ResizeObserver, MutationObserver,
3
+ * IntersectionObserver, PerformanceObserver) whose reference is discarded,
4
+ * making disposal impossible.
5
+ */
6
+ import type { Rule } from 'eslint';
7
+ declare const rule: Rule.RuleModule;
8
+ export default rule;
@@ -0,0 +1,69 @@
1
+ import { findEnclosingClass } from "./utils.js";
2
+ //#region src/eslint/rules/require-observer-cleanup.ts
3
+ var OBSERVER_NAMES = /* @__PURE__ */ new Set([
4
+ "ResizeObserver",
5
+ "MutationObserver",
6
+ "IntersectionObserver",
7
+ "PerformanceObserver"
8
+ ]);
9
+ var RETAINED_CONTAINER_TYPES = /* @__PURE__ */ new Set([
10
+ "ReturnStatement",
11
+ "ArrowFunctionExpression",
12
+ "ArrayExpression",
13
+ "SpreadElement"
14
+ ]);
15
+ var rule = {
16
+ meta: {
17
+ type: "problem",
18
+ docs: {
19
+ description: "Flags observer instances created without storing a reference, which prevents them from being disconnected.",
20
+ recommended: true
21
+ },
22
+ schema: [],
23
+ messages: { "inline-observer-leak": "The `{{kind}}` is created but its reference is discarded, so it can never be `.disconnect()`-ed. Assign it to a class field (e.g., `this.#observer = new {{kind}}(...)`) and call `this.#observer.disconnect()` in the teardown lifecycle method to prevent the callback from firing on a detached instance." }
24
+ },
25
+ create(context) {
26
+ return { NewExpression(node) {
27
+ if (node.callee.type !== "Identifier" || !OBSERVER_NAMES.has(node.callee.name)) return;
28
+ if (!findEnclosingClass(node)) return;
29
+ if (isReferenceRetained(node)) return;
30
+ context.report({
31
+ node,
32
+ messageId: "inline-observer-leak",
33
+ data: { kind: node.callee.name }
34
+ });
35
+ } };
36
+ }
37
+ };
38
+ function isReferenceRetained(node) {
39
+ const parent = node.parent;
40
+ if (isTransparentWrapper(parent, node)) return isReferenceRetained(parent);
41
+ if (RETAINED_CONTAINER_TYPES.has(parent.type)) return true;
42
+ if (isRetainedAssignmentTarget(parent, node)) return true;
43
+ return isRetainedCallArgument(parent, node);
44
+ }
45
+ /**
46
+ * `?:` and `??`/`||` forward one operand through as their own result, so an
47
+ * observer sitting in a forwarded branch is exactly as retained as the
48
+ * wrapper expression - the caller re-runs the same retention check one level
49
+ * up. A `ConditionalExpression` `test` is consumed for its boolean-ness, not
50
+ * forwarded, so an observer there is still a discarded leak.
51
+ */
52
+ function isTransparentWrapper(parent, node) {
53
+ if (parent.type === "ConditionalExpression") return parent.consequent === node || parent.alternate === node;
54
+ if (parent.type === "LogicalExpression") return parent.left === node || parent.right === node;
55
+ return false;
56
+ }
57
+ function isRetainedAssignmentTarget(parent, node) {
58
+ if (parent.type === "VariableDeclarator") return parent.init === node;
59
+ if (parent.type === "AssignmentExpression") return parent.right === node;
60
+ if (parent.type === "PropertyDefinition") return parent.value === node;
61
+ if (parent.type === "Property") return parent.value === node;
62
+ return false;
63
+ }
64
+ function isRetainedCallArgument(parent, node) {
65
+ if (parent.type === "CallExpression" || parent.type === "NewExpression") return parent.arguments.includes(node);
66
+ return false;
67
+ }
68
+ //#endregion
69
+ export { rule as default };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ESLint rule that flags timer leaks in class bodies: an unstored
3
+ * `setInterval` (which can never be cleared), a `setInterval` handle stored in
4
+ * a local variable that is never cleared or handed off, and stored
5
+ * `setInterval`/`setTimeout` handles that are never passed to a matching
6
+ * `clearInterval`/`clearTimeout` anywhere in the class.
7
+ */
8
+ import type { Rule } from 'eslint';
9
+ declare const rule: Rule.RuleModule;
10
+ export default rule;
@@ -0,0 +1,142 @@
1
+ import { findEnclosingClass, normalize, walk } from "./utils.js";
2
+ //#region src/eslint/rules/require-timer-cleanup.ts
3
+ var SET_FNS = /* @__PURE__ */ new Set(["setInterval", "setTimeout"]);
4
+ var CLEAR_FNS = /* @__PURE__ */ new Set(["clearInterval", "clearTimeout"]);
5
+ var rule = {
6
+ meta: {
7
+ type: "problem",
8
+ docs: {
9
+ description: "Flags setInterval without a stored handle, and stored setInterval/setTimeout calls whose handle is never cleared, in class bodies.",
10
+ recommended: true
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ "unstoppable-interval": "`setInterval(...)` result is not stored, so the interval cannot be cleared. Assign it to a class field (e.g., `this.#intervalId = setInterval(...)`) and call `clearInterval(this.#intervalId)` in the teardown lifecycle method to stop the interval when the instance is torn down.",
15
+ "missing-timer-cleanup": "`{{fn}}(...)` result is stored on `{{target}}` but no matching `{{clear}}({{target}})` is called anywhere in the class. Clear the timer in the teardown lifecycle method so it does not fire after the instance is gone."
16
+ }
17
+ },
18
+ create(context) {
19
+ return { CallExpression(node) {
20
+ const fnName = getTimerFnName(node.callee);
21
+ if (!fnName) return;
22
+ const classNode = findEnclosingClass(node);
23
+ if (!classNode) return;
24
+ classifyAndReport(context, node, {
25
+ fnName,
26
+ classNode
27
+ });
28
+ } };
29
+ }
30
+ };
31
+ function getTimerFnName(callee) {
32
+ if (callee.type === "Identifier" && SET_FNS.has(callee.name)) return callee.name;
33
+ if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && SET_FNS.has(callee.property.name)) return callee.property.name;
34
+ return null;
35
+ }
36
+ function classifyAndReport(context, callNode, info) {
37
+ const parent = callNode.parent;
38
+ const stored = getStoredMemberText(parent, callNode, context);
39
+ if (stored) {
40
+ reportIfNotCleared(context, callNode, {
41
+ ...info,
42
+ stored
43
+ });
44
+ return;
45
+ }
46
+ if (info.fnName !== "setInterval") return;
47
+ if (parent.type === "ExpressionStatement") {
48
+ context.report({
49
+ node: callNode,
50
+ messageId: "unstoppable-interval"
51
+ });
52
+ return;
53
+ }
54
+ const localHandle = getLocalIntervalHandleDeclarator(parent, callNode);
55
+ if (localHandle && !localIntervalHandleIsClearedOrEscapes(context, localHandle)) context.report({
56
+ node: callNode,
57
+ messageId: "unstoppable-interval"
58
+ });
59
+ }
60
+ function reportIfNotCleared(context, callNode, info) {
61
+ const clearName = info.fnName === "setInterval" ? "clearInterval" : "clearTimeout";
62
+ if (classHasClearForTarget(info.classNode, {
63
+ clearName,
64
+ targetText: info.stored
65
+ }, context)) return;
66
+ context.report({
67
+ node: callNode,
68
+ messageId: "missing-timer-cleanup",
69
+ data: {
70
+ fn: info.fnName,
71
+ target: info.stored,
72
+ clear: clearName
73
+ }
74
+ });
75
+ }
76
+ function getStoredMemberText(parent, callNode, context) {
77
+ if (parent.type === "AssignmentExpression" && parent.right === callNode) return thisMemberText(parent.left, context);
78
+ if (parent.type === "PropertyDefinition" && parent.value === callNode) return propertyDefinitionAsThisMember(parent, context);
79
+ return null;
80
+ }
81
+ function thisMemberText(node, context) {
82
+ if (node.type !== "MemberExpression") return null;
83
+ if (node.object.type !== "ThisExpression") return null;
84
+ return normalize(context.sourceCode.getText(node));
85
+ }
86
+ function propertyDefinitionAsThisMember(node, context) {
87
+ if (node.static) return null;
88
+ const key = node.key;
89
+ if (key.type === "PrivateIdentifier") return `this.#${key.name}`;
90
+ if (key.type === "Identifier") return `this.${key.name}`;
91
+ return `this[${context.sourceCode.getText(key)}]`;
92
+ }
93
+ /**
94
+ * Matches `const id = setInterval(...)` (any of `const`/`let`/`var`), where
95
+ * the handle is bound to a plain identifier rather than destructured. A
96
+ * destructuring pattern can't be resolved back to a single traceable binding
97
+ * here, so it falls outside this rule's scope-analysis path.
98
+ */
99
+ function getLocalIntervalHandleDeclarator(parent, callNode) {
100
+ if (parent.type !== "VariableDeclarator" || parent.init !== callNode || parent.id.type !== "Identifier") return null;
101
+ return parent;
102
+ }
103
+ /**
104
+ * A local interval handle is safe to leave unstored-on-`this` when every use
105
+ * of the binding shows the handle is either cleared or handed off before the
106
+ * enclosing function returns: passed to any call (`clearInterval(id)`, or an
107
+ * external owner like `registry.track(id)`), returned to the caller, or
108
+ * assigned onward to a longer-lived reference (`this.#timer = id`). Uses scope
109
+ * analysis (rather than walking the enclosing function's AST) so a reference
110
+ * inside the interval's own callback - a closure over the binding - still
111
+ * resolves back to the same variable.
112
+ */
113
+ function localIntervalHandleIsClearedOrEscapes(context, declarator) {
114
+ const [handleVariable] = context.sourceCode.getDeclaredVariables(declarator);
115
+ if (!handleVariable) return true;
116
+ return handleVariable.references.some((reference) => referenceClearsOrReleasesHandle(reference));
117
+ }
118
+ function referenceClearsOrReleasesHandle(reference) {
119
+ const identifier = reference.identifier;
120
+ const parent = identifier.parent;
121
+ if (parent.type === "CallExpression") return parent.arguments.includes(identifier);
122
+ if (parent.type === "ReturnStatement") return parent.argument === identifier;
123
+ if (parent.type === "AssignmentExpression") return parent.right === identifier;
124
+ return false;
125
+ }
126
+ function classHasClearForTarget(classNode, search, context) {
127
+ let found = false;
128
+ walk(classNode.body, (node) => {
129
+ if (found || node.type !== "CallExpression") return;
130
+ if (getClearFnName(node.callee) !== search.clearName) return;
131
+ const firstArg = node.arguments[0];
132
+ if (firstArg && normalize(context.sourceCode.getText(firstArg)) === search.targetText) found = true;
133
+ });
134
+ return found;
135
+ }
136
+ function getClearFnName(callee) {
137
+ if (callee.type === "Identifier" && CLEAR_FNS.has(callee.name)) return callee.name;
138
+ if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && CLEAR_FNS.has(callee.property.name)) return callee.property.name;
139
+ return null;
140
+ }
141
+ //#endregion
142
+ export { rule as default };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared helpers for rules that need to reason about AST shape generically
3
+ * (walking a subtree, matching normalized text), about lexical scope (which
4
+ * class a node lives in), or about the filesystem (listing a package's source
5
+ * files, reading them safely). Extracted so that logic stays consistent
6
+ * across the `tools/*` rules that build on top of them.
7
+ */
8
+ import type { Rule } from 'eslint';
9
+ /**
10
+ * Recursively visits every node reachable from `node`, in no particular
11
+ * guaranteed order beyond depth-first. Skips the `parent` back-pointer (every
12
+ * node points back to its parent, so following it would recurse forever) and
13
+ * descends into arrays of children and into single child nodes.
14
+ */
15
+ export declare function walk(node: unknown, visit: (node: Rule.Node) => void): void;
16
+ /** Collapses internal whitespace so multi-line source text matches regardless of formatting. */
17
+ export declare function normalize(text: string): string;
18
+ type ClassNode = Extract<Rule.Node, {
19
+ type: 'ClassDeclaration' | 'ClassExpression';
20
+ }>;
21
+ /**
22
+ * Walks up `.parent` looking for the nearest enclosing class. Returns `null`
23
+ * when `node` lives at module scope (or otherwise outside any class body).
24
+ */
25
+ export declare function findEnclosingClass(node: Rule.Node): ClassNode | null;
26
+ /** Directories skipped by every recursive package-file listing, regardless of rule config. */
27
+ export declare const IMPLICIT_EXCLUDED_DIRS: readonly string[];
28
+ /**
29
+ * Milliseconds a package's recursive file listing stays cached before
30
+ * {@link getPackageFiles} recomputes it. Long enough to amortize repeated
31
+ * lookups across every candidate class linted within one file and across one
32
+ * lint pass; short enough that a long-lived ESLint process (IDE language
33
+ * server, `eslint_d`, watch mode) reflects a file added or removed on disk
34
+ * within about one save-and-relint cycle instead of requiring a restart.
35
+ */
36
+ export declare const FILE_CACHE_TTL_MS = 5000;
37
+ /** Every file under `root`, recursively, excluding {@link IMPLICIT_EXCLUDED_DIRS}. Cached per root. */
38
+ export declare function getPackageFiles(root: string): readonly string[];
39
+ /** Reads `file` as UTF-8, or `null` when it doesn't exist or isn't readable. */
40
+ export declare function readFileSafe(file: string): string | null;
41
+ /** Parses `text` as JSON, or `null` when it isn't valid JSON. */
42
+ export declare function tryParseJson(text: string): unknown;
43
+ /** Converts a `file://` URL or OS-native path to a forward-slash path for cross-platform text matching. */
44
+ export declare function normalizePath(filepath: string): string;
45
+ export {};
@@ -0,0 +1,109 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { join, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/eslint/rules/utils.ts
5
+ /**
6
+ * Shared helpers for rules that need to reason about AST shape generically
7
+ * (walking a subtree, matching normalized text), about lexical scope (which
8
+ * class a node lives in), or about the filesystem (listing a package's source
9
+ * files, reading them safely). Extracted so that logic stays consistent
10
+ * across the `tools/*` rules that build on top of them.
11
+ */
12
+ /** Narrows an unknown value to `AstNode` without asserting anything about it. */
13
+ function isAstNode(value) {
14
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
15
+ }
16
+ /**
17
+ * Recursively visits every node reachable from `node`, in no particular
18
+ * guaranteed order beyond depth-first. Skips the `parent` back-pointer (every
19
+ * node points back to its parent, so following it would recurse forever) and
20
+ * descends into arrays of children and into single child nodes.
21
+ */
22
+ function walk(node, visit) {
23
+ if (!isAstNode(node)) return;
24
+ visit(node);
25
+ for (const key of Object.keys(node)) {
26
+ if (key === "parent") continue;
27
+ const child = node[key];
28
+ if (Array.isArray(child)) for (const item of child) walk(item, visit);
29
+ else if (isAstNode(child)) walk(child, visit);
30
+ }
31
+ }
32
+ /** Collapses internal whitespace so multi-line source text matches regardless of formatting. */
33
+ function normalize(text) {
34
+ return text.replace(/\s+/g, " ").trim();
35
+ }
36
+ /**
37
+ * Walks up `.parent` looking for the nearest enclosing class. Returns `null`
38
+ * when `node` lives at module scope (or otherwise outside any class body).
39
+ */
40
+ function findEnclosingClass(node) {
41
+ let current = node.parent;
42
+ while (current) {
43
+ if (current.type === "ClassDeclaration" || current.type === "ClassExpression") return current;
44
+ current = current.parent;
45
+ }
46
+ return null;
47
+ }
48
+ /** Directories skipped by every recursive package-file listing, regardless of rule config. */
49
+ var IMPLICIT_EXCLUDED_DIRS = ["node_modules", "dist"];
50
+ /**
51
+ * Milliseconds a package's recursive file listing stays cached before
52
+ * {@link getPackageFiles} recomputes it. Long enough to amortize repeated
53
+ * lookups across every candidate class linted within one file and across one
54
+ * lint pass; short enough that a long-lived ESLint process (IDE language
55
+ * server, `eslint_d`, watch mode) reflects a file added or removed on disk
56
+ * within about one save-and-relint cycle instead of requiring a restart.
57
+ */
58
+ var FILE_CACHE_TTL_MS = 5e3;
59
+ /** Recursive file listing per package root, cached for {@link FILE_CACHE_TTL_MS}. */
60
+ var FILE_CACHE = /* @__PURE__ */ new Map();
61
+ /** Every file under `root`, recursively, excluding {@link IMPLICIT_EXCLUDED_DIRS}. Cached per root. */
62
+ function getPackageFiles(root) {
63
+ const key = normalizePath(root);
64
+ const cached = FILE_CACHE.get(key);
65
+ const now = Date.now();
66
+ if (cached !== void 0 && now - cached.cachedAtMs < 5e3) return cached.files;
67
+ const files = collectFilesSafe(key).map(normalizePath);
68
+ FILE_CACHE.set(key, {
69
+ files,
70
+ cachedAtMs: now
71
+ });
72
+ return files;
73
+ }
74
+ function collectFilesSafe(dir) {
75
+ return readDirSafe(dir).flatMap((entry) => collectEntry(dir, entry));
76
+ }
77
+ function collectEntry(dir, entry) {
78
+ if (entry.isDirectory()) return IMPLICIT_EXCLUDED_DIRS.includes(entry.name) ? [] : collectFilesSafe(join(dir, entry.name));
79
+ return entry.isFile() ? [join(dir, entry.name)] : [];
80
+ }
81
+ function readDirSafe(dir) {
82
+ try {
83
+ return readdirSync(dir, { withFileTypes: true });
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+ /** Reads `file` as UTF-8, or `null` when it doesn't exist or isn't readable. */
89
+ function readFileSafe(file) {
90
+ try {
91
+ return readFileSync(file, "utf8");
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+ /** Parses `text` as JSON, or `null` when it isn't valid JSON. */
97
+ function tryParseJson(text) {
98
+ try {
99
+ return JSON.parse(text);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ /** Converts a `file://` URL or OS-native path to a forward-slash path for cross-platform text matching. */
105
+ function normalizePath(filepath) {
106
+ return (filepath.startsWith("file://") ? fileURLToPath(filepath) : filepath).replaceAll(sep, "/");
107
+ }
108
+ //#endregion
109
+ export { FILE_CACHE_TTL_MS, IMPLICIT_EXCLUDED_DIRS, findEnclosingClass, getPackageFiles, normalize, normalizePath, readFileSafe, tryParseJson, walk };
@@ -0,0 +1,15 @@
1
+ import type { Config } from 'prettier';
2
+ /**
3
+ * Personal Prettier preferences, shared across projects so formatting stops
4
+ * being a per-repo decision. Point `package.json`'s `"prettier"` field at
5
+ * `"@coryrylan/tools/prettier"` to consume it directly, or import this
6
+ * default export and spread it into a local `prettier.config.js` to layer
7
+ * project-specific overrides on top.
8
+ *
9
+ * `embeddedLanguageFormatting: 'off'` is deliberate: embedded template
10
+ * literals (Lit `html`/`css` tagged templates) are hand-formatted rather
11
+ * than reflowed by Prettier's embedded-language support, which tends to
12
+ * mangle multi-line markup readability.
13
+ */
14
+ declare const config: Config;
15
+ export default config;
@@ -0,0 +1,26 @@
1
+ //#region src/prettier/index.ts
2
+ /**
3
+ * Personal Prettier preferences, shared across projects so formatting stops
4
+ * being a per-repo decision. Point `package.json`'s `"prettier"` field at
5
+ * `"@coryrylan/tools/prettier"` to consume it directly, or import this
6
+ * default export and spread it into a local `prettier.config.js` to layer
7
+ * project-specific overrides on top.
8
+ *
9
+ * `embeddedLanguageFormatting: 'off'` is deliberate: embedded template
10
+ * literals (Lit `html`/`css` tagged templates) are hand-formatted rather
11
+ * than reflowed by Prettier's embedded-language support, which tends to
12
+ * mangle multi-line markup readability.
13
+ */
14
+ var config = {
15
+ trailingComma: "none",
16
+ tabWidth: 2,
17
+ printWidth: 120,
18
+ semi: true,
19
+ singleQuote: true,
20
+ arrowParens: "avoid",
21
+ bracketSameLine: true,
22
+ embeddedLanguageFormatting: "off",
23
+ singleAttributePerLine: false
24
+ };
25
+ //#endregion
26
+ export { config as default };
@@ -0,0 +1,10 @@
1
+ import type { Config } from 'stylelint';
2
+ /**
3
+ * Personal Stylelint preferences layered on `stylelint-config-standard`.
4
+ * Point a `stylelint.config.js` at it with
5
+ * `export { default } from '@coryrylan/tools/stylelint';`, or import the
6
+ * default export and add `rules`/`overrides` on top for project-specific
7
+ * exceptions.
8
+ */
9
+ declare const config: Config;
10
+ export default config;