@shriyanss/js-recon 1.3.1-alpha.1 → 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 +47 -0
- package/CLAUDE.md +136 -0
- package/build/analyze/engine/astEngine.js +34 -6
- package/build/analyze/engine/astEngine.js.map +1 -1
- package/build/analyze/engine/index.js +2 -2
- package/build/analyze/engine/index.js.map +1 -1
- package/build/analyze/helpers/engineHelpers/taintFlow.js +218 -0
- package/build/analyze/helpers/engineHelpers/taintFlow.js.map +1 -0
- package/build/analyze/helpers/schemas.js +3 -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 +190 -0
- package/build/lazyLoad/downloadQueue.js.map +1 -0
- package/build/lazyLoad/index.js +57 -55
- package/build/lazyLoad/index.js.map +1 -1
- package/build/lazyLoad/next_js/NextJsCrawler.js +34 -11
- package/build/lazyLoad/next_js/NextJsCrawler.js.map +1 -1
- package/build/lazyLoad/next_js/next_GetJSScript.js +24 -3
- package/build/lazyLoad/next_js/next_GetJSScript.js.map +1 -1
- package/build/lazyLoad/next_js/next_SubsequentRequests.js +20 -2
- package/build/lazyLoad/next_js/next_SubsequentRequests.js.map +1 -1
- package/build/lazyLoad/next_js/next_scriptTagsSubsequentRequests.js +10 -1
- package/build/lazyLoad/next_js/next_scriptTagsSubsequentRequests.js.map +1 -1
- package/build/lazyLoad/techDetect/checkReact.js +13 -2
- package/build/lazyLoad/techDetect/checkReact.js.map +1 -1
- package/build/lazyLoad/techDetect/index.js +37 -24
- package/build/lazyLoad/techDetect/index.js.map +1 -1
- package/build/lazyLoad/vue/vue_discoverJsFiles.js +25 -11
- package/build/lazyLoad/vue/vue_discoverJsFiles.js.map +1 -1
- package/build/lazyLoad/vue/vue_getClientSidePaths.js +31 -4
- package/build/lazyLoad/vue/vue_getClientSidePaths.js.map +1 -1
- package/build/lazyLoad/vue/vue_recursiveClientSidePathDownload.js +3 -3
- package/build/lazyLoad/vue/vue_recursiveClientSidePathDownload.js.map +1 -1
- package/build/lazyLoad/vue/vue_stringJsFiles.js +142 -0
- package/build/lazyLoad/vue/vue_stringJsFiles.js.map +1 -0
- package/build/load/index.js +316 -0
- package/build/load/index.js.map +1 -0
- package/build/map/index.js +42 -5
- package/build/map/index.js.map +1 -1
- package/build/map/next_js/getExports.js +11 -5
- package/build/map/next_js/getExports.js.map +1 -1
- package/build/map/next_js/getFetchInstances.js +11 -5
- package/build/map/next_js/getFetchInstances.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/resolveAxios.js +24 -5
- package/build/map/next_js/resolveAxios.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/astNodeToJsonString.js +2 -2
- package/build/map/next_js/resolveAxiosHelpers/astNodeToJsonString.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/findCrossChunkParams.js +11 -5
- package/build/map/next_js/resolveAxiosHelpers/findCrossChunkParams.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/interceptorHeaders.js +206 -0
- package/build/map/next_js/resolveAxiosHelpers/interceptorHeaders.js.map +1 -0
- package/build/map/next_js/resolveAxiosHelpers/processAxiosCall.js +25 -8
- package/build/map/next_js/resolveAxiosHelpers/processAxiosCall.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/processDirectAxiosCall.js +14 -6
- package/build/map/next_js/resolveAxiosHelpers/processDirectAxiosCall.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/traceAxiosInstanceExports.js +22 -10
- package/build/map/next_js/resolveAxiosHelpers/traceAxiosInstanceExports.js.map +1 -1
- package/build/map/next_js/resolveAxiosHelpers/traceBody.js +913 -0
- package/build/map/next_js/resolveAxiosHelpers/traceBody.js.map +1 -0
- package/build/map/next_js/resolveFetch.js +115 -3
- package/build/map/next_js/resolveFetch.js.map +1 -1
- package/build/map/next_js/resolveNewRequest.js +749 -0
- package/build/map/next_js/resolveNewRequest.js.map +1 -0
- package/build/map/next_js/utils.js +397 -49
- 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 +722 -0
- package/build/map/vue_js/vue_resolveFetch.js.map +1 -0
- package/build/report/utility/populateDb/populateAnalysisFindings.js +1 -1
- package/build/report/utility/populateDb/populateAnalysisFindings.js.map +1 -1
- package/build/run/index.js +27 -5
- package/build/run/index.js.map +1 -1
- package/build/strings/index.js +5 -1
- package/build/strings/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 +90 -11
- package/build/utility/makeReq.js.map +1 -1
- package/build/utility/openapiGenerator.js +60 -4
- package/build/utility/openapiGenerator.js.map +1 -1
- package/build/utility/postmanGenerator.js +167 -0
- package/build/utility/postmanGenerator.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
import _traverse from "@babel/traverse";
|
|
2
|
+
import parser from "@babel/parser";
|
|
3
|
+
import { astNodeToJsonString } from "./astNodeToJsonString.js";
|
|
4
|
+
const traverse = _traverse.default;
|
|
5
|
+
// AST parse cache keyed by chunk id, so that cross-chunk tracing doesn't reparse
|
|
6
|
+
// the same chunk file dozens of times across recursive calls.
|
|
7
|
+
const astCache = new Map();
|
|
8
|
+
const parseChunkAst = (chunkId, code) => {
|
|
9
|
+
const cached = astCache.get(chunkId);
|
|
10
|
+
if (cached !== undefined)
|
|
11
|
+
return cached;
|
|
12
|
+
try {
|
|
13
|
+
const ast = parser.parse(code, {
|
|
14
|
+
sourceType: "unambiguous",
|
|
15
|
+
plugins: ["jsx", "typescript"],
|
|
16
|
+
errorRecovery: true,
|
|
17
|
+
});
|
|
18
|
+
astCache.set(chunkId, ast);
|
|
19
|
+
return ast;
|
|
20
|
+
}
|
|
21
|
+
catch (_a) {
|
|
22
|
+
astCache.set(chunkId, null);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Resolves an Identifier value by walking variable initializers in scope.
|
|
28
|
+
*
|
|
29
|
+
* Used when an object property is assigned an identifier like `{username: e, password: t}` —
|
|
30
|
+
* we follow `e` to its declaration to find the real shape (e.g. another property of a state
|
|
31
|
+
* value or a string literal).
|
|
32
|
+
*/
|
|
33
|
+
// Reconstruct a dotted-path string for `a.b.c` / `a["b"]` / `this.x` chains so
|
|
34
|
+
// downstream consumers see `"e.ssn"` instead of the raw `"[MemberExpression]"`
|
|
35
|
+
// AST-tag fallback.
|
|
36
|
+
const memberExpressionToString = (node) => {
|
|
37
|
+
if (!node)
|
|
38
|
+
return "?";
|
|
39
|
+
if (node.type === "Identifier")
|
|
40
|
+
return node.name;
|
|
41
|
+
if (node.type === "ThisExpression")
|
|
42
|
+
return "this";
|
|
43
|
+
if (node.type === "MemberExpression") {
|
|
44
|
+
const obj = memberExpressionToString(node.object);
|
|
45
|
+
if (node.computed) {
|
|
46
|
+
if (node.property.type === "StringLiteral")
|
|
47
|
+
return `${obj}["${node.property.value}"]`;
|
|
48
|
+
if (node.property.type === "NumericLiteral")
|
|
49
|
+
return `${obj}[${node.property.value}]`;
|
|
50
|
+
return `${obj}[?]`;
|
|
51
|
+
}
|
|
52
|
+
if (node.property.type === "Identifier")
|
|
53
|
+
return `${obj}.${node.property.name}`;
|
|
54
|
+
return `${obj}.?`;
|
|
55
|
+
}
|
|
56
|
+
return "?";
|
|
57
|
+
};
|
|
58
|
+
const resolvePropertyValueNode = (node, scopePath, depth = 0) => {
|
|
59
|
+
if (depth > 6)
|
|
60
|
+
return `"<unknown>"`;
|
|
61
|
+
if (!node)
|
|
62
|
+
return `""`;
|
|
63
|
+
if (node.type === "Identifier") {
|
|
64
|
+
const binding = scopePath.scope.getBinding(node.name);
|
|
65
|
+
if (binding && binding.path.isVariableDeclarator() && binding.path.node.init) {
|
|
66
|
+
return resolvePropertyValueNode(binding.path.node.init, binding.path, depth + 1);
|
|
67
|
+
}
|
|
68
|
+
return `"<unknown>"`;
|
|
69
|
+
}
|
|
70
|
+
if (node.type === "ObjectExpression") {
|
|
71
|
+
const props = node.properties
|
|
72
|
+
.map((prop) => {
|
|
73
|
+
if (prop.type === "ObjectProperty") {
|
|
74
|
+
let key = "";
|
|
75
|
+
if (prop.key.type === "Identifier")
|
|
76
|
+
key = prop.key.name;
|
|
77
|
+
else if (prop.key.type === "StringLiteral")
|
|
78
|
+
key = prop.key.value;
|
|
79
|
+
else
|
|
80
|
+
key = "[unresolved key]";
|
|
81
|
+
const value = resolvePropertyValueNode(prop.value, scopePath, depth + 1);
|
|
82
|
+
return `"${key}": ${value}`;
|
|
83
|
+
}
|
|
84
|
+
if (prop.type === "SpreadElement") {
|
|
85
|
+
return `"...": ${resolvePropertyValueNode(prop.argument, scopePath, depth + 1)}`;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
})
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
return `{${props.join(", ")}}`;
|
|
91
|
+
}
|
|
92
|
+
if (node.type === "StringLiteral")
|
|
93
|
+
return JSON.stringify(node.value);
|
|
94
|
+
if (node.type === "NumericLiteral")
|
|
95
|
+
return String(node.value);
|
|
96
|
+
if (node.type === "BooleanLiteral")
|
|
97
|
+
return String(node.value);
|
|
98
|
+
if (node.type === "NullLiteral")
|
|
99
|
+
return "null";
|
|
100
|
+
if (node.type === "MemberExpression") {
|
|
101
|
+
return JSON.stringify(memberExpressionToString(node));
|
|
102
|
+
}
|
|
103
|
+
// `new Date(...)` is the only NewExpression that shows up in practice (date
|
|
104
|
+
// pickers materializing `new Date(value)` before the POST). Treat it as a
|
|
105
|
+
// date placeholder; everything else degrades to "<unknown>".
|
|
106
|
+
if (node.type === "NewExpression") {
|
|
107
|
+
const callee = node.callee;
|
|
108
|
+
if (callee && callee.type === "Identifier" && callee.name === "Date")
|
|
109
|
+
return `"<date>"`;
|
|
110
|
+
return `"<unknown>"`;
|
|
111
|
+
}
|
|
112
|
+
if (node.type === "TemplateLiteral")
|
|
113
|
+
return `"<string>"`;
|
|
114
|
+
if (node.type === "ArrayExpression")
|
|
115
|
+
return `["<array>"]`;
|
|
116
|
+
if (node.type === "CallExpression")
|
|
117
|
+
return `"<unknown>"`;
|
|
118
|
+
if (node.type === "ConditionalExpression") {
|
|
119
|
+
return resolvePropertyValueNode(node.consequent, scopePath, depth + 1);
|
|
120
|
+
}
|
|
121
|
+
if (node.type === "LogicalExpression") {
|
|
122
|
+
return resolvePropertyValueNode(node.left, scopePath, depth + 1);
|
|
123
|
+
}
|
|
124
|
+
if (node.type === "UnaryExpression" && node.operator === "void")
|
|
125
|
+
return `"<unknown>"`;
|
|
126
|
+
return `"<unknown>"`;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Builds a JSON-like body string from an ObjectExpression argument, resolving identifier
|
|
130
|
+
* property values via Babel scope where possible.
|
|
131
|
+
*/
|
|
132
|
+
const objectExpressionToBody = (node, scopePath) => {
|
|
133
|
+
const props = node.properties
|
|
134
|
+
.map((prop) => {
|
|
135
|
+
if (prop.type === "ObjectProperty") {
|
|
136
|
+
let key = "";
|
|
137
|
+
if (prop.key.type === "Identifier")
|
|
138
|
+
key = prop.key.name;
|
|
139
|
+
else if (prop.key.type === "StringLiteral")
|
|
140
|
+
key = prop.key.value;
|
|
141
|
+
else
|
|
142
|
+
key = "[unresolved key]";
|
|
143
|
+
const value = resolvePropertyValueNode(prop.value, scopePath, 0);
|
|
144
|
+
return `"${key}": ${value}`;
|
|
145
|
+
}
|
|
146
|
+
if (prop.type === "SpreadElement") {
|
|
147
|
+
return `"...": ${resolvePropertyValueNode(prop.argument, scopePath, 0)}`;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
})
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
return `{${props.join(", ")}}`;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Walks any zod-style schema initializer to extract the keys that will end up in
|
|
156
|
+
* the parsed value, even when the schema is composed of chained builder calls
|
|
157
|
+
* (extend / merge / pick / omit / partial / required / passthrough / strict / etc.).
|
|
158
|
+
*
|
|
159
|
+
* Supports cross-chunk references when `chunks` + `chunkId` + `thirdArgName` are
|
|
160
|
+
* supplied: `let x = thirdArg(NNN); x.SCHEMA.extend(...)`.
|
|
161
|
+
*/
|
|
162
|
+
const buildSchemaFromInitNode = (initNode, ast, chunks, chunkId, thirdArgName, visited = new Set(), depth = 0) => {
|
|
163
|
+
var _a;
|
|
164
|
+
if (depth > 8)
|
|
165
|
+
return null;
|
|
166
|
+
if (!initNode)
|
|
167
|
+
return null;
|
|
168
|
+
// Schema reference: an Identifier pointing to another schema variable in this chunk.
|
|
169
|
+
if (initNode.type === "Identifier") {
|
|
170
|
+
return resolveSchemaVarToFields(initNode.name, ast, chunks, chunkId, thirdArgName, visited, depth + 1);
|
|
171
|
+
}
|
|
172
|
+
// Cross-chunk reference: `x.SCHEMA` where `x = thirdArg(NNN)`.
|
|
173
|
+
if (initNode.type === "MemberExpression" &&
|
|
174
|
+
initNode.property.type === "Identifier" &&
|
|
175
|
+
initNode.object.type === "Identifier") {
|
|
176
|
+
const result = resolveCrossChunkSchemaRef(initNode.object.name, initNode.property.name, ast, chunks, chunkId, thirdArgName, visited, depth + 1);
|
|
177
|
+
if (result)
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
if (initNode.type === "CallExpression" && initNode.callee.type === "MemberExpression") {
|
|
181
|
+
const methodName = initNode.callee.property.type === "Identifier" ? initNode.callee.property.name : null;
|
|
182
|
+
const base = initNode.callee.object;
|
|
183
|
+
const arg0 = initNode.arguments[0];
|
|
184
|
+
// <X>.object({ ... }) — terminal: the keys are the object literal's properties.
|
|
185
|
+
if (methodName === "object" && arg0 && arg0.type === "ObjectExpression") {
|
|
186
|
+
const fields = new Map();
|
|
187
|
+
for (const prop of arg0.properties) {
|
|
188
|
+
if (prop.type !== "ObjectProperty")
|
|
189
|
+
continue;
|
|
190
|
+
let key = "";
|
|
191
|
+
if (prop.key.type === "Identifier")
|
|
192
|
+
key = prop.key.name;
|
|
193
|
+
else if (prop.key.type === "StringLiteral")
|
|
194
|
+
key = prop.key.value;
|
|
195
|
+
else
|
|
196
|
+
continue;
|
|
197
|
+
fields.set(key, prop.value);
|
|
198
|
+
}
|
|
199
|
+
return { fields };
|
|
200
|
+
}
|
|
201
|
+
// <base>.extend({ ... }) or <base>.merge(<schema>) — accumulate the base's
|
|
202
|
+
// fields then add the extension's.
|
|
203
|
+
if (methodName === "extend" || methodName === "merge") {
|
|
204
|
+
const baseResult = (_a = buildSchemaFromInitNode(base, ast, chunks, chunkId, thirdArgName, visited, depth + 1)) !== null && _a !== void 0 ? _a : {
|
|
205
|
+
fields: new Map(),
|
|
206
|
+
};
|
|
207
|
+
if (arg0 && arg0.type === "ObjectExpression") {
|
|
208
|
+
for (const prop of arg0.properties) {
|
|
209
|
+
if (prop.type !== "ObjectProperty")
|
|
210
|
+
continue;
|
|
211
|
+
let key = "";
|
|
212
|
+
if (prop.key.type === "Identifier")
|
|
213
|
+
key = prop.key.name;
|
|
214
|
+
else if (prop.key.type === "StringLiteral")
|
|
215
|
+
key = prop.key.value;
|
|
216
|
+
else
|
|
217
|
+
continue;
|
|
218
|
+
baseResult.fields.set(key, prop.value);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (arg0) {
|
|
222
|
+
const extResult = buildSchemaFromInitNode(arg0, ast, chunks, chunkId, thirdArgName, visited, depth + 1);
|
|
223
|
+
if (extResult) {
|
|
224
|
+
for (const [k, v] of extResult.fields)
|
|
225
|
+
baseResult.fields.set(k, v);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return baseResult;
|
|
229
|
+
}
|
|
230
|
+
// <base>.pick({ keyA: true, keyB: true }) / .omit(...) — narrow the field set.
|
|
231
|
+
// The pick/omit spec may be an inline ObjectExpression *or* an Identifier
|
|
232
|
+
// pointing to a `let spec = { keyA: !0, ... }` declaration earlier in the chunk.
|
|
233
|
+
if (methodName === "pick" || methodName === "omit") {
|
|
234
|
+
let specObj = null;
|
|
235
|
+
if (arg0 && arg0.type === "ObjectExpression") {
|
|
236
|
+
specObj = arg0;
|
|
237
|
+
}
|
|
238
|
+
else if (arg0 && arg0.type === "Identifier") {
|
|
239
|
+
traverse(ast, {
|
|
240
|
+
VariableDeclarator(p) {
|
|
241
|
+
if (specObj)
|
|
242
|
+
return;
|
|
243
|
+
if (p.node.id.type === "Identifier" &&
|
|
244
|
+
p.node.id.name === arg0.name &&
|
|
245
|
+
p.node.init &&
|
|
246
|
+
p.node.init.type === "ObjectExpression") {
|
|
247
|
+
specObj = p.node.init;
|
|
248
|
+
p.stop();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (!specObj) {
|
|
254
|
+
return buildSchemaFromInitNode(base, ast, chunks, chunkId, thirdArgName, visited, depth + 1);
|
|
255
|
+
}
|
|
256
|
+
const keys = new Set();
|
|
257
|
+
for (const prop of specObj.properties) {
|
|
258
|
+
if (prop.type !== "ObjectProperty")
|
|
259
|
+
continue;
|
|
260
|
+
let key = "";
|
|
261
|
+
if (prop.key.type === "Identifier")
|
|
262
|
+
key = prop.key.name;
|
|
263
|
+
else if (prop.key.type === "StringLiteral")
|
|
264
|
+
key = prop.key.value;
|
|
265
|
+
else
|
|
266
|
+
continue;
|
|
267
|
+
keys.add(key);
|
|
268
|
+
}
|
|
269
|
+
const baseResult = buildSchemaFromInitNode(base, ast, chunks, chunkId, thirdArgName, visited, depth + 1);
|
|
270
|
+
if (!baseResult)
|
|
271
|
+
return { fields: new Map(), [methodName === "pick" ? "pickKeys" : "omitKeys"]: keys };
|
|
272
|
+
if (methodName === "pick") {
|
|
273
|
+
const filtered = new Map();
|
|
274
|
+
for (const [k, v] of baseResult.fields) {
|
|
275
|
+
if (keys.has(k))
|
|
276
|
+
filtered.set(k, v);
|
|
277
|
+
}
|
|
278
|
+
return { fields: filtered };
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const filtered = new Map();
|
|
282
|
+
for (const [k, v] of baseResult.fields) {
|
|
283
|
+
if (!keys.has(k))
|
|
284
|
+
filtered.set(k, v);
|
|
285
|
+
}
|
|
286
|
+
return { fields: filtered };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Pass-through wrappers — schema stays the same.
|
|
290
|
+
if (methodName === "partial" ||
|
|
291
|
+
methodName === "required" ||
|
|
292
|
+
methodName === "passthrough" ||
|
|
293
|
+
methodName === "strict" ||
|
|
294
|
+
methodName === "strip" ||
|
|
295
|
+
methodName === "describe" ||
|
|
296
|
+
methodName === "refine" ||
|
|
297
|
+
methodName === "superRefine" ||
|
|
298
|
+
methodName === "transform" ||
|
|
299
|
+
methodName === "default" ||
|
|
300
|
+
methodName === "optional" ||
|
|
301
|
+
methodName === "nullable" ||
|
|
302
|
+
methodName === "nullish" ||
|
|
303
|
+
methodName === "readonly" ||
|
|
304
|
+
methodName === "brand" ||
|
|
305
|
+
methodName === "catch" ||
|
|
306
|
+
methodName === "innerType" ||
|
|
307
|
+
methodName === "unwrap" ||
|
|
308
|
+
methodName === "removeDefault" ||
|
|
309
|
+
methodName === "removeCatch" ||
|
|
310
|
+
methodName === "promise" ||
|
|
311
|
+
methodName === "array") {
|
|
312
|
+
return buildSchemaFromInitNode(base, ast, chunks, chunkId, thirdArgName, visited, depth + 1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Resolves a local schema variable name to its keys, recursively chasing through
|
|
319
|
+
* builder chains and aliases.
|
|
320
|
+
*/
|
|
321
|
+
const resolveSchemaVarToFields = (varName, ast, chunks, chunkId, thirdArgName, visited, depth) => {
|
|
322
|
+
const cycleKey = `${chunkId !== null && chunkId !== void 0 ? chunkId : "_"}:${varName}`;
|
|
323
|
+
if (visited.has(cycleKey))
|
|
324
|
+
return null;
|
|
325
|
+
visited.add(cycleKey);
|
|
326
|
+
let initNode = null;
|
|
327
|
+
traverse(ast, {
|
|
328
|
+
VariableDeclarator(p) {
|
|
329
|
+
if (initNode)
|
|
330
|
+
return;
|
|
331
|
+
if (p.node.id.type === "Identifier" && p.node.id.name === varName && p.node.init) {
|
|
332
|
+
initNode = p.node.init;
|
|
333
|
+
p.stop();
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
if (!initNode)
|
|
338
|
+
return null;
|
|
339
|
+
const built = buildSchemaFromInitNode(initNode, ast, chunks, chunkId, thirdArgName, visited, depth);
|
|
340
|
+
return built ? { fields: built.fields } : null;
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Resolves `<importVar>.<exportName>` where `<importVar> = <thirdArg>(<chunkId>)`,
|
|
344
|
+
* locating the corresponding schema in the imported chunk.
|
|
345
|
+
*/
|
|
346
|
+
const resolveCrossChunkSchemaRef = (importVar, exportName, ast, chunks, _chunkId, thirdArgName, visited, depth) => {
|
|
347
|
+
if (!chunks || !thirdArgName)
|
|
348
|
+
return null;
|
|
349
|
+
// Identify which chunk `importVar` points to in the current chunk.
|
|
350
|
+
let targetChunkId = null;
|
|
351
|
+
traverse(ast, {
|
|
352
|
+
VariableDeclarator(p) {
|
|
353
|
+
if (targetChunkId)
|
|
354
|
+
return;
|
|
355
|
+
if (p.node.id.type === "Identifier" &&
|
|
356
|
+
p.node.id.name === importVar &&
|
|
357
|
+
p.node.init &&
|
|
358
|
+
p.node.init.type === "CallExpression" &&
|
|
359
|
+
p.node.init.callee.type === "Identifier" &&
|
|
360
|
+
p.node.init.callee.name === thirdArgName &&
|
|
361
|
+
p.node.init.arguments.length === 1 &&
|
|
362
|
+
p.node.init.arguments[0].type === "NumericLiteral") {
|
|
363
|
+
targetChunkId = String(p.node.init.arguments[0].value);
|
|
364
|
+
p.stop();
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
if (!targetChunkId || !chunks[targetChunkId])
|
|
369
|
+
return null;
|
|
370
|
+
const targetChunk = chunks[targetChunkId];
|
|
371
|
+
const targetAst = parseChunkAst(targetChunkId, targetChunk.code);
|
|
372
|
+
if (!targetAst)
|
|
373
|
+
return null;
|
|
374
|
+
// Find which local variable backs the requested export.
|
|
375
|
+
let exportVarName = null;
|
|
376
|
+
traverse(targetAst, {
|
|
377
|
+
CallExpression(p) {
|
|
378
|
+
if (exportVarName)
|
|
379
|
+
return;
|
|
380
|
+
const callee = p.node.callee;
|
|
381
|
+
if (callee.type !== "MemberExpression" ||
|
|
382
|
+
callee.property.type !== "Identifier" ||
|
|
383
|
+
callee.property.name !== "d" ||
|
|
384
|
+
p.node.arguments.length !== 2 ||
|
|
385
|
+
p.node.arguments[1].type !== "ObjectExpression") {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
for (const prop of p.node.arguments[1].properties) {
|
|
389
|
+
if (prop.type !== "ObjectProperty" || prop.key.type !== "Identifier" || prop.key.name !== exportName) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const value = prop.value;
|
|
393
|
+
if (value.type !== "FunctionExpression" && value.type !== "ArrowFunctionExpression")
|
|
394
|
+
continue;
|
|
395
|
+
let returnNode = null;
|
|
396
|
+
if (value.type === "ArrowFunctionExpression" && value.body.type !== "BlockStatement") {
|
|
397
|
+
returnNode = value.body;
|
|
398
|
+
}
|
|
399
|
+
else if (value.body.type === "BlockStatement") {
|
|
400
|
+
const ret = value.body.body.find((s) => s.type === "ReturnStatement");
|
|
401
|
+
if (ret)
|
|
402
|
+
returnNode = ret.argument;
|
|
403
|
+
}
|
|
404
|
+
if (returnNode && returnNode.type === "Identifier") {
|
|
405
|
+
exportVarName = returnNode.name;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
if (!exportVarName)
|
|
412
|
+
return null;
|
|
413
|
+
const targetThirdArg = findThirdArgInAst(targetAst);
|
|
414
|
+
return resolveSchemaVarToFields(exportVarName, targetAst, chunks, targetChunkId, targetThirdArg, visited, depth);
|
|
415
|
+
};
|
|
416
|
+
const zodTypeToPlaceholder = (valueNode) => {
|
|
417
|
+
if (!valueNode)
|
|
418
|
+
return `"<unknown>"`;
|
|
419
|
+
let cur = valueNode;
|
|
420
|
+
while (cur &&
|
|
421
|
+
cur.type === "CallExpression" &&
|
|
422
|
+
cur.callee.type === "MemberExpression" &&
|
|
423
|
+
cur.callee.property.type === "Identifier" &&
|
|
424
|
+
[
|
|
425
|
+
"optional",
|
|
426
|
+
"nullable",
|
|
427
|
+
"nullish",
|
|
428
|
+
"min",
|
|
429
|
+
"max",
|
|
430
|
+
"length",
|
|
431
|
+
"email",
|
|
432
|
+
"url",
|
|
433
|
+
"uuid",
|
|
434
|
+
"regex",
|
|
435
|
+
"trim",
|
|
436
|
+
"default",
|
|
437
|
+
"describe",
|
|
438
|
+
"refine",
|
|
439
|
+
"transform",
|
|
440
|
+
"superRefine",
|
|
441
|
+
].includes(cur.callee.property.name)) {
|
|
442
|
+
cur = cur.callee.object;
|
|
443
|
+
}
|
|
444
|
+
if (cur &&
|
|
445
|
+
cur.type === "CallExpression" &&
|
|
446
|
+
cur.callee.type === "MemberExpression" &&
|
|
447
|
+
cur.callee.property.type === "Identifier") {
|
|
448
|
+
const name = cur.callee.property.name;
|
|
449
|
+
switch (name) {
|
|
450
|
+
case "string":
|
|
451
|
+
return `"<string>"`;
|
|
452
|
+
case "number":
|
|
453
|
+
return `"<number>"`;
|
|
454
|
+
case "boolean":
|
|
455
|
+
return `"<boolean>"`;
|
|
456
|
+
case "bigint":
|
|
457
|
+
return `"<bigint>"`;
|
|
458
|
+
case "date":
|
|
459
|
+
return `"<date>"`;
|
|
460
|
+
case "array":
|
|
461
|
+
return `["<array>"]`;
|
|
462
|
+
case "object":
|
|
463
|
+
if (cur.arguments.length === 1 && cur.arguments[0].type === "ObjectExpression") {
|
|
464
|
+
return fieldsObjectToString(cur.arguments[0]);
|
|
465
|
+
}
|
|
466
|
+
return `"<object>"`;
|
|
467
|
+
case "enum":
|
|
468
|
+
return `"<enum>"`;
|
|
469
|
+
case "literal":
|
|
470
|
+
if (cur.arguments.length === 1 && cur.arguments[0].type === "StringLiteral") {
|
|
471
|
+
return JSON.stringify(cur.arguments[0].value);
|
|
472
|
+
}
|
|
473
|
+
return `"<literal>"`;
|
|
474
|
+
case "coerce":
|
|
475
|
+
return `"<coerce>"`;
|
|
476
|
+
default:
|
|
477
|
+
return `"<${name}>"`;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return `"<unknown>"`;
|
|
481
|
+
};
|
|
482
|
+
const fieldsObjectToString = (objNode) => {
|
|
483
|
+
const parts = objNode.properties
|
|
484
|
+
.map((prop) => {
|
|
485
|
+
if (prop.type !== "ObjectProperty")
|
|
486
|
+
return null;
|
|
487
|
+
let key = "";
|
|
488
|
+
if (prop.key.type === "Identifier")
|
|
489
|
+
key = prop.key.name;
|
|
490
|
+
else if (prop.key.type === "StringLiteral")
|
|
491
|
+
key = prop.key.value;
|
|
492
|
+
else
|
|
493
|
+
return null;
|
|
494
|
+
return `"${key}": ${zodTypeToPlaceholder(prop.value)}`;
|
|
495
|
+
})
|
|
496
|
+
.filter(Boolean);
|
|
497
|
+
return `{${parts.join(", ")}}`;
|
|
498
|
+
};
|
|
499
|
+
const fieldsMapToString = (fields) => {
|
|
500
|
+
const parts = [];
|
|
501
|
+
for (const [k, v] of fields) {
|
|
502
|
+
parts.push(`"${k}": ${zodTypeToPlaceholder(v)}`);
|
|
503
|
+
}
|
|
504
|
+
return `{${parts.join(", ")}}`;
|
|
505
|
+
};
|
|
506
|
+
/**
|
|
507
|
+
* Discovers zod-style schemas in the chunk (e.g. `let w = x.z.object({...})`,
|
|
508
|
+
* `objectSchema: o.VG`) and converts them to a JSON-like body shape.
|
|
509
|
+
*
|
|
510
|
+
* Preference order:
|
|
511
|
+
* 1. Schemas referenced via `objectSchema:` or `resolver:` JSX props. Supports
|
|
512
|
+
* both local Identifiers and cross-chunk MemberExpressions (`o.VG`).
|
|
513
|
+
* Multiple matches are merged so multi-step forms whose final POST body is
|
|
514
|
+
* the union of all step schemas come out closer to reality.
|
|
515
|
+
* 2. The first standalone `z.object({...})` definition in the chunk.
|
|
516
|
+
*/
|
|
517
|
+
const findZodSchemaInChunk = (ast, chunks, chunkId, thirdArgName) => {
|
|
518
|
+
const schemaRefs = [];
|
|
519
|
+
traverse(ast, {
|
|
520
|
+
ObjectProperty(p) {
|
|
521
|
+
const key = p.node.key;
|
|
522
|
+
const keyName = key.type === "Identifier" ? key.name : key.type === "StringLiteral" ? key.value : null;
|
|
523
|
+
const value = p.node.value;
|
|
524
|
+
const captureFromValue = (v) => {
|
|
525
|
+
if (!v)
|
|
526
|
+
return;
|
|
527
|
+
if (v.type === "Identifier") {
|
|
528
|
+
schemaRefs.push({ kind: "local", name: v.name });
|
|
529
|
+
}
|
|
530
|
+
else if (v.type === "MemberExpression" &&
|
|
531
|
+
v.object.type === "Identifier" &&
|
|
532
|
+
v.property.type === "Identifier") {
|
|
533
|
+
schemaRefs.push({ kind: "cross", importVar: v.object.name, exportName: v.property.name });
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
if (keyName === "objectSchema") {
|
|
537
|
+
captureFromValue(value);
|
|
538
|
+
}
|
|
539
|
+
else if (keyName === "resolver" && value.type === "CallExpression" && value.arguments.length > 0) {
|
|
540
|
+
captureFromValue(value.arguments[0]);
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
const resolvedFieldMaps = [];
|
|
545
|
+
const seen = new Set();
|
|
546
|
+
for (const ref of schemaRefs) {
|
|
547
|
+
const key = ref.kind === "local" ? `L:${ref.name}` : `X:${ref.importVar}.${ref.exportName}`;
|
|
548
|
+
if (seen.has(key))
|
|
549
|
+
continue;
|
|
550
|
+
seen.add(key);
|
|
551
|
+
// Each top-level schema ref starts with a fresh visited set — otherwise
|
|
552
|
+
// resolving the first schema would block the second from following
|
|
553
|
+
// shared base schemas (e.g. multi-step forms where every step extends
|
|
554
|
+
// the same `p`).
|
|
555
|
+
const refVisited = new Set();
|
|
556
|
+
let resolved = null;
|
|
557
|
+
if (ref.kind === "local") {
|
|
558
|
+
resolved = resolveSchemaVarToFields(ref.name, ast, chunks, chunkId, thirdArgName !== null && thirdArgName !== void 0 ? thirdArgName : null, refVisited, 0);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
resolved = resolveCrossChunkSchemaRef(ref.importVar, ref.exportName, ast, chunks, chunkId, thirdArgName !== null && thirdArgName !== void 0 ? thirdArgName : null, refVisited, 0);
|
|
562
|
+
}
|
|
563
|
+
if (resolved && resolved.fields.size > 0) {
|
|
564
|
+
resolvedFieldMaps.push(resolved.fields);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (resolvedFieldMaps.length > 0) {
|
|
568
|
+
// Merge fields from all referenced schemas. For multi-step forms this gives
|
|
569
|
+
// the union — the final POST body is generally the accumulated form state.
|
|
570
|
+
const merged = new Map();
|
|
571
|
+
for (const fields of resolvedFieldMaps) {
|
|
572
|
+
for (const [k, v] of fields)
|
|
573
|
+
merged.set(k, v);
|
|
574
|
+
}
|
|
575
|
+
return fieldsMapToString(merged);
|
|
576
|
+
}
|
|
577
|
+
// Fallback: the first standalone `<X>.object({...})` in the chunk.
|
|
578
|
+
let schemaNode = null;
|
|
579
|
+
traverse(ast, {
|
|
580
|
+
CallExpression(p) {
|
|
581
|
+
if (schemaNode)
|
|
582
|
+
return;
|
|
583
|
+
const callee = p.node.callee;
|
|
584
|
+
if (callee.type === "MemberExpression" &&
|
|
585
|
+
callee.property.type === "Identifier" &&
|
|
586
|
+
callee.property.name === "object" &&
|
|
587
|
+
p.node.arguments.length === 1 &&
|
|
588
|
+
p.node.arguments[0].type === "ObjectExpression") {
|
|
589
|
+
schemaNode = p.node.arguments[0];
|
|
590
|
+
p.stop();
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
if (!schemaNode)
|
|
595
|
+
return null;
|
|
596
|
+
return fieldsObjectToString(schemaNode);
|
|
597
|
+
};
|
|
598
|
+
/**
|
|
599
|
+
* Finds the webpack-export name a local function is exposed under.
|
|
600
|
+
*
|
|
601
|
+
* Webpack emits exports as:
|
|
602
|
+
* <thirdArg>.d(<exportObj>, { EXPNAME: function() { return funcName; } })
|
|
603
|
+
*
|
|
604
|
+
* Given `funcName`, returns the matching `EXPNAME` (or null if not exported).
|
|
605
|
+
*/
|
|
606
|
+
const findExportNameForFunc = (ast, funcName) => {
|
|
607
|
+
let exportName = null;
|
|
608
|
+
traverse(ast, {
|
|
609
|
+
CallExpression(p) {
|
|
610
|
+
if (exportName)
|
|
611
|
+
return;
|
|
612
|
+
const callee = p.node.callee;
|
|
613
|
+
if (callee.type !== "MemberExpression")
|
|
614
|
+
return;
|
|
615
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "d")
|
|
616
|
+
return;
|
|
617
|
+
if (p.node.arguments.length !== 2)
|
|
618
|
+
return;
|
|
619
|
+
if (p.node.arguments[1].type !== "ObjectExpression")
|
|
620
|
+
return;
|
|
621
|
+
for (const prop of p.node.arguments[1].properties) {
|
|
622
|
+
if (prop.type !== "ObjectProperty" || prop.key.type !== "Identifier")
|
|
623
|
+
continue;
|
|
624
|
+
const value = prop.value;
|
|
625
|
+
if (value.type !== "FunctionExpression" && value.type !== "ArrowFunctionExpression")
|
|
626
|
+
continue;
|
|
627
|
+
let returnNode = null;
|
|
628
|
+
if (value.type === "ArrowFunctionExpression" && value.body.type !== "BlockStatement") {
|
|
629
|
+
returnNode = value.body;
|
|
630
|
+
}
|
|
631
|
+
else if (value.body.type === "BlockStatement") {
|
|
632
|
+
const ret = value.body.body.find((s) => s.type === "ReturnStatement");
|
|
633
|
+
if (ret)
|
|
634
|
+
returnNode = ret.argument;
|
|
635
|
+
}
|
|
636
|
+
if (returnNode && returnNode.type === "Identifier" && returnNode.name === funcName) {
|
|
637
|
+
exportName = prop.key.name;
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
return exportName;
|
|
644
|
+
};
|
|
645
|
+
/**
|
|
646
|
+
* Finds the third parameter (webpack-require) name of a module function in an AST.
|
|
647
|
+
*
|
|
648
|
+
* Webpack module wrappers look like `function (e, t, n) { ... }` or
|
|
649
|
+
* `MODULE_ID: function (e, t, n) { ... }`. The third param is the require function
|
|
650
|
+
* used to import other chunks: `var X = n(<chunkId>)`.
|
|
651
|
+
*/
|
|
652
|
+
const findThirdArgInAst = (ast) => {
|
|
653
|
+
let thirdArg = null;
|
|
654
|
+
traverse(ast, {
|
|
655
|
+
Function(p) {
|
|
656
|
+
if (thirdArg)
|
|
657
|
+
return;
|
|
658
|
+
const fn = p.node;
|
|
659
|
+
if (fn.params && fn.params.length === 3 && fn.params[2].type === "Identifier") {
|
|
660
|
+
thirdArg = fn.params[2].name;
|
|
661
|
+
p.stop();
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
return thirdArg;
|
|
666
|
+
};
|
|
667
|
+
/**
|
|
668
|
+
* Cross-chunk callsite trace.
|
|
669
|
+
*
|
|
670
|
+
* When a wrapper function (e.g. `let i = (t, e) => axios.post("/url", t, e)`) has
|
|
671
|
+
* no local callsites — typical for "service module" chunks whose only purpose is
|
|
672
|
+
* to export wrappers — search every chunk that imports the current chunk, look
|
|
673
|
+
* for invocations of the corresponding export (`x.cr(args)` or `(0, x.cr)(args)`),
|
|
674
|
+
* and recurse into the body argument's identifier in that scope.
|
|
675
|
+
*/
|
|
676
|
+
const crossChunkTrace = (funcName, paramIndex, currentChunkId, currentAst, chunks, visited, depth) => {
|
|
677
|
+
const exportName = findExportNameForFunc(currentAst, funcName);
|
|
678
|
+
if (!exportName)
|
|
679
|
+
return null;
|
|
680
|
+
for (const [otherChunkId, otherChunk] of Object.entries(chunks)) {
|
|
681
|
+
if (otherChunkId === currentChunkId)
|
|
682
|
+
continue;
|
|
683
|
+
if (!otherChunk.imports || !otherChunk.imports.includes(currentChunkId))
|
|
684
|
+
continue;
|
|
685
|
+
const otherAst = parseChunkAst(otherChunkId, otherChunk.code);
|
|
686
|
+
if (!otherAst)
|
|
687
|
+
continue;
|
|
688
|
+
const otherThirdArg = findThirdArgInAst(otherAst);
|
|
689
|
+
if (!otherThirdArg)
|
|
690
|
+
continue;
|
|
691
|
+
// Find every variable that is `<otherThirdArg>(<currentChunkId>)`.
|
|
692
|
+
const importVarNames = new Set();
|
|
693
|
+
traverse(otherAst, {
|
|
694
|
+
VariableDeclarator(p) {
|
|
695
|
+
if (p.node.id.type === "Identifier" &&
|
|
696
|
+
p.node.init &&
|
|
697
|
+
p.node.init.type === "CallExpression" &&
|
|
698
|
+
p.node.init.callee.type === "Identifier" &&
|
|
699
|
+
p.node.init.callee.name === otherThirdArg &&
|
|
700
|
+
p.node.init.arguments.length === 1 &&
|
|
701
|
+
p.node.init.arguments[0].type === "NumericLiteral" &&
|
|
702
|
+
String(p.node.init.arguments[0].value) === currentChunkId) {
|
|
703
|
+
importVarNames.add(p.node.id.name);
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
if (importVarNames.size === 0)
|
|
708
|
+
continue;
|
|
709
|
+
let resolved = null;
|
|
710
|
+
traverse(otherAst, {
|
|
711
|
+
CallExpression(callPath) {
|
|
712
|
+
if (resolved)
|
|
713
|
+
return;
|
|
714
|
+
// Unwrap (0, X.exp)(...) → callee is SequenceExpression whose last element
|
|
715
|
+
// is the member expression we care about.
|
|
716
|
+
let callee = callPath.node.callee;
|
|
717
|
+
if (callee.type === "SequenceExpression") {
|
|
718
|
+
callee = callee.expressions[callee.expressions.length - 1];
|
|
719
|
+
}
|
|
720
|
+
if (callee.type !== "MemberExpression" ||
|
|
721
|
+
callee.object.type !== "Identifier" ||
|
|
722
|
+
!importVarNames.has(callee.object.name) ||
|
|
723
|
+
callee.property.type !== "Identifier" ||
|
|
724
|
+
callee.property.name !== exportName) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const argNode = callPath.node.arguments[paramIndex];
|
|
728
|
+
if (!argNode)
|
|
729
|
+
return;
|
|
730
|
+
if (argNode.type === "ObjectExpression") {
|
|
731
|
+
resolved = objectExpressionToBody(argNode, callPath);
|
|
732
|
+
callPath.stop();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (argNode.type === "Identifier") {
|
|
736
|
+
const traced = traceIdentifierBody(argNode.name, callPath, otherAst, otherChunk.code, chunks, visited, depth + 1, otherChunkId);
|
|
737
|
+
if (traced) {
|
|
738
|
+
resolved = traced;
|
|
739
|
+
callPath.stop();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
if (resolved)
|
|
745
|
+
return resolved;
|
|
746
|
+
}
|
|
747
|
+
return null;
|
|
748
|
+
};
|
|
749
|
+
/**
|
|
750
|
+
* Traces a function-parameter identifier back through call sites to find its
|
|
751
|
+
* actual body shape.
|
|
752
|
+
*
|
|
753
|
+
* Algorithm:
|
|
754
|
+
* 1. Look up the binding for `paramName` in `path.scope`.
|
|
755
|
+
* 2. If the binding is a function parameter, walk to the enclosing function,
|
|
756
|
+
* determine the parameter index, and find the function's bound name (if any).
|
|
757
|
+
* 3. Search the current chunk's AST for direct call sites of that function. For
|
|
758
|
+
* each call site, inspect the argument at the same index:
|
|
759
|
+
* - If it's an ObjectExpression, convert it to a JSON-like body and return.
|
|
760
|
+
* - If it's an Identifier, recurse on it.
|
|
761
|
+
* 4. If no local call site resolves the shape:
|
|
762
|
+
* a. Try cross-chunk tracing: find the chunk's export name for this function
|
|
763
|
+
* and recurse into every chunk that imports the export.
|
|
764
|
+
* b. As a last resort, fall back to discovering a zod schema in the same chunk.
|
|
765
|
+
*/
|
|
766
|
+
export const traceIdentifierBody = (paramName, path, ast, chunkCode, chunks, visited = new Set(), depth = 0, chunkId) => {
|
|
767
|
+
var _a, _b, _c;
|
|
768
|
+
if (depth > 6)
|
|
769
|
+
return null;
|
|
770
|
+
const binding = path.scope.getBinding(paramName);
|
|
771
|
+
if (!binding)
|
|
772
|
+
return null;
|
|
773
|
+
// Non-parameter bindings (let / const / var) can still have a useful initializer
|
|
774
|
+
// — most commonly `let r = { reportType: n.type, ...e }` immediately before an
|
|
775
|
+
// axios call. Render the initializer's ObjectExpression directly when we have one.
|
|
776
|
+
if (binding.kind !== "param") {
|
|
777
|
+
if (binding.path.isVariableDeclarator() && binding.path.node.init) {
|
|
778
|
+
const init = binding.path.node.init;
|
|
779
|
+
if (init.type === "ObjectExpression") {
|
|
780
|
+
return objectExpressionToBody(init, binding.path);
|
|
781
|
+
}
|
|
782
|
+
if (init.type === "Identifier") {
|
|
783
|
+
return traceIdentifierBody(init.name, binding.path, ast, chunkCode, chunks, visited, depth + 1, chunkId);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
const funcPath = binding.path.find((p) => p.isFunction());
|
|
789
|
+
if (!funcPath)
|
|
790
|
+
return null;
|
|
791
|
+
const funcNode = funcPath.node;
|
|
792
|
+
const paramIndex = funcNode.params.findIndex((p) => p.type === "Identifier" && p.name === paramName);
|
|
793
|
+
if (paramIndex === -1)
|
|
794
|
+
return null;
|
|
795
|
+
let funcName = null;
|
|
796
|
+
if (funcPath.isFunctionDeclaration() && ((_a = funcNode.id) === null || _a === void 0 ? void 0 : _a.type) === "Identifier") {
|
|
797
|
+
funcName = funcNode.id.name;
|
|
798
|
+
}
|
|
799
|
+
else if (funcPath.parentPath) {
|
|
800
|
+
const parent = funcPath.parentPath.node;
|
|
801
|
+
if (parent.type === "VariableDeclarator" && ((_b = parent.id) === null || _b === void 0 ? void 0 : _b.type) === "Identifier") {
|
|
802
|
+
funcName = parent.id.name;
|
|
803
|
+
}
|
|
804
|
+
else if (parent.type === "AssignmentExpression" && ((_c = parent.left) === null || _c === void 0 ? void 0 : _c.type) === "Identifier") {
|
|
805
|
+
funcName = parent.left.name;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const localThirdArg = findThirdArgInAst(ast);
|
|
809
|
+
if (!funcName) {
|
|
810
|
+
return findZodSchemaInChunk(ast, chunks, chunkId, localThirdArg);
|
|
811
|
+
}
|
|
812
|
+
const cycleKey = `${chunkId !== null && chunkId !== void 0 ? chunkId : "_"}:${funcName}:${paramIndex}`;
|
|
813
|
+
if (visited.has(cycleKey))
|
|
814
|
+
return null;
|
|
815
|
+
visited.add(cycleKey);
|
|
816
|
+
const targetFuncNode = funcNode;
|
|
817
|
+
let resolved = null;
|
|
818
|
+
let directCallCount = 0;
|
|
819
|
+
traverse(ast, {
|
|
820
|
+
CallExpression(callPath) {
|
|
821
|
+
if (resolved)
|
|
822
|
+
return;
|
|
823
|
+
const callee = callPath.node.callee;
|
|
824
|
+
if (callee.type === "Identifier" && callee.name === funcName) {
|
|
825
|
+
const callBinding = callPath.scope.getBinding(funcName);
|
|
826
|
+
if (!callBinding)
|
|
827
|
+
return;
|
|
828
|
+
const callTargetFunc = callBinding.path.isVariableDeclarator() && callBinding.path.node.init
|
|
829
|
+
? callBinding.path.node.init
|
|
830
|
+
: callBinding.path.node;
|
|
831
|
+
if (callTargetFunc !== targetFuncNode) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
directCallCount++;
|
|
835
|
+
const argNode = callPath.node.arguments[paramIndex];
|
|
836
|
+
if (!argNode)
|
|
837
|
+
return;
|
|
838
|
+
if (argNode.type === "ObjectExpression") {
|
|
839
|
+
resolved = objectExpressionToBody(argNode, callPath);
|
|
840
|
+
callPath.stop();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (argNode.type === "Identifier") {
|
|
844
|
+
const traced = traceIdentifierBody(argNode.name, callPath, ast, chunkCode, chunks, visited, depth + 1, chunkId);
|
|
845
|
+
if (traced) {
|
|
846
|
+
resolved = traced;
|
|
847
|
+
callPath.stop();
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
if (resolved)
|
|
854
|
+
return resolved;
|
|
855
|
+
// No local resolution. Try cross-chunk callsite trace before falling back to
|
|
856
|
+
// schema discovery — cross-chunk usually carries real call-site evidence,
|
|
857
|
+
// while zod schemas in unrelated chunks are easy to misidentify.
|
|
858
|
+
if (chunkId && chunks) {
|
|
859
|
+
const crossChunk = crossChunkTrace(funcName, paramIndex, chunkId, ast, chunks, visited, depth);
|
|
860
|
+
if (crossChunk)
|
|
861
|
+
return crossChunk;
|
|
862
|
+
}
|
|
863
|
+
// Schema fallback. Used when:
|
|
864
|
+
// - the function is never called directly (only passed as a callback to a
|
|
865
|
+
// form/library), or
|
|
866
|
+
// - it *is* called locally but with state/non-traceable identifiers (so the
|
|
867
|
+
// local callsite gave us nothing).
|
|
868
|
+
// The schema search prefers `objectSchema:` / `resolver:` JSX props, which
|
|
869
|
+
// are usually the right wiring even when the handler's data flow is opaque.
|
|
870
|
+
return findZodSchemaInChunk(ast, chunks, chunkId, localThirdArg);
|
|
871
|
+
};
|
|
872
|
+
/**
|
|
873
|
+
* Convenience entry that converts the result back into something acceptable as a
|
|
874
|
+
* `callBody` string. Returns null if no improvement is possible.
|
|
875
|
+
*/
|
|
876
|
+
export const maybeTraceBodyForIdentifier = (bodyArgNode, parentCallPath, ast, chunkCode, chunks, chunkId) => {
|
|
877
|
+
if (!bodyArgNode || bodyArgNode.type !== "Identifier")
|
|
878
|
+
return null;
|
|
879
|
+
const idName = bodyArgNode.name;
|
|
880
|
+
return traceIdentifierBody(idName, parentCallPath, ast, chunkCode, chunks, new Set(), 0, chunkId);
|
|
881
|
+
};
|
|
882
|
+
/**
|
|
883
|
+
* Helper: rebuild the original astNodeToJsonString result, but for an Identifier-only
|
|
884
|
+
* body, attempt to improve via taint trace.
|
|
885
|
+
*/
|
|
886
|
+
/**
|
|
887
|
+
* Detects literal-undefined body arguments. Minified bundles emit `undefined` as
|
|
888
|
+
* the `void 0` shorthand (UnaryExpression with operator `void` over any operand),
|
|
889
|
+
* which axios treats as "no body". The bare identifier `undefined` is the same
|
|
890
|
+
* thing in any non-shadowed scope. Either form should map to an empty body so
|
|
891
|
+
* downstream consumers (openapi/postman) omit the request body entirely instead
|
|
892
|
+
* of rendering the literal string `"void 0"`.
|
|
893
|
+
*/
|
|
894
|
+
const isUndefinedBodyNode = (node) => {
|
|
895
|
+
if (!node)
|
|
896
|
+
return false;
|
|
897
|
+
if (node.type === "UnaryExpression" && node.operator === "void")
|
|
898
|
+
return true;
|
|
899
|
+
if (node.type === "Identifier" && node.name === "undefined")
|
|
900
|
+
return true;
|
|
901
|
+
return false;
|
|
902
|
+
};
|
|
903
|
+
export const resolveBodyArg = (bodyArgNode, parentCallPath, ast, chunkCode, chunks, chunkId) => {
|
|
904
|
+
if (isUndefinedBodyNode(bodyArgNode))
|
|
905
|
+
return "";
|
|
906
|
+
if (bodyArgNode && bodyArgNode.type === "Identifier") {
|
|
907
|
+
const traced = maybeTraceBodyForIdentifier(bodyArgNode, parentCallPath, ast, chunkCode, chunks, chunkId);
|
|
908
|
+
if (traced)
|
|
909
|
+
return traced;
|
|
910
|
+
}
|
|
911
|
+
return astNodeToJsonString(bodyArgNode, chunkCode);
|
|
912
|
+
};
|
|
913
|
+
//# sourceMappingURL=traceBody.js.map
|