@shriyanss/js-recon 1.3.1-alpha.2 → 1.3.1-alpha.3

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 (63) hide show
  1. package/.github/workflows/pr_checker.yml +9 -8
  2. package/CHANGELOG.md +20 -0
  3. package/CLAUDE.md +136 -0
  4. package/build/analyze/engine/index.js +2 -2
  5. package/build/analyze/engine/index.js.map +1 -1
  6. package/build/analyze/helpers/schemas.js +2 -1
  7. package/build/analyze/helpers/schemas.js.map +1 -1
  8. package/build/analyze/helpers/validate.js +49 -9
  9. package/build/analyze/helpers/validate.js.map +1 -1
  10. package/build/analyze/index.js +3 -2
  11. package/build/analyze/index.js.map +1 -1
  12. package/build/globalConfig.js +1 -1
  13. package/build/index.js +21 -1
  14. package/build/index.js.map +1 -1
  15. package/build/lazyLoad/downloadFilesUtil.js +3 -3
  16. package/build/lazyLoad/downloadFilesUtil.js.map +1 -1
  17. package/build/lazyLoad/downloadQueue.js +7 -3
  18. package/build/lazyLoad/downloadQueue.js.map +1 -1
  19. package/build/lazyLoad/next_js/next_GetJSScript.js +8 -4
  20. package/build/lazyLoad/next_js/next_GetJSScript.js.map +1 -1
  21. package/build/lazyLoad/techDetect/index.js +22 -20
  22. package/build/lazyLoad/techDetect/index.js.map +1 -1
  23. package/build/load/index.js +316 -0
  24. package/build/load/index.js.map +1 -0
  25. package/build/map/index.js +11 -5
  26. package/build/map/index.js.map +1 -1
  27. package/build/map/next_js/interactive.js +30 -0
  28. package/build/map/next_js/interactive.js.map +1 -1
  29. package/build/map/next_js/interactive_helpers/commandHandler.js +22 -0
  30. package/build/map/next_js/interactive_helpers/commandHandler.js.map +1 -1
  31. package/build/map/next_js/interactive_helpers/esqueryGen.js +370 -0
  32. package/build/map/next_js/interactive_helpers/esqueryGen.js.map +1 -0
  33. package/build/map/next_js/interactive_helpers/helpMenu.js +1 -0
  34. package/build/map/next_js/interactive_helpers/helpMenu.js.map +1 -1
  35. package/build/map/next_js/interactive_helpers/inputPatch.js +207 -0
  36. package/build/map/next_js/interactive_helpers/inputPatch.js.map +1 -0
  37. package/build/map/next_js/interactive_helpers/ui.js +0 -1
  38. package/build/map/next_js/interactive_helpers/ui.js.map +1 -1
  39. package/build/map/next_js/utils.js +88 -2
  40. package/build/map/next_js/utils.js.map +1 -1
  41. package/build/map/vue_js/getViteConnections.js +27 -3
  42. package/build/map/vue_js/getViteConnections.js.map +1 -1
  43. package/build/map/vue_js/interactive.js +28 -0
  44. package/build/map/vue_js/interactive.js.map +1 -1
  45. package/build/map/vue_js/interactive_helpers/commandHandler.js +22 -0
  46. package/build/map/vue_js/interactive_helpers/commandHandler.js.map +1 -1
  47. package/build/map/vue_js/interactive_helpers/helpMenu.js +1 -0
  48. package/build/map/vue_js/interactive_helpers/helpMenu.js.map +1 -1
  49. package/build/map/vue_js/vue_resolveFetch.js +588 -21
  50. package/build/map/vue_js/vue_resolveFetch.js.map +1 -1
  51. package/build/report/utility/populateDb/populateAnalysisFindings.js +1 -1
  52. package/build/report/utility/populateDb/populateAnalysisFindings.js.map +1 -1
  53. package/build/run/index.js +22 -5
  54. package/build/run/index.js.map +1 -1
  55. package/build/utility/globals.js +9 -0
  56. package/build/utility/globals.js.map +1 -1
  57. package/build/utility/makeReq.js +25 -3
  58. package/build/utility/makeReq.js.map +1 -1
  59. package/build/utility/openapiGenerator.js +15 -3
  60. package/build/utility/openapiGenerator.js.map +1 -1
  61. package/build/utility/postmanGenerator.js +16 -12
  62. package/build/utility/postmanGenerator.js.map +1 -1
  63. package/package.json +1 -1
@@ -12,9 +12,387 @@ import parser from "@babel/parser";
12
12
  import _traverse from "@babel/traverse";
13
13
  import fs from "fs";
14
14
  import path from "path";
15
- import { resolveNodeValue, substituteVariablesInString } from "../next_js/utils.js";
15
+ import { resolveNodeValue, substituteVariablesInString, memberChainToString } from "../next_js/utils.js";
16
16
  import * as globals from "../../utility/globals.js";
17
17
  const traverse = _traverse.default;
18
+ /**
19
+ * Checks whether invoking the given function body would directly execute a
20
+ * `fetch(...)` call (as a bare Identifier callee). Used to recognise functions
21
+ * that wrap the native fetch API so destructured aliases of those wrappers can
22
+ * be resolved as fetch calls.
23
+ *
24
+ * The walk stops at nested function boundaries so a factory function that
25
+ * RETURNS a fetch-wrapping arrow isn't itself classified as a wrapper — only
26
+ * the arrow it returns is.
27
+ */
28
+ const NESTED_FN_TYPES = new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
29
+ /**
30
+ * A function is treated as a "transparent" fetch wrapper only if its first
31
+ * argument is forwarded as-is to fetch as the URL. Wrappers that construct the
32
+ * URL from a property of their input object have a different calling convention
33
+ * than `fetch(url, options)` — treating callsites of those as fetch calls
34
+ * produces garbage URLs. So we require that fetch's first arg is the function's
35
+ * first param (an Identifier match).
36
+ */
37
+ const bodyCallsFetch = (functionNode) => {
38
+ var _a;
39
+ if (!functionNode || typeof functionNode !== "object")
40
+ return false;
41
+ const params = Array.isArray(functionNode.params) ? functionNode.params : [];
42
+ const firstParamName = params[0] && params[0].type === "Identifier"
43
+ ? params[0].name
44
+ : params[0] && params[0].type === "AssignmentPattern" && ((_a = params[0].left) === null || _a === void 0 ? void 0 : _a.type) === "Identifier"
45
+ ? params[0].left.name
46
+ : null;
47
+ if (!firstParamName)
48
+ return false;
49
+ let found = false;
50
+ const visit = (n, depth) => {
51
+ if (found || !n || typeof n !== "object")
52
+ return;
53
+ if (Array.isArray(n)) {
54
+ for (const child of n)
55
+ visit(child, depth);
56
+ return;
57
+ }
58
+ if (typeof n.type !== "string")
59
+ return;
60
+ // Don't descend into nested functions — they have their own invocation context.
61
+ if (depth > 0 && NESTED_FN_TYPES.has(n.type))
62
+ return;
63
+ if (n.type === "CallExpression" &&
64
+ n.callee &&
65
+ n.callee.type === "Identifier" &&
66
+ n.callee.name === "fetch" &&
67
+ Array.isArray(n.arguments) &&
68
+ n.arguments.length > 0 &&
69
+ n.arguments[0].type === "Identifier" &&
70
+ n.arguments[0].name === firstParamName) {
71
+ found = true;
72
+ return;
73
+ }
74
+ for (const key of Object.keys(n)) {
75
+ if (key === "loc" ||
76
+ key === "start" ||
77
+ key === "end" ||
78
+ key === "leadingComments" ||
79
+ key === "trailingComments") {
80
+ continue;
81
+ }
82
+ visit(n[key], depth + 1);
83
+ }
84
+ };
85
+ visit(functionNode, 0);
86
+ return found;
87
+ };
88
+ const isFunctionLike = (node) => !!node &&
89
+ (node.type === "ArrowFunctionExpression" ||
90
+ node.type === "FunctionExpression" ||
91
+ node.type === "FunctionDeclaration");
92
+ /**
93
+ * Pulls out the function binding name from the surrounding declarator /
94
+ * assignment so we can later match callsites by identifier. Handles four
95
+ * common shapes seen in Vite-bundled code:
96
+ * const fn = (arg) => { ... }
97
+ * fn = (arg) => { ... }
98
+ * function fn(arg) { ... }
99
+ * { fn: (arg) => { ... } } // object-literal property value
100
+ */
101
+ const inferEnclosingFn = (callPath, file) => {
102
+ var _a, _b, _c, _d, _e, _f, _g, _h;
103
+ const fnPath = callPath.getFunctionParent();
104
+ if (!fnPath)
105
+ return null;
106
+ const fnNode = fnPath.node;
107
+ const firstParamName = fnNode.params && ((_a = fnNode.params[0]) === null || _a === void 0 ? void 0 : _a.type) === "Identifier"
108
+ ? fnNode.params[0].name
109
+ : fnNode.params &&
110
+ ((_b = fnNode.params[0]) === null || _b === void 0 ? void 0 : _b.type) === "AssignmentPattern" &&
111
+ ((_c = fnNode.params[0].left) === null || _c === void 0 ? void 0 : _c.type) === "Identifier"
112
+ ? fnNode.params[0].left.name
113
+ : null;
114
+ let bindingName = null;
115
+ if (fnNode.type === "FunctionDeclaration" && ((_d = fnNode.id) === null || _d === void 0 ? void 0 : _d.type) === "Identifier") {
116
+ bindingName = fnNode.id.name;
117
+ }
118
+ else {
119
+ const parentNode = (_e = fnPath.parentPath) === null || _e === void 0 ? void 0 : _e.node;
120
+ if (parentNode) {
121
+ if (parentNode.type === "VariableDeclarator" && ((_f = parentNode.id) === null || _f === void 0 ? void 0 : _f.type) === "Identifier") {
122
+ bindingName = parentNode.id.name;
123
+ }
124
+ else if (parentNode.type === "AssignmentExpression" && ((_g = parentNode.left) === null || _g === void 0 ? void 0 : _g.type) === "Identifier") {
125
+ bindingName = parentNode.left.name;
126
+ }
127
+ else if (parentNode.type === "ObjectProperty" &&
128
+ !parentNode.computed &&
129
+ ((_h = parentNode.key) === null || _h === void 0 ? void 0 : _h.type) === "Identifier") {
130
+ bindingName = parentNode.key.name;
131
+ }
132
+ }
133
+ }
134
+ return { bindingName, firstParamName, node: fnNode, file };
135
+ };
136
+ /**
137
+ * Walks an ObjectExpression and returns the property value for the given
138
+ * dotted property path (e.g. ["data"] -> the value node of `data: ...`).
139
+ * Returns null if any segment of the path isn't a literal property.
140
+ */
141
+ const lookupObjectExpressionProp = (objExpr, propPath) => {
142
+ if (!objExpr || objExpr.type !== "ObjectExpression")
143
+ return null;
144
+ let current = objExpr;
145
+ for (const segment of propPath) {
146
+ if (!current || current.type !== "ObjectExpression")
147
+ return null;
148
+ let next = null;
149
+ for (const prop of current.properties) {
150
+ if (prop.type !== "ObjectProperty")
151
+ continue;
152
+ const key = prop.key.type === "Identifier"
153
+ ? prop.key.name
154
+ : prop.key.type === "StringLiteral"
155
+ ? prop.key.value
156
+ : null;
157
+ if (key === segment) {
158
+ next = prop.value;
159
+ break;
160
+ }
161
+ }
162
+ if (!next)
163
+ return null;
164
+ current = next;
165
+ }
166
+ return current;
167
+ };
168
+ /**
169
+ * Resolves the value of `paramName.propPath` by walking back through the
170
+ * enclosing function's callers. If a caller's argument is itself an Identifier
171
+ * pointing to a constant initializer, we follow that binding; if it's the
172
+ * caller's own first parameter, we recurse to that function's callers.
173
+ *
174
+ * Returns the resolved AST node + the scope it lives in (so further sub-property
175
+ * resolution can use the correct binding lookup), or null when the chain breaks.
176
+ */
177
+ const resolveParamProperty = (paramName, propPath, enclosingFn, getCallers, depth = 0) => {
178
+ var _a, _b, _c;
179
+ if (!enclosingFn || !enclosingFn.bindingName || depth > 6)
180
+ return null;
181
+ // Only the function's first parameter is supported for now — every observed
182
+ // case has fetch wrappers shaped as `(e) => fetch(... e.X ...)`.
183
+ if (enclosingFn.firstParamName !== paramName)
184
+ return null;
185
+ const callers = getCallers(enclosingFn.bindingName);
186
+ if (!callers || callers.length === 0)
187
+ return null;
188
+ for (const caller of callers) {
189
+ const arg = caller.args[0];
190
+ if (!arg)
191
+ continue;
192
+ // Case 1: caller passes an object literal directly — fn({ a: ..., b: ... })
193
+ if (arg.type === "ObjectExpression") {
194
+ const node = lookupObjectExpressionProp(arg, propPath);
195
+ if (node)
196
+ return { node, scope: caller.scope, fileContent: caller.fileContent };
197
+ }
198
+ // Case 2: caller passes an Identifier pointing to a const-initialized
199
+ // object literal — `const x = { a: ... }; fn(x)`.
200
+ if (arg.type === "Identifier") {
201
+ const binding = caller.scope.getBinding(arg.name);
202
+ const initNode = (_b = (_a = binding === null || binding === void 0 ? void 0 : binding.path) === null || _a === void 0 ? void 0 : _a.node) === null || _b === void 0 ? void 0 : _b.init;
203
+ if (initNode && initNode.type === "ObjectExpression") {
204
+ const node = lookupObjectExpressionProp(initNode, propPath);
205
+ if (node)
206
+ return { node, scope: caller.scope, fileContent: caller.fileContent };
207
+ }
208
+ // Case 3: caller passes its own first param straight through —
209
+ // `outer = (p) => fn(p)`. Recurse up to that function's callers.
210
+ if (((_c = caller.enclosingFn) === null || _c === void 0 ? void 0 : _c.firstParamName) === arg.name) {
211
+ const result = resolveParamProperty(arg.name, propPath, caller.enclosingFn, getCallers, depth + 1);
212
+ if (result)
213
+ return result;
214
+ }
215
+ }
216
+ }
217
+ return null;
218
+ };
219
+ /**
220
+ * Renders an ObjectExpression as a `k1={k1}&k2={k2}` query-string fragment.
221
+ * Literal property values are URL-encoded directly; non-literal values fall
222
+ * back to the placeholder form so the schema reader can see what's missing.
223
+ */
224
+ const renderObjectAsQuery = (objExpr, scope, fileContent) => {
225
+ if (!objExpr || objExpr.type !== "ObjectExpression")
226
+ return null;
227
+ const parts = [];
228
+ for (const prop of objExpr.properties) {
229
+ if (prop.type !== "ObjectProperty")
230
+ continue;
231
+ const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : null;
232
+ if (!key)
233
+ continue;
234
+ let valStr;
235
+ try {
236
+ const resolved = resolveNodeValue(prop.value, scope, "", "fetch", fileContent);
237
+ if (resolved !== null && resolved !== undefined && !String(resolved).startsWith("[")) {
238
+ valStr = encodeURIComponent(String(resolved));
239
+ }
240
+ else {
241
+ valStr = `{${key}}`;
242
+ }
243
+ }
244
+ catch (_a) {
245
+ valStr = `{${key}}`;
246
+ }
247
+ parts.push(`${key}=${valStr}`);
248
+ }
249
+ return parts.length > 0 ? parts.join("&") : null;
250
+ };
251
+ /**
252
+ * Returns a plain object derived from an ObjectExpression's properties.
253
+ * Spread elements are merged in when they themselves resolve to a literal
254
+ * object; otherwise the spread is preserved as a sentinel key so the schema
255
+ * reader knows extra fields exist at runtime.
256
+ */
257
+ const renderObjectExpression = (objExpr, scope, fileContent) => {
258
+ if (!objExpr || objExpr.type !== "ObjectExpression")
259
+ return null;
260
+ const out = {};
261
+ for (const prop of objExpr.properties) {
262
+ if (prop.type === "ObjectProperty") {
263
+ const key = prop.key.type === "Identifier"
264
+ ? prop.key.name
265
+ : prop.key.type === "StringLiteral"
266
+ ? prop.key.value
267
+ : null;
268
+ if (!key)
269
+ continue;
270
+ try {
271
+ const resolved = resolveNodeValue(prop.value, scope, "", "fetch", fileContent);
272
+ out[key] = resolved === undefined || resolved === null ? "" : String(resolved);
273
+ }
274
+ catch (_a) {
275
+ out[key] = "";
276
+ }
277
+ }
278
+ else if (prop.type === "SpreadElement") {
279
+ try {
280
+ const resolved = resolveNodeValue(prop.argument, scope, "", "fetch", fileContent);
281
+ if (resolved && typeof resolved === "object") {
282
+ for (const [k, v] of Object.entries(resolved)) {
283
+ if (!(k in out))
284
+ out[k] = v === undefined || v === null ? "" : String(v);
285
+ }
286
+ }
287
+ else {
288
+ const chain = memberChainToString(prop.argument);
289
+ out[`...${chain !== null && chain !== void 0 ? chain : "spread"}`] = "<spread>";
290
+ }
291
+ }
292
+ catch (_b) {
293
+ const chain = memberChainToString(prop.argument);
294
+ out[`...${chain !== null && chain !== void 0 ? chain : "spread"}`] = "<spread>";
295
+ }
296
+ }
297
+ }
298
+ return out;
299
+ };
300
+ /**
301
+ * Tries to convert a value node into a printable string, transparently
302
+ * unwrapping template literals built from caller-side bindings. Returns null
303
+ * when the value is itself a Vue-style ref / member expression that we can't
304
+ * pin down statically.
305
+ */
306
+ const renderValueNode = (node, scope, fileContent) => {
307
+ if (!node)
308
+ return null;
309
+ try {
310
+ const resolved = resolveNodeValue(node, scope, "", "fetch", fileContent);
311
+ if (resolved === null || resolved === undefined)
312
+ return null;
313
+ if (typeof resolved === "object") {
314
+ // Avoid emitting `[object Object]` — leave it to the upstream
315
+ // placeholder so the consumer knows it's structured.
316
+ return null;
317
+ }
318
+ const s = String(resolved);
319
+ if (s.startsWith("[unresolved"))
320
+ return null;
321
+ return s;
322
+ }
323
+ catch (_a) {
324
+ return null;
325
+ }
326
+ };
327
+ /**
328
+ * Substitutes [member:P.X], [param:P], and [urlsearchparams:P.X] markers in a
329
+ * string by walking back to the enclosing function's caller(s). The string is
330
+ * returned with as many markers resolved as we could trace.
331
+ */
332
+ const substituteCallerPlaceholders = (input, enclosingFn, getCallers) => {
333
+ if (!input || !enclosingFn)
334
+ return input;
335
+ let output = input;
336
+ output = output.replace(/\[urlsearchparams:([A-Za-z_$][\w$.]*)\]/g, (match, chain) => {
337
+ const parts = chain.split(".");
338
+ if (parts.length < 1)
339
+ return match;
340
+ const paramName = parts[0];
341
+ const propPath = parts.slice(1);
342
+ const resolved = resolveParamProperty(paramName, propPath, enclosingFn, getCallers);
343
+ if (!resolved)
344
+ return match;
345
+ const rendered = renderObjectAsQuery(resolved.node, resolved.scope, resolved.fileContent);
346
+ return rendered !== null && rendered !== void 0 ? rendered : match;
347
+ });
348
+ output = output.replace(/\[member:([A-Za-z_$][\w$.]*)\]/g, (match, chain) => {
349
+ const parts = chain.split(".");
350
+ if (parts.length < 1)
351
+ return match;
352
+ const paramName = parts[0];
353
+ const propPath = parts.slice(1);
354
+ const resolved = resolveParamProperty(paramName, propPath, enclosingFn, getCallers);
355
+ if (!resolved)
356
+ return match;
357
+ const rendered = renderValueNode(resolved.node, resolved.scope, resolved.fileContent);
358
+ return rendered !== null && rendered !== void 0 ? rendered : match;
359
+ });
360
+ return output;
361
+ };
362
+ /**
363
+ * Substitutes placeholders in a header bag. `...P.X: <spread>` entries are
364
+ * expanded when the corresponding caller-side value is a literal object;
365
+ * `[member:P.X]` markers inside header values are substituted in-place.
366
+ */
367
+ const substituteCallerHeaders = (headers, enclosingFn, getCallers) => {
368
+ if (!enclosingFn)
369
+ return headers;
370
+ const out = {};
371
+ for (const [k, v] of Object.entries(headers)) {
372
+ if (k.startsWith("...") && v === "<spread>") {
373
+ const chain = k.slice(3);
374
+ const parts = chain.split(".");
375
+ const paramName = parts[0];
376
+ const propPath = parts.slice(1);
377
+ const resolved = resolveParamProperty(paramName, propPath, enclosingFn, getCallers);
378
+ if (resolved && resolved.node.type === "ObjectExpression") {
379
+ const obj = renderObjectExpression(resolved.node, resolved.scope, resolved.fileContent);
380
+ if (obj) {
381
+ for (const [hk, hv] of Object.entries(obj)) {
382
+ if (!(hk in out))
383
+ out[hk] = hv;
384
+ }
385
+ continue;
386
+ }
387
+ }
388
+ out[k] = v;
389
+ }
390
+ else {
391
+ out[k] = substituteCallerPlaceholders(v, enclosingFn, getCallers);
392
+ }
393
+ }
394
+ return out;
395
+ };
18
396
  /**
19
397
  * Scans all JS files in the given directory for fetch() calls,
20
398
  * resolves their URL / method / headers / body, and registers each
@@ -36,7 +414,13 @@ const vue_resolveFetch = (directory) => __awaiter(void 0, void 0, void 0, functi
36
414
  files = files
37
415
  .filter((f) => f.endsWith(".js") && !f.includes("___subsequent_requests"))
38
416
  .filter((f) => !fs.lstatSync(path.join(directory, f)).isDirectory());
39
- let totalFetchCalls = 0;
417
+ // Pre-pass: scan every file for object-literal properties whose value is a
418
+ // function that ultimately calls fetch(). Their property names are the
419
+ // fetch-wrapper keys downstream code destructures and invokes in place of
420
+ // fetch — so we need to recognise those aliases.
421
+ const wrapperKeyNames = new Set();
422
+ const fileAstCache = new Map();
423
+ const fileContentCache = new Map();
40
424
  for (const file of files) {
41
425
  const filePath = path.join(directory, file);
42
426
  let fileContent;
@@ -46,7 +430,6 @@ const vue_resolveFetch = (directory) => __awaiter(void 0, void 0, void 0, functi
46
430
  catch (_b) {
47
431
  continue;
48
432
  }
49
- // Fast path: skip files with no fetch keyword at all
50
433
  if (!fileContent.includes("fetch"))
51
434
  continue;
52
435
  let fileAst;
@@ -60,17 +443,147 @@ const vue_resolveFetch = (directory) => __awaiter(void 0, void 0, void 0, functi
60
443
  catch (_c) {
61
444
  continue;
62
445
  }
63
- // Collect fetch aliases (const x = fetch)
446
+ fileAstCache.set(filePath, fileAst);
447
+ fileContentCache.set(filePath, fileContent);
448
+ traverse(fileAst, {
449
+ ObjectProperty(p) {
450
+ const key = p.node.key;
451
+ const value = p.node.value;
452
+ if (!isFunctionLike(value))
453
+ return;
454
+ if (!bodyCallsFetch(value))
455
+ return;
456
+ const keyName = key.type === "Identifier" ? key.name : key.type === "StringLiteral" ? key.value : null;
457
+ if (keyName)
458
+ wrapperKeyNames.add(keyName);
459
+ },
460
+ });
461
+ }
462
+ // Lazy caller lookup. Rather than building a global index of every
463
+ // CallExpression in every bundle (which becomes O(huge) for minified Vite
464
+ // chunks), we only search when phase-2 actually asks for a specific binding
465
+ // name, and cache the per-name result. The fast `fileContent.includes`
466
+ // check skips files that can't possibly contain the call. When the call
467
+ // count blows past a sane budget we treat the name as too ambiguous to be
468
+ // useful for tracing and short-circuit to an empty list.
469
+ const MAX_CALLERS_PER_NAME = 64;
470
+ const callerCache = new Map();
471
+ const getCallers = (bindingName) => {
472
+ var _a;
473
+ const cached = callerCache.get(bindingName);
474
+ if (cached)
475
+ return cached;
476
+ const needle = `${bindingName}(`;
477
+ const out = [];
478
+ let overflowed = false;
479
+ for (const [filePath, fileAst] of fileAstCache.entries()) {
480
+ if (overflowed)
481
+ break;
482
+ const fileContent = (_a = fileContentCache.get(filePath)) !== null && _a !== void 0 ? _a : "";
483
+ if (!fileContent.includes(needle))
484
+ continue;
485
+ traverse(fileAst, {
486
+ CallExpression(callPath) {
487
+ if (overflowed) {
488
+ callPath.stop();
489
+ return;
490
+ }
491
+ const callee = callPath.node.callee;
492
+ if (callee.type !== "Identifier" || callee.name !== bindingName)
493
+ return;
494
+ if (out.length >= MAX_CALLERS_PER_NAME) {
495
+ overflowed = true;
496
+ callPath.stop();
497
+ return;
498
+ }
499
+ out.push({
500
+ file: filePath,
501
+ fileContent,
502
+ callNode: callPath.node,
503
+ scope: callPath.scope,
504
+ args: callPath.node.arguments,
505
+ enclosingFn: inferEnclosingFn(callPath, filePath),
506
+ });
507
+ },
508
+ });
509
+ }
510
+ // A name with this many callsites is almost certainly a minifier
511
+ // single-letter local (e, t, n, …) — tracing it is noise, not signal.
512
+ const result = overflowed ? [] : out;
513
+ callerCache.set(bindingName, result);
514
+ return result;
515
+ };
516
+ let totalFetchCalls = 0;
517
+ // Counter for per-callsite uniqueness so two callsites at the same path+method
518
+ // are kept distinct in mapped.json and downstream specs.
519
+ let callsiteCounter = 0;
520
+ const entries = [];
521
+ for (const file of files) {
522
+ const filePath = path.join(directory, file);
523
+ let fileContent = fileContentCache.get(filePath);
524
+ let fileAst = fileAstCache.get(filePath);
525
+ if (!fileContent || !fileAst) {
526
+ try {
527
+ fileContent = fs.readFileSync(filePath, "utf-8");
528
+ }
529
+ catch (_d) {
530
+ continue;
531
+ }
532
+ if (!fileContent.includes("fetch"))
533
+ continue;
534
+ try {
535
+ fileAst = parser.parse(fileContent, {
536
+ sourceType: "unambiguous",
537
+ plugins: ["jsx", "typescript"],
538
+ errorRecovery: true,
539
+ });
540
+ }
541
+ catch (_e) {
542
+ continue;
543
+ }
544
+ }
545
+ // Collect fetch aliases:
546
+ // 1. `const x = fetch` (direct identifier aliasing)
547
+ // 2. `const x = (i, l) => fetch(i, l)` (function-literal wrappers)
548
+ // 3. `const { wrapperKey: x } = factory({...})` (destructured wrapper keys)
64
549
  const fetchAliases = new Set();
65
550
  traverse(fileAst, {
66
551
  VariableDeclarator(p) {
67
552
  const { id, init } = p.node;
68
- if (id.type !== "Identifier" || !init)
553
+ if (!init)
69
554
  return;
70
- if (init.type === "Identifier" && init.name === "fetch") {
71
- const binding = p.scope.getBinding(id.name);
72
- if (binding)
73
- fetchAliases.add(binding);
555
+ if (id.type === "Identifier") {
556
+ if (init.type === "Identifier" && init.name === "fetch") {
557
+ const binding = p.scope.getBinding(id.name);
558
+ if (binding)
559
+ fetchAliases.add(binding);
560
+ return;
561
+ }
562
+ if (isFunctionLike(init) && bodyCallsFetch(init)) {
563
+ const binding = p.scope.getBinding(id.name);
564
+ if (binding)
565
+ fetchAliases.add(binding);
566
+ return;
567
+ }
568
+ }
569
+ if (id.type === "ObjectPattern") {
570
+ for (const prop of id.properties) {
571
+ if (prop.type !== "ObjectProperty")
572
+ continue;
573
+ const keyName = prop.key.type === "Identifier"
574
+ ? prop.key.name
575
+ : prop.key.type === "StringLiteral"
576
+ ? prop.key.value
577
+ : null;
578
+ if (!keyName || !wrapperKeyNames.has(keyName))
579
+ continue;
580
+ const valueName = prop.value.type === "Identifier" ? prop.value.name : null;
581
+ if (!valueName)
582
+ continue;
583
+ const binding = p.scope.getBinding(valueName);
584
+ if (binding)
585
+ fetchAliases.add(binding);
586
+ }
74
587
  }
75
588
  },
76
589
  });
@@ -108,7 +621,6 @@ const vue_resolveFetch = (directory) => __awaiter(void 0, void 0, void 0, functi
108
621
  url = substituted;
109
622
  }
110
623
  }
111
- console.log(chalk.green(` URL: ${url}`));
112
624
  let method = "GET";
113
625
  let headers = {};
114
626
  let body = "";
@@ -129,26 +641,81 @@ const vue_resolveFetch = (directory) => __awaiter(void 0, void 0, void 0, functi
129
641
  body =
130
642
  typeof options.body === "object" ? JSON.stringify(options.body) : String(options.body);
131
643
  }
132
- console.log(chalk.green(` Method: ${method}`));
133
- if (Object.keys(headers).length > 0)
134
- console.log(chalk.green(` Headers: ${JSON.stringify(headers)}`));
135
- if (body)
136
- console.log(chalk.green(` Body: ${body}`));
137
644
  }
138
645
  }
139
- globals.addOpenapiOutput({
140
- url: String(url),
646
+ const enclosingFn = inferEnclosingFn(callPath, filePath);
647
+ entries.push({
648
+ file,
649
+ filePath,
650
+ fileContent,
651
+ fileLine,
652
+ url: typeof url === "string" ? url : "",
141
653
  method,
142
- path: String(url),
143
654
  headers,
144
655
  body,
145
- chunkId: file,
146
- functionFile: filePath,
147
- functionFileLine: fileLine,
656
+ enclosingFn,
148
657
  });
149
658
  },
150
659
  });
151
660
  }
661
+ // Second pass: walk back to each fetch's enclosing function callers and
662
+ // substitute markers we couldn't resolve in the first pass. This is where
663
+ // `?[urlsearchparams:p.q]` turns into `?k1={k1}&k2={k2}`, and where
664
+ // `...p.q: <spread>` gets expanded if a literal object was passed.
665
+ const MARKER_RE = /\[(urlsearchparams|member|param):/;
666
+ const headersHaveMarkers = (h) => {
667
+ for (const [k, v] of Object.entries(h)) {
668
+ if (k.startsWith("...") && v === "<spread>")
669
+ return true;
670
+ if (MARKER_RE.test(v))
671
+ return true;
672
+ }
673
+ return false;
674
+ };
675
+ for (const entry of entries) {
676
+ if (entry.enclosingFn &&
677
+ entry.enclosingFn.bindingName &&
678
+ entry.enclosingFn.firstParamName &&
679
+ (MARKER_RE.test(entry.url) || MARKER_RE.test(entry.body) || headersHaveMarkers(entry.headers))) {
680
+ entry.url = substituteCallerPlaceholders(entry.url, entry.enclosingFn, getCallers);
681
+ entry.headers = substituteCallerHeaders(entry.headers, entry.enclosingFn, getCallers);
682
+ entry.body = substituteCallerPlaceholders(entry.body, entry.enclosingFn, getCallers);
683
+ }
684
+ console.log(chalk.green(` URL: ${entry.url}`));
685
+ if (entry.method !== "GET" || Object.keys(entry.headers).length > 0 || entry.body) {
686
+ console.log(chalk.green(` Method: ${entry.method}`));
687
+ }
688
+ if (Object.keys(entry.headers).length > 0)
689
+ console.log(chalk.green(` Headers: ${JSON.stringify(entry.headers)}`));
690
+ if (entry.body)
691
+ console.log(chalk.green(` Body: ${entry.body}`));
692
+ // Skip the openapi/postman registration for callsites where the URL
693
+ // never resolved to anything URL-shaped. Examples:
694
+ // - inner wrapper bodies like `fetch(i, l)` where `i` is a param
695
+ // - non-string AST nodes (objects, member expressions) that have
696
+ // no chance of being a real path
697
+ const urlStr = entry.url;
698
+ const looksLikeUrl = urlStr.length > 0 &&
699
+ !urlStr.startsWith("[") &&
700
+ urlStr !== "[object Object]" &&
701
+ (urlStr.startsWith("http://") ||
702
+ urlStr.startsWith("https://") ||
703
+ urlStr.startsWith("/") ||
704
+ /^[A-Za-z0-9_\-.]+\//.test(urlStr));
705
+ if (!looksLikeUrl)
706
+ continue;
707
+ callsiteCounter++;
708
+ globals.addOpenapiOutput({
709
+ url: urlStr,
710
+ method: entry.method,
711
+ path: urlStr,
712
+ headers: entry.headers,
713
+ body: entry.body,
714
+ chunkId: `${entry.file}:${entry.fileLine}`,
715
+ functionFile: entry.filePath,
716
+ functionFileLine: entry.fileLine,
717
+ });
718
+ }
152
719
  console.log(chalk.green(`[✓] Found and resolved ${totalFetchCalls} fetch call(s) across Vue.JS files`));
153
720
  });
154
721
  export default vue_resolveFetch;