hook-o-gnese 0.0.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/LICENSE +15 -0
- package/README.md +182 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +237 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/engine-DJFFKwTZ.mjs +114 -0
- package/dist/engine-DJFFKwTZ.mjs.map +1 -0
- package/dist/engine.d.mts +27 -0
- package/dist/engine.mjs +2 -0
- package/dist/index.d.mts +3993 -0
- package/dist/index.mjs +357 -0
- package/dist/index.mjs.map +1 -0
- package/dist/registry-iRG6wil9.mjs +460 -0
- package/dist/registry-iRG6wil9.mjs.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
//#region src/ast-helpers.ts
|
|
3
|
+
const HOOK_RE = /^use[A-Z]/;
|
|
4
|
+
function getHookName(node) {
|
|
5
|
+
if (node.type !== "CallExpression") return null;
|
|
6
|
+
const callee = node.callee;
|
|
7
|
+
if (callee?.type !== "Identifier") return null;
|
|
8
|
+
const name = callee.name;
|
|
9
|
+
return HOOK_RE.test(name) ? name : null;
|
|
10
|
+
}
|
|
11
|
+
function isHookCall(node, expected) {
|
|
12
|
+
return getHookName(node) === expected;
|
|
13
|
+
}
|
|
14
|
+
function isReactComponent(node) {
|
|
15
|
+
if (node.type === "FunctionDeclaration") {
|
|
16
|
+
const name = node.id?.name;
|
|
17
|
+
if (!name || !/^[A-Z]/.test(name)) return false;
|
|
18
|
+
return findReturnsJSX(node);
|
|
19
|
+
}
|
|
20
|
+
if (node.type === "VariableDeclaration") {
|
|
21
|
+
const decl = node.declarations?.[0];
|
|
22
|
+
const name = decl?.id?.name;
|
|
23
|
+
const init = decl?.init;
|
|
24
|
+
if (!name || !/^[A-Z]/.test(name) || !init) return false;
|
|
25
|
+
if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") return findReturnsJSX(init);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function findReturnsJSX(node) {
|
|
30
|
+
let found = false;
|
|
31
|
+
walk(node, (n) => {
|
|
32
|
+
if (n.type === "JSXElement" || n.type === "JSXFragment" || n.type === "JSXSelfClosingElement") {
|
|
33
|
+
found = true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
function walk(node, visit, seen = /* @__PURE__ */ new WeakSet()) {
|
|
41
|
+
if (seen.has(node)) return;
|
|
42
|
+
seen.add(node);
|
|
43
|
+
if (visit(node) === false) return;
|
|
44
|
+
for (const key in node) {
|
|
45
|
+
if (key === "parent") continue;
|
|
46
|
+
const val = node[key];
|
|
47
|
+
if (Array.isArray(val)) {
|
|
48
|
+
for (const child of val) if (child && typeof child === "object" && "type" in child) walk(child, visit, seen);
|
|
49
|
+
} else if (val && typeof val === "object" && "type" in val) walk(val, visit, seen);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/scoring/effect-score.ts
|
|
54
|
+
const SET_STATE_RE = /^set[A-Z]/;
|
|
55
|
+
const BRANCH_TYPES = new Set([
|
|
56
|
+
"IfStatement",
|
|
57
|
+
"ConditionalExpression",
|
|
58
|
+
"SwitchCase",
|
|
59
|
+
"LogicalExpression"
|
|
60
|
+
]);
|
|
61
|
+
function scoreEffect(node) {
|
|
62
|
+
const args = node.arguments;
|
|
63
|
+
const fn = args?.[0];
|
|
64
|
+
const depsArr = args?.[1];
|
|
65
|
+
const deps = Array.isArray(depsArr?.elements) ? depsArr.elements.length : 0;
|
|
66
|
+
let branches = 0;
|
|
67
|
+
let setStateCount = 0;
|
|
68
|
+
let nestedEffects = 0;
|
|
69
|
+
let hasCleanup = false;
|
|
70
|
+
let hasSubscriptionLike = false;
|
|
71
|
+
if (fn) {
|
|
72
|
+
const body = fn.body;
|
|
73
|
+
if (body?.type === "BlockStatement") {
|
|
74
|
+
for (const stmt of body.body) if (stmt.type === "ReturnStatement") {
|
|
75
|
+
const arg = stmt.argument;
|
|
76
|
+
if (arg && (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression")) hasCleanup = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
walk(fn, (n) => {
|
|
80
|
+
if (BRANCH_TYPES.has(n.type)) branches++;
|
|
81
|
+
if (n.type === "CallExpression") {
|
|
82
|
+
const callee = n.callee;
|
|
83
|
+
if (callee?.type === "Identifier") {
|
|
84
|
+
const name = callee.name;
|
|
85
|
+
if (SET_STATE_RE.test(name)) setStateCount++;
|
|
86
|
+
if (name === "useEffect" && n !== node) nestedEffects++;
|
|
87
|
+
if (name === "addEventListener" || name === "subscribe" || name === "setInterval" || name === "setTimeout") hasSubscriptionLike = true;
|
|
88
|
+
}
|
|
89
|
+
if (callee?.type === "MemberExpression") {
|
|
90
|
+
const prop = callee.property;
|
|
91
|
+
if (prop?.type === "Identifier") {
|
|
92
|
+
const name = prop.name;
|
|
93
|
+
if (name === "addEventListener" || name === "subscribe" || name === "on") hasSubscriptionLike = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const cleanupPenalty = hasSubscriptionLike && !hasCleanup ? 3 : 0;
|
|
101
|
+
const total = deps + branches * 2 + setStateCount * 1.5 + nestedEffects * 5 + cleanupPenalty;
|
|
102
|
+
return {
|
|
103
|
+
deps,
|
|
104
|
+
branches,
|
|
105
|
+
setStateCount,
|
|
106
|
+
nestedEffects,
|
|
107
|
+
hasCleanup,
|
|
108
|
+
hasSubscriptionLike,
|
|
109
|
+
total
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/scoring/thresholds.ts
|
|
114
|
+
const DEFAULT_THRESHOLDS = {
|
|
115
|
+
fatEffect: {
|
|
116
|
+
warn: 10,
|
|
117
|
+
error: 20
|
|
118
|
+
},
|
|
119
|
+
stateScatter: {
|
|
120
|
+
warn: 5,
|
|
121
|
+
error: 8
|
|
122
|
+
},
|
|
123
|
+
hookCoupling: {
|
|
124
|
+
warn: 3,
|
|
125
|
+
error: 6
|
|
126
|
+
},
|
|
127
|
+
customHookDepth: {
|
|
128
|
+
warn: 3,
|
|
129
|
+
error: 5
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/rules/no-fat-effects.ts
|
|
134
|
+
const noFatEffects = {
|
|
135
|
+
meta: {
|
|
136
|
+
type: "suggestion",
|
|
137
|
+
docs: { description: "Flag dense useEffect blocks" }
|
|
138
|
+
},
|
|
139
|
+
create(context) {
|
|
140
|
+
const opts = context.options[0] ?? {};
|
|
141
|
+
const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.fatEffect.warn;
|
|
142
|
+
const errorThreshold = opts.errorThreshold ?? DEFAULT_THRESHOLDS.fatEffect.error;
|
|
143
|
+
return { CallExpression(node) {
|
|
144
|
+
if (!isHookCall(node, "useEffect")) return;
|
|
145
|
+
const score = scoreEffect(node);
|
|
146
|
+
if (score.total >= threshold) {
|
|
147
|
+
const breakdown = `deps=${score.deps} branches=${score.branches} setStates=${score.setStateCount} nested=${score.nestedEffects}` + (score.hasSubscriptionLike && !score.hasCleanup ? " missing-cleanup" : "");
|
|
148
|
+
const severity = score.total >= errorThreshold ? "error" : "warn";
|
|
149
|
+
context.report({
|
|
150
|
+
message: `useEffect entropy ${score.total.toFixed(1)} ≥ ${threshold} (${breakdown})`,
|
|
151
|
+
node,
|
|
152
|
+
severity
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} };
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/scoring/state-score.ts
|
|
160
|
+
function scoreComponentState(componentNode) {
|
|
161
|
+
const setterNames = /* @__PURE__ */ new Set();
|
|
162
|
+
let useStateCount = 0;
|
|
163
|
+
walk(componentNode, (n) => {
|
|
164
|
+
if (!n) return true;
|
|
165
|
+
if (n.type === "VariableDeclarator") {
|
|
166
|
+
const init = n.init;
|
|
167
|
+
const id = n.id;
|
|
168
|
+
if (init?.type === "CallExpression" && isHookCall(init, "useState") && id?.type === "ArrayPattern") {
|
|
169
|
+
useStateCount++;
|
|
170
|
+
const setter = id.elements?.[1];
|
|
171
|
+
if (setter?.type === "Identifier") setterNames.add(setter.name);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
let correlatedSetters = 0;
|
|
177
|
+
walk(componentNode, (n) => {
|
|
178
|
+
if (!n) return true;
|
|
179
|
+
if (n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression") {
|
|
180
|
+
const calledSetters = /* @__PURE__ */ new Set();
|
|
181
|
+
walk(n, (m) => {
|
|
182
|
+
if (!m) return true;
|
|
183
|
+
if (m.type === "CallExpression") {
|
|
184
|
+
const callee = m.callee;
|
|
185
|
+
if (callee?.type === "Identifier") {
|
|
186
|
+
const name = callee.name;
|
|
187
|
+
if (setterNames.has(name)) calledSetters.add(name);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
if (calledSetters.size >= 2) correlatedSetters += calledSetters.size;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
196
|
+
const total = useStateCount + correlatedSetters * .5;
|
|
197
|
+
return {
|
|
198
|
+
useStateCount,
|
|
199
|
+
correlatedSetters,
|
|
200
|
+
total
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/rules/state-scatter.ts
|
|
205
|
+
const stateScatter = {
|
|
206
|
+
meta: {
|
|
207
|
+
type: "suggestion",
|
|
208
|
+
docs: { description: "Flag components with too many useState calls" }
|
|
209
|
+
},
|
|
210
|
+
create(context) {
|
|
211
|
+
const opts = context.options[0] ?? {};
|
|
212
|
+
const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.stateScatter.warn;
|
|
213
|
+
const errorThreshold = opts.errorThreshold ?? DEFAULT_THRESHOLDS.stateScatter.error;
|
|
214
|
+
function check(node) {
|
|
215
|
+
if (!isReactComponent(node)) return;
|
|
216
|
+
const s = scoreComponentState(node);
|
|
217
|
+
if (s.total >= threshold) {
|
|
218
|
+
const severity = s.total >= errorThreshold ? "error" : "warn";
|
|
219
|
+
context.report({
|
|
220
|
+
message: `state scatter ${s.total} ≥ ${threshold} (useStates=${s.useStateCount}, correlated setters=${s.correlatedSetters}). Consider useReducer.`,
|
|
221
|
+
node,
|
|
222
|
+
severity
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
FunctionDeclaration: check,
|
|
228
|
+
VariableDeclaration: check
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/scoring/coupling-score.ts
|
|
234
|
+
function scoreCoupling(componentNode) {
|
|
235
|
+
const stateBySetter = /* @__PURE__ */ new Map();
|
|
236
|
+
walk(componentNode, (n) => {
|
|
237
|
+
if (n.type === "VariableDeclarator") {
|
|
238
|
+
const init = n.init;
|
|
239
|
+
const id = n.id;
|
|
240
|
+
if (init?.type === "CallExpression" && isHookCall(init, "useState") && id?.type === "ArrayPattern") {
|
|
241
|
+
const els = id.elements;
|
|
242
|
+
const stateId = els?.[0];
|
|
243
|
+
const setterId = els?.[1];
|
|
244
|
+
if (stateId?.type === "Identifier" && setterId?.type === "Identifier") stateBySetter.set(setterId.name, stateId.name);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
});
|
|
249
|
+
const readWriteSame = [];
|
|
250
|
+
let total = 0;
|
|
251
|
+
walk(componentNode, (n) => {
|
|
252
|
+
if (n.type === "CallExpression" && isHookCall(n, "useEffect")) {
|
|
253
|
+
const effectFn = n.arguments?.[0];
|
|
254
|
+
if (!effectFn) return true;
|
|
255
|
+
const stateRefs = /* @__PURE__ */ new Set();
|
|
256
|
+
const stateWrites = /* @__PURE__ */ new Set();
|
|
257
|
+
const stateNames = new Set(stateBySetter.values());
|
|
258
|
+
walk(effectFn, (m) => {
|
|
259
|
+
if (m.type === "Identifier") {
|
|
260
|
+
const name = m.name;
|
|
261
|
+
if (stateNames.has(name)) stateRefs.add(name);
|
|
262
|
+
}
|
|
263
|
+
if (m.type === "CallExpression") {
|
|
264
|
+
const callee = m.callee;
|
|
265
|
+
if (callee?.type === "Identifier") {
|
|
266
|
+
const setter = callee.name;
|
|
267
|
+
const stateName = stateBySetter.get(setter);
|
|
268
|
+
if (stateName) stateWrites.add(stateName);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
});
|
|
273
|
+
for (const written of stateWrites) if (stateRefs.has(written)) {
|
|
274
|
+
readWriteSame.push({
|
|
275
|
+
state: written,
|
|
276
|
+
effect: n
|
|
277
|
+
});
|
|
278
|
+
total += 3;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
total,
|
|
285
|
+
readWriteSame
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/rules/hook-coupling.ts
|
|
290
|
+
const hookCoupling = {
|
|
291
|
+
meta: {
|
|
292
|
+
type: "problem",
|
|
293
|
+
docs: { description: "Flag effects that read state they also write (loop bait)" }
|
|
294
|
+
},
|
|
295
|
+
create(context) {
|
|
296
|
+
const opts = context.options[0] ?? {};
|
|
297
|
+
const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.hookCoupling.warn;
|
|
298
|
+
const errorThreshold = opts.errorThreshold ?? DEFAULT_THRESHOLDS.hookCoupling.error;
|
|
299
|
+
function check(node) {
|
|
300
|
+
if (!isReactComponent(node)) return;
|
|
301
|
+
const s = scoreCoupling(node);
|
|
302
|
+
if (s.total < threshold) return;
|
|
303
|
+
const severity = s.total >= errorThreshold ? "error" : "warn";
|
|
304
|
+
for (const v of s.readWriteSame) context.report({
|
|
305
|
+
message: `useEffect reads + writes same state '${v.state}' (loop risk)`,
|
|
306
|
+
node: v.effect,
|
|
307
|
+
severity
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
FunctionDeclaration: check,
|
|
312
|
+
VariableDeclaration: check
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/ts-program.ts
|
|
318
|
+
var TsProgramCache = class {
|
|
319
|
+
program = null;
|
|
320
|
+
rootDir;
|
|
321
|
+
constructor(rootDir) {
|
|
322
|
+
this.rootDir = rootDir;
|
|
323
|
+
}
|
|
324
|
+
getProgram() {
|
|
325
|
+
if (this.program) return this.program;
|
|
326
|
+
const configPath = ts.findConfigFile(this.rootDir, ts.sys.fileExists, "tsconfig.json");
|
|
327
|
+
let compilerOptions = {
|
|
328
|
+
target: ts.ScriptTarget.ESNext,
|
|
329
|
+
module: ts.ModuleKind.ESNext,
|
|
330
|
+
jsx: ts.JsxEmit.Preserve,
|
|
331
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
332
|
+
allowJs: true,
|
|
333
|
+
noEmit: true,
|
|
334
|
+
skipLibCheck: true,
|
|
335
|
+
strict: false
|
|
336
|
+
};
|
|
337
|
+
let fileNames = [];
|
|
338
|
+
if (configPath) {
|
|
339
|
+
const cfg = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
340
|
+
const configDir = configPath.slice(0, configPath.lastIndexOf("/"));
|
|
341
|
+
const parsed = ts.parseJsonConfigFileContent(cfg.config, ts.sys, configDir);
|
|
342
|
+
compilerOptions = {
|
|
343
|
+
...compilerOptions,
|
|
344
|
+
...parsed.options
|
|
345
|
+
};
|
|
346
|
+
fileNames = parsed.fileNames;
|
|
347
|
+
}
|
|
348
|
+
this.program = ts.createProgram(fileNames, compilerOptions);
|
|
349
|
+
return this.program;
|
|
350
|
+
}
|
|
351
|
+
resolveIdentifierDeclaration(filePath, identifier) {
|
|
352
|
+
const program = this.getProgram();
|
|
353
|
+
const checker = program.getTypeChecker();
|
|
354
|
+
const absolute = filePath.startsWith("/") ? filePath : `${this.rootDir.replace(/\/$/, "")}/${filePath}`;
|
|
355
|
+
const sourceFile = program.getSourceFile(absolute) ?? program.getSourceFile(filePath);
|
|
356
|
+
if (!sourceFile) return null;
|
|
357
|
+
let target = null;
|
|
358
|
+
function find(node) {
|
|
359
|
+
if (target) return;
|
|
360
|
+
if (ts.isIdentifier(node) && node.text === identifier) {
|
|
361
|
+
target = node;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
ts.forEachChild(node, find);
|
|
365
|
+
}
|
|
366
|
+
find(sourceFile);
|
|
367
|
+
if (!target) return null;
|
|
368
|
+
const symbol = checker.getSymbolAtLocation(target);
|
|
369
|
+
if (!symbol) return null;
|
|
370
|
+
return (symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol).declarations?.[0] ?? null;
|
|
371
|
+
}
|
|
372
|
+
countTransitiveHookCalls(decl, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
373
|
+
if (depth > 10 || seen.has(decl)) return depth;
|
|
374
|
+
seen.add(decl);
|
|
375
|
+
const checker = this.getProgram().getTypeChecker();
|
|
376
|
+
let maxDepth = depth;
|
|
377
|
+
const visit = (node) => {
|
|
378
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && /^use[A-Z]/.test(node.expression.text)) {
|
|
379
|
+
const sym = checker.getSymbolAtLocation(node.expression);
|
|
380
|
+
if (sym) {
|
|
381
|
+
const innerDecl = (sym.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(sym) : sym).declarations?.[0];
|
|
382
|
+
if (innerDecl) {
|
|
383
|
+
const sf = innerDecl.getSourceFile();
|
|
384
|
+
if (sf.fileName.includes("node_modules/@types/react") || sf.fileName.includes("node_modules/react/")) maxDepth = Math.max(maxDepth, depth + 1);
|
|
385
|
+
else {
|
|
386
|
+
const childDepth = this.countTransitiveHookCalls(innerDecl, depth + 1, seen);
|
|
387
|
+
maxDepth = Math.max(maxDepth, childDepth);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
ts.forEachChild(node, visit);
|
|
393
|
+
};
|
|
394
|
+
visit(decl);
|
|
395
|
+
return maxDepth;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/rules/custom-hook-depth.ts
|
|
400
|
+
const REACT_HOOKS = new Set([
|
|
401
|
+
"useState",
|
|
402
|
+
"useEffect",
|
|
403
|
+
"useLayoutEffect",
|
|
404
|
+
"useMemo",
|
|
405
|
+
"useCallback",
|
|
406
|
+
"useReducer",
|
|
407
|
+
"useContext",
|
|
408
|
+
"useRef",
|
|
409
|
+
"useImperativeHandle",
|
|
410
|
+
"useDebugValue",
|
|
411
|
+
"useId",
|
|
412
|
+
"useTransition",
|
|
413
|
+
"useDeferredValue",
|
|
414
|
+
"useSyncExternalStore",
|
|
415
|
+
"useInsertionEffect"
|
|
416
|
+
]);
|
|
417
|
+
let sharedCache = null;
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/rules/registry.ts
|
|
420
|
+
const ALL_RULES = {
|
|
421
|
+
"no-fat-effects": noFatEffects,
|
|
422
|
+
"state-scatter": stateScatter,
|
|
423
|
+
"hook-coupling": hookCoupling,
|
|
424
|
+
"custom-hook-depth": {
|
|
425
|
+
meta: {
|
|
426
|
+
type: "suggestion",
|
|
427
|
+
docs: { description: "Flag custom hooks whose transitive nesting exceeds maxDepth (type-aware)." }
|
|
428
|
+
},
|
|
429
|
+
create(context) {
|
|
430
|
+
const opts = context.options[0] ?? {};
|
|
431
|
+
const maxDepth = opts.maxDepth ?? DEFAULT_THRESHOLDS.customHookDepth.warn;
|
|
432
|
+
const errorMaxDepth = opts.errorMaxDepth ?? DEFAULT_THRESHOLDS.customHookDepth.error;
|
|
433
|
+
const g = globalThis;
|
|
434
|
+
const cwd = context.cwd ?? g.process?.cwd() ?? g.Deno?.cwd() ?? ".";
|
|
435
|
+
sharedCache ??= new TsProgramCache(cwd);
|
|
436
|
+
const cache = sharedCache;
|
|
437
|
+
const filename = context.filename;
|
|
438
|
+
return { CallExpression(node) {
|
|
439
|
+
const name = getHookName(node);
|
|
440
|
+
if (!name || REACT_HOOKS.has(name)) return;
|
|
441
|
+
if (!filename) return;
|
|
442
|
+
const decl = cache.resolveIdentifierDeclaration(filename, name);
|
|
443
|
+
if (!decl) return;
|
|
444
|
+
const depth = cache.countTransitiveHookCalls(decl);
|
|
445
|
+
if (depth >= maxDepth) {
|
|
446
|
+
const severity = depth >= errorMaxDepth ? "error" : "warn";
|
|
447
|
+
context.report({
|
|
448
|
+
message: `custom hook '${name}' transitive depth ${depth} ≥ ${maxDepth}`,
|
|
449
|
+
node,
|
|
450
|
+
severity
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
} };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
//#endregion
|
|
458
|
+
export { ALL_RULES as t };
|
|
459
|
+
|
|
460
|
+
//# sourceMappingURL=registry-iRG6wil9.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-iRG6wil9.mjs","names":[],"sources":["../src/ast-helpers.ts","../src/scoring/effect-score.ts","../src/scoring/thresholds.ts","../src/rules/no-fat-effects.ts","../src/scoring/state-score.ts","../src/rules/state-scatter.ts","../src/scoring/coupling-score.ts","../src/rules/hook-coupling.ts","../src/ts-program.ts","../src/rules/custom-hook-depth.ts","../src/rules/registry.ts"],"sourcesContent":["type Node = { type: string; [k: string]: unknown };\n\nconst HOOK_RE = /^use[A-Z]/;\n\nexport function getHookName(node: Node): string | null {\n if (node.type !== \"CallExpression\") return null;\n const callee = (node as any).callee as Node;\n if (callee?.type !== \"Identifier\") return null;\n const name = (callee as any).name as string;\n return HOOK_RE.test(name) ? name : null;\n}\n\nexport function isHookCall(node: Node, expected: string): boolean {\n return getHookName(node) === expected;\n}\n\nexport function isReactComponent(node: Node): boolean {\n if (node.type === \"FunctionDeclaration\") {\n const name = (node as any).id?.name as string | undefined;\n if (!name || !/^[A-Z]/.test(name)) return false;\n return findReturnsJSX(node);\n }\n if (node.type === \"VariableDeclaration\") {\n const decl = (node as any).declarations?.[0];\n const name = decl?.id?.name as string | undefined;\n const init = decl?.init as Node | undefined;\n if (!name || !/^[A-Z]/.test(name) || !init) return false;\n if (\n init.type === \"ArrowFunctionExpression\" ||\n init.type === \"FunctionExpression\"\n ) {\n return findReturnsJSX(init);\n }\n }\n return false;\n}\n\nexport function findReturnsJSX(node: Node): boolean {\n let found = false;\n walk(node, (n) => {\n if (\n n.type === \"JSXElement\" ||\n n.type === \"JSXFragment\" ||\n n.type === \"JSXSelfClosingElement\"\n ) {\n found = true;\n return false;\n }\n return true;\n });\n return found;\n}\n\nexport function walk(\n node: Node,\n visit: (n: Node) => boolean | void,\n seen: WeakSet<Node> = new WeakSet(),\n): void {\n if (seen.has(node)) return;\n seen.add(node);\n const cont = visit(node);\n if (cont === false) return;\n for (const key in node) {\n if (key === \"parent\") continue;\n const val = (node as any)[key];\n if (Array.isArray(val)) {\n for (const child of val) {\n if (child && typeof child === \"object\" && \"type\" in child) {\n walk(child as Node, visit, seen);\n }\n }\n } else if (val && typeof val === \"object\" && \"type\" in val) {\n walk(val as Node, visit, seen);\n }\n }\n}\n","import { walk } from \"../ast-helpers.ts\";\n\ntype Node = { type: string; [k: string]: unknown };\n\nexport interface EffectScore {\n deps: number;\n branches: number;\n setStateCount: number;\n nestedEffects: number;\n hasCleanup: boolean;\n hasSubscriptionLike: boolean;\n total: number;\n}\n\nconst SET_STATE_RE = /^set[A-Z]/;\nconst BRANCH_TYPES = new Set([\n \"IfStatement\",\n \"ConditionalExpression\",\n \"SwitchCase\",\n \"LogicalExpression\",\n]);\n\nexport function scoreEffect(node: Node): EffectScore {\n const args = (node as any).arguments as Node[];\n const fn = args?.[0] as Node | undefined;\n const depsArr = args?.[1] as any;\n\n const deps = Array.isArray(depsArr?.elements) ? depsArr.elements.length : 0;\n let branches = 0;\n let setStateCount = 0;\n let nestedEffects = 0;\n let hasCleanup = false;\n let hasSubscriptionLike = false;\n\n if (fn) {\n const body = (fn as any).body as Node;\n if (body?.type === \"BlockStatement\") {\n for (const stmt of (body as any).body as Node[]) {\n if (stmt.type === \"ReturnStatement\") {\n const arg = (stmt as any).argument as Node | undefined;\n if (\n arg &&\n (arg.type === \"ArrowFunctionExpression\" ||\n arg.type === \"FunctionExpression\")\n ) hasCleanup = true;\n }\n }\n }\n\n walk(fn, (n) => {\n if (BRANCH_TYPES.has(n.type)) branches++;\n if (n.type === \"CallExpression\") {\n const callee = (n as any).callee as Node;\n if (callee?.type === \"Identifier\") {\n const name = (callee as any).name as string;\n if (SET_STATE_RE.test(name)) setStateCount++;\n if (name === \"useEffect\" && n !== node) nestedEffects++;\n if (\n name === \"addEventListener\" ||\n name === \"subscribe\" ||\n name === \"setInterval\" ||\n name === \"setTimeout\"\n ) hasSubscriptionLike = true;\n }\n if (callee?.type === \"MemberExpression\") {\n const prop = (callee as any).property as Node;\n if (prop?.type === \"Identifier\") {\n const name = (prop as any).name as string;\n if (\n name === \"addEventListener\" ||\n name === \"subscribe\" ||\n name === \"on\"\n ) hasSubscriptionLike = true;\n }\n }\n }\n return true;\n });\n }\n\n const cleanupPenalty = hasSubscriptionLike && !hasCleanup ? 3 : 0;\n const total = deps + branches * 2 + setStateCount * 1.5 +\n nestedEffects * 5 + cleanupPenalty;\n\n return {\n deps,\n branches,\n setStateCount,\n nestedEffects,\n hasCleanup,\n hasSubscriptionLike,\n total,\n };\n}\n","export interface Thresholds {\n fatEffect: { warn: number; error: number };\n stateScatter: { warn: number; error: number };\n hookCoupling: { warn: number; error: number };\n customHookDepth: { warn: number; error: number };\n}\n\nexport const DEFAULT_THRESHOLDS: Thresholds = {\n fatEffect: { warn: 10, error: 20 },\n stateScatter: { warn: 5, error: 8 },\n hookCoupling: { warn: 3, error: 6 },\n customHookDepth: { warn: 3, error: 5 },\n};\n","import { scoreEffect } from \"../scoring/effect-score.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { isHookCall } from \"../ast-helpers.ts\";\n\ninterface Options { threshold?: number; errorThreshold?: number }\n\nexport interface RuleContext {\n options: unknown[];\n filename?: string;\n cwd?: string;\n report: (d: {\n message: string;\n node: unknown;\n severity?: \"warn\" | \"error\";\n }) => void;\n}\n\nexport const noFatEffects = {\n meta: {\n type: \"suggestion\" as const,\n docs: { description: \"Flag dense useEffect blocks\" },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.fatEffect.warn;\n const errorThreshold = opts.errorThreshold ??\n DEFAULT_THRESHOLDS.fatEffect.error;\n return {\n CallExpression(node: any) {\n if (!isHookCall(node, \"useEffect\")) return;\n const score = scoreEffect(node);\n if (score.total >= threshold) {\n const breakdown = `deps=${score.deps} branches=${score.branches} ` +\n `setStates=${score.setStateCount} nested=${score.nestedEffects}` +\n (score.hasSubscriptionLike && !score.hasCleanup\n ? \" missing-cleanup\"\n : \"\");\n const severity = score.total >= errorThreshold ? \"error\" : \"warn\";\n context.report({\n message:\n `useEffect entropy ${score.total.toFixed(1)} ≥ ${threshold} (${breakdown})`,\n node,\n severity,\n });\n }\n },\n };\n },\n};\n","import { isHookCall, walk } from \"../ast-helpers.ts\";\n\ntype Node = { type: string; [k: string]: unknown };\n\nexport interface StateScore {\n useStateCount: number;\n correlatedSetters: number;\n total: number;\n}\n\nexport function scoreComponentState(componentNode: Node): StateScore {\n const setterNames = new Set<string>();\n let useStateCount = 0;\n\n // First pass: collect useState setters\n walk(componentNode, (n) => {\n if (!n) return true;\n if (n.type === \"VariableDeclarator\") {\n const init = (n as any).init as Node | undefined;\n const id = (n as any).id as Node | undefined;\n if (\n init?.type === \"CallExpression\" &&\n isHookCall(init, \"useState\") &&\n id?.type === \"ArrayPattern\"\n ) {\n useStateCount++;\n const els = (id as any).elements as Node[];\n const setter = els?.[1];\n if (setter?.type === \"Identifier\") {\n setterNames.add((setter as any).name as string);\n }\n }\n }\n return true;\n });\n\n // Second pass: count correlated setters\n let correlatedSetters = 0;\n walk(componentNode, (n) => {\n if (!n) return true;\n if (\n n.type === \"FunctionDeclaration\" ||\n n.type === \"FunctionExpression\" ||\n n.type === \"ArrowFunctionExpression\"\n ) {\n const calledSetters = new Set<string>();\n walk(n, (m) => {\n if (!m) return true;\n if (m.type === \"CallExpression\") {\n const callee = (m as any).callee as Node;\n if (callee?.type === \"Identifier\") {\n const name = (callee as any).name as string;\n if (setterNames.has(name)) calledSetters.add(name);\n }\n }\n return true;\n });\n if (calledSetters.size >= 2) correlatedSetters += calledSetters.size;\n }\n return true;\n });\n\n const total = useStateCount + correlatedSetters * 0.5;\n return { useStateCount, correlatedSetters, total };\n}\n","import { scoreComponentState } from \"../scoring/state-score.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { isReactComponent } from \"../ast-helpers.ts\";\nimport type { RuleContext } from \"./no-fat-effects.ts\";\n\ninterface Options { threshold?: number; errorThreshold?: number }\n\nexport const stateScatter = {\n meta: {\n type: \"suggestion\" as const,\n docs: { description: \"Flag components with too many useState calls\" },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.stateScatter.warn;\n const errorThreshold = opts.errorThreshold ??\n DEFAULT_THRESHOLDS.stateScatter.error;\n function check(node: any) {\n if (!isReactComponent(node)) return;\n const s = scoreComponentState(node);\n if (s.total >= threshold) {\n const severity = s.total >= errorThreshold ? \"error\" : \"warn\";\n context.report({\n message:\n `state scatter ${s.total} ≥ ${threshold} (useStates=${s.useStateCount}, correlated setters=${s.correlatedSetters}). Consider useReducer.`,\n node,\n severity,\n });\n }\n }\n return {\n FunctionDeclaration: check,\n VariableDeclaration: check,\n };\n },\n};\n","import { isHookCall, walk } from \"../ast-helpers.ts\";\n\ntype Node = { type: string; [k: string]: unknown };\n\nexport interface CouplingScore {\n total: number;\n readWriteSame: Array<{ state: string; effect: Node }>;\n}\n\nexport function scoreCoupling(componentNode: Node): CouplingScore {\n const stateBySetter = new Map<string, string>();\n walk(componentNode, (n) => {\n if (n.type === \"VariableDeclarator\") {\n const init = (n as any).init as Node | undefined;\n const id = (n as any).id as Node | undefined;\n if (\n init?.type === \"CallExpression\" &&\n isHookCall(init, \"useState\") &&\n id?.type === \"ArrayPattern\"\n ) {\n const els = (id as any).elements as Node[];\n const stateId = els?.[0];\n const setterId = els?.[1];\n if (stateId?.type === \"Identifier\" && setterId?.type === \"Identifier\") {\n stateBySetter.set(\n (setterId as any).name as string,\n (stateId as any).name as string,\n );\n }\n }\n }\n return true;\n });\n\n const readWriteSame: Array<{ state: string; effect: Node }> = [];\n let total = 0;\n\n walk(componentNode, (n) => {\n if (n.type === \"CallExpression\" && isHookCall(n, \"useEffect\")) {\n const effectFn = ((n as any).arguments as Node[])?.[0];\n if (!effectFn) return true;\n\n const stateRefs = new Set<string>();\n const stateWrites = new Set<string>();\n const stateNames = new Set(stateBySetter.values());\n\n walk(effectFn, (m) => {\n if (m.type === \"Identifier\") {\n const name = (m as any).name as string;\n if (stateNames.has(name)) stateRefs.add(name);\n }\n if (m.type === \"CallExpression\") {\n const callee = (m as any).callee as Node;\n if (callee?.type === \"Identifier\") {\n const setter = (callee as any).name as string;\n const stateName = stateBySetter.get(setter);\n if (stateName) stateWrites.add(stateName);\n }\n }\n return true;\n });\n\n for (const written of stateWrites) {\n if (stateRefs.has(written)) {\n readWriteSame.push({ state: written, effect: n });\n total += 3;\n }\n }\n }\n return true;\n });\n\n return { total, readWriteSame };\n}\n","import { scoreCoupling } from \"../scoring/coupling-score.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { isReactComponent } from \"../ast-helpers.ts\";\nimport type { RuleContext } from \"./no-fat-effects.ts\";\n\ninterface Options { threshold?: number; errorThreshold?: number }\n\nexport const hookCoupling = {\n meta: {\n type: \"problem\" as const,\n docs: {\n description: \"Flag effects that read state they also write (loop bait)\",\n },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.hookCoupling.warn;\n const errorThreshold = opts.errorThreshold ??\n DEFAULT_THRESHOLDS.hookCoupling.error;\n function check(node: any) {\n if (!isReactComponent(node)) return;\n const s = scoreCoupling(node);\n if (s.total < threshold) return;\n const severity = s.total >= errorThreshold ? \"error\" : \"warn\";\n for (const v of s.readWriteSame) {\n context.report({\n message:\n `useEffect reads + writes same state '${v.state}' (loop risk)`,\n node: v.effect,\n severity,\n });\n }\n }\n return { FunctionDeclaration: check, VariableDeclaration: check };\n },\n};\n","import ts from \"typescript\";\n\nexport class TsProgramCache {\n private program: ts.Program | null = null;\n private rootDir: string;\n\n constructor(rootDir: string) {\n this.rootDir = rootDir;\n }\n\n private getProgram(): ts.Program {\n if (this.program) return this.program;\n const configPath = ts.findConfigFile(\n this.rootDir,\n ts.sys.fileExists,\n \"tsconfig.json\",\n );\n let compilerOptions: ts.CompilerOptions = {\n target: ts.ScriptTarget.ESNext,\n module: ts.ModuleKind.ESNext,\n jsx: ts.JsxEmit.Preserve,\n moduleResolution: ts.ModuleResolutionKind.Bundler,\n allowJs: true,\n noEmit: true,\n skipLibCheck: true,\n strict: false,\n };\n let fileNames: string[] = [];\n if (configPath) {\n const cfg = ts.readConfigFile(configPath, ts.sys.readFile);\n const configDir = configPath.slice(0, configPath.lastIndexOf(\"/\"));\n const parsed = ts.parseJsonConfigFileContent(\n cfg.config,\n ts.sys,\n configDir,\n );\n compilerOptions = { ...compilerOptions, ...parsed.options };\n fileNames = parsed.fileNames;\n }\n this.program = ts.createProgram(fileNames, compilerOptions);\n return this.program;\n }\n\n resolveIdentifierDeclaration(\n filePath: string,\n identifier: string,\n ): ts.Declaration | null {\n const program = this.getProgram();\n const checker = program.getTypeChecker();\n const absolute = filePath.startsWith(\"/\")\n ? filePath\n : `${this.rootDir.replace(/\\/$/, \"\")}/${filePath}`;\n const sourceFile = program.getSourceFile(absolute) ??\n program.getSourceFile(filePath);\n if (!sourceFile) return null;\n\n let target: ts.Node | null = null;\n function find(node: ts.Node) {\n if (target) return;\n if (ts.isIdentifier(node) && node.text === identifier) {\n target = node;\n return;\n }\n ts.forEachChild(node, find);\n }\n find(sourceFile);\n if (!target) return null;\n\n const symbol = checker.getSymbolAtLocation(target);\n if (!symbol) return null;\n const aliased = symbol.flags & ts.SymbolFlags.Alias\n ? checker.getAliasedSymbol(symbol)\n : symbol;\n return aliased.declarations?.[0] ?? null;\n }\n\n countTransitiveHookCalls(\n decl: ts.Declaration,\n depth = 0,\n seen = new Set<ts.Declaration>(),\n ): number {\n if (depth > 10 || seen.has(decl)) return depth;\n seen.add(decl);\n const program = this.getProgram();\n const checker = program.getTypeChecker();\n let maxDepth = depth;\n\n const visit = (node: ts.Node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isIdentifier(node.expression) &&\n /^use[A-Z]/.test(node.expression.text)\n ) {\n const sym = checker.getSymbolAtLocation(node.expression);\n if (sym) {\n const aliased = sym.flags & ts.SymbolFlags.Alias\n ? checker.getAliasedSymbol(sym)\n : sym;\n const innerDecl = aliased.declarations?.[0];\n if (innerDecl) {\n const sf = innerDecl.getSourceFile();\n if (\n sf.fileName.includes(\"node_modules/@types/react\") ||\n sf.fileName.includes(\"node_modules/react/\")\n ) {\n maxDepth = Math.max(maxDepth, depth + 1);\n } else {\n const childDepth = this.countTransitiveHookCalls(\n innerDecl,\n depth + 1,\n seen,\n );\n maxDepth = Math.max(maxDepth, childDepth);\n }\n }\n }\n }\n ts.forEachChild(node, visit);\n };\n\n visit(decl);\n return maxDepth;\n }\n}\n","import { TsProgramCache } from \"../ts-program.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { getHookName } from \"../ast-helpers.ts\";\nimport type { RuleContext } from \"./no-fat-effects.ts\";\n\ninterface Options { maxDepth?: number; errorMaxDepth?: number }\n\nconst REACT_HOOKS = new Set([\n \"useState\", \"useEffect\", \"useLayoutEffect\", \"useMemo\", \"useCallback\",\n \"useReducer\", \"useContext\", \"useRef\", \"useImperativeHandle\",\n \"useDebugValue\", \"useId\", \"useTransition\", \"useDeferredValue\",\n \"useSyncExternalStore\", \"useInsertionEffect\",\n]);\n\nlet sharedCache: TsProgramCache | null = null;\n\nexport const customHookDepth = {\n meta: {\n type: \"suggestion\" as const,\n docs: {\n description:\n \"Flag custom hooks whose transitive nesting exceeds maxDepth (type-aware).\",\n },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const maxDepth = opts.maxDepth ?? DEFAULT_THRESHOLDS.customHookDepth.warn;\n const errorMaxDepth = opts.errorMaxDepth ??\n DEFAULT_THRESHOLDS.customHookDepth.error;\n const g = globalThis as { Deno?: { cwd(): string }; process?: { cwd(): string } };\n const cwd = context.cwd ?? g.process?.cwd() ?? g.Deno?.cwd() ?? \".\";\n sharedCache ??= new TsProgramCache(cwd);\n const cache = sharedCache;\n const filename = context.filename;\n\n return {\n CallExpression(node: any) {\n const name = getHookName(node);\n if (!name || REACT_HOOKS.has(name)) return;\n if (!filename) return;\n const decl = cache.resolveIdentifierDeclaration(filename, name);\n if (!decl) return;\n const depth = cache.countTransitiveHookCalls(decl);\n if (depth >= maxDepth) {\n const severity = depth >= errorMaxDepth ? \"error\" : \"warn\";\n context.report({\n message:\n `custom hook '${name}' transitive depth ${depth} ≥ ${maxDepth}`,\n node,\n severity,\n });\n }\n },\n };\n },\n};\n","import type { Rule } from \"@oxlint/plugins\";\nimport { noFatEffects } from \"./no-fat-effects.ts\";\nimport { stateScatter } from \"./state-scatter.ts\";\nimport { hookCoupling } from \"./hook-coupling.ts\";\nimport { customHookDepth } from \"./custom-hook-depth.ts\";\n\n// Rules use our internal ESLint-compatible RuleContext shape; eslintCompatPlugin\n// adapts them to oxlint's stricter Rule contract at runtime.\nexport const ALL_RULES: Record<string, Rule> = {\n \"no-fat-effects\": noFatEffects as unknown as Rule,\n \"state-scatter\": stateScatter as unknown as Rule,\n \"hook-coupling\": hookCoupling as unknown as Rule,\n \"custom-hook-depth\": customHookDepth as unknown as Rule,\n};\n\nexport type RuleId =\n | \"no-fat-effects\"\n | \"state-scatter\"\n | \"hook-coupling\"\n | \"custom-hook-depth\";\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hook-o-gnese",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Score React hook complexity. Runs as oxlint plugin or standalone CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"hook-o-gnese": "./dist/cli.mjs"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"types": "./dist/index.d.mts"
|
|
15
|
+
},
|
|
16
|
+
"./engine": {
|
|
17
|
+
"import": "./dist/engine.mjs",
|
|
18
|
+
"types": "./dist/engine.d.mts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
22
|
+
"keywords": ["oxlint", "oxlint-plugin", "react", "hooks", "complexity", "lint", "cli"],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Michal Rehout",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/rehoutm/spaghetti-hook-o-gnese.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/rehoutm/spaghetti-hook-o-gnese/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/rehoutm/spaghetti-hook-o-gnese#readme",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"oxlint": ">=1.63.0",
|
|
38
|
+
"typescript": ">=6.0.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"oxlint": { "optional": true }
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"globby": "^16.2.0",
|
|
45
|
+
"oxc-parser": "^0.129.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"tsdown": "^0.21.10",
|
|
49
|
+
"typescript": "^6.0.3",
|
|
50
|
+
"@oxlint/plugins": "^1.63.0"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20.18.0"
|
|
54
|
+
}
|
|
55
|
+
}
|