@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.
- package/.github/workflows/pr_checker.yml +9 -8
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +136 -0
- package/build/analyze/engine/index.js +2 -2
- package/build/analyze/engine/index.js.map +1 -1
- package/build/analyze/helpers/schemas.js +2 -1
- package/build/analyze/helpers/schemas.js.map +1 -1
- package/build/analyze/helpers/validate.js +49 -9
- package/build/analyze/helpers/validate.js.map +1 -1
- package/build/analyze/index.js +3 -2
- package/build/analyze/index.js.map +1 -1
- package/build/globalConfig.js +1 -1
- package/build/index.js +21 -1
- package/build/index.js.map +1 -1
- package/build/lazyLoad/downloadFilesUtil.js +3 -3
- package/build/lazyLoad/downloadFilesUtil.js.map +1 -1
- package/build/lazyLoad/downloadQueue.js +7 -3
- package/build/lazyLoad/downloadQueue.js.map +1 -1
- package/build/lazyLoad/next_js/next_GetJSScript.js +8 -4
- package/build/lazyLoad/next_js/next_GetJSScript.js.map +1 -1
- package/build/lazyLoad/techDetect/index.js +22 -20
- package/build/lazyLoad/techDetect/index.js.map +1 -1
- package/build/load/index.js +316 -0
- package/build/load/index.js.map +1 -0
- package/build/map/index.js +11 -5
- package/build/map/index.js.map +1 -1
- package/build/map/next_js/interactive.js +30 -0
- package/build/map/next_js/interactive.js.map +1 -1
- package/build/map/next_js/interactive_helpers/commandHandler.js +22 -0
- package/build/map/next_js/interactive_helpers/commandHandler.js.map +1 -1
- package/build/map/next_js/interactive_helpers/esqueryGen.js +370 -0
- package/build/map/next_js/interactive_helpers/esqueryGen.js.map +1 -0
- package/build/map/next_js/interactive_helpers/helpMenu.js +1 -0
- package/build/map/next_js/interactive_helpers/helpMenu.js.map +1 -1
- package/build/map/next_js/interactive_helpers/inputPatch.js +207 -0
- package/build/map/next_js/interactive_helpers/inputPatch.js.map +1 -0
- package/build/map/next_js/interactive_helpers/ui.js +0 -1
- package/build/map/next_js/interactive_helpers/ui.js.map +1 -1
- package/build/map/next_js/utils.js +88 -2
- package/build/map/next_js/utils.js.map +1 -1
- package/build/map/vue_js/getViteConnections.js +27 -3
- package/build/map/vue_js/getViteConnections.js.map +1 -1
- package/build/map/vue_js/interactive.js +28 -0
- package/build/map/vue_js/interactive.js.map +1 -1
- package/build/map/vue_js/interactive_helpers/commandHandler.js +22 -0
- package/build/map/vue_js/interactive_helpers/commandHandler.js.map +1 -1
- package/build/map/vue_js/interactive_helpers/helpMenu.js +1 -0
- package/build/map/vue_js/interactive_helpers/helpMenu.js.map +1 -1
- package/build/map/vue_js/vue_resolveFetch.js +588 -21
- package/build/map/vue_js/vue_resolveFetch.js.map +1 -1
- package/build/report/utility/populateDb/populateAnalysisFindings.js +1 -1
- package/build/report/utility/populateDb/populateAnalysisFindings.js.map +1 -1
- package/build/run/index.js +22 -5
- package/build/run/index.js.map +1 -1
- package/build/utility/globals.js +9 -0
- package/build/utility/globals.js.map +1 -1
- package/build/utility/makeReq.js +25 -3
- package/build/utility/makeReq.js.map +1 -1
- package/build/utility/openapiGenerator.js +15 -3
- package/build/utility/openapiGenerator.js.map +1 -1
- package/build/utility/postmanGenerator.js +16 -12
- package/build/utility/postmanGenerator.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
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 (
|
|
553
|
+
if (!init)
|
|
69
554
|
return;
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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;
|