@timo9378/flow2code 0.1.1 → 0.1.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/CHANGELOG.md +56 -56
- package/LICENSE +21 -21
- package/README.md +230 -223
- package/dist/cli.js +61 -2
- package/dist/server.d.ts +27 -27
- package/dist/server.js +876 -4
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +4 -4
- package/out/__next._full.txt +13 -13
- package/out/__next._head.txt +4 -4
- package/out/__next._index.txt +6 -6
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/chunks/{41ec06cb3e44c96a.js → 5f1a9fec0e69c483.js} +2 -2
- package/out/_next/static/chunks/6b84376656bd9887.js +1 -0
- package/out/_next/static/chunks/993eabba22c07d39.js +54 -0
- package/out/_next/static/chunks/a6dad97d9634a72d.js.map +1 -1
- package/out/_next/static/chunks/ab8888d4b78b94be.js +5 -0
- package/out/_next/static/chunks/acf223168ac429f7.js +1 -0
- package/out/_next/static/chunks/{8486af13ea541d4d.js → b112c2f519e4b429.js} +1 -1
- package/out/_next/static/chunks/{4b43c69e91d19eee.js → b163b5d7cccbcf42.js} +1 -1
- package/out/_next/static/chunks/b6e8711267bccbbd.js +1 -0
- package/out/_next/static/chunks/c8a26302d935bf6e.css +1 -0
- package/out/_next/static/chunks/fbca595129527827.js +1 -0
- package/out/_next/static/chunks/{turbopack-b95472a0b02a2729.js → turbopack-576234c945ffdc44.js} +1 -1
- package/out/_not-found/__next._full.txt +11 -11
- package/out/_not-found/__next._head.txt +4 -4
- package/out/_not-found/__next._index.txt +6 -6
- package/out/_not-found/{__next._not-found.__PAGE__.txt → __next._not-found/__PAGE__.txt} +2 -2
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +2 -2
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +11 -11
- package/out/index.html +2 -2
- package/out/index.txt +13 -13
- package/package.json +123 -121
- package/scripts/postinstall.cjs +23 -0
- package/out/_next/static/chunks/3638bc7cb3d72bbe.js +0 -379
- package/out/_next/static/chunks/46e882944a7f3aed.js +0 -1
- package/out/_next/static/chunks/59b058cf94cc555e.js +0 -1
- package/out/_next/static/chunks/5c37aae6b3fd1bfe.js +0 -52
- package/out/_next/static/chunks/6e36daee8960e188.js +0 -5
- package/out/_next/static/chunks/80a5431d4e78c469.js +0 -1
- package/out/_next/static/chunks/8a38426c5ed81587.js +0 -1
- package/out/_next/static/chunks/8b2510d8037d3419.css +0 -1
- /package/out/_next/static/{MU8PWJNAi-sgHZxErePm1 → gs3QpnA696kN6tOSlwt6o}/_buildManifest.js +0 -0
- /package/out/_next/static/{MU8PWJNAi-sgHZxErePm1 → gs3QpnA696kN6tOSlwt6o}/_clientMiddlewareManifest.json +0 -0
- /package/out/_next/static/{MU8PWJNAi-sgHZxErePm1 → gs3QpnA696kN6tOSlwt6o}/_ssgManifest.js +0 -0
package/dist/server.js
CHANGED
|
@@ -2175,6 +2175,820 @@ function buildSourceMap(code, ir, filePath) {
|
|
|
2175
2175
|
};
|
|
2176
2176
|
}
|
|
2177
2177
|
|
|
2178
|
+
// src/lib/compiler/decompiler.ts
|
|
2179
|
+
import {
|
|
2180
|
+
Project as Project2,
|
|
2181
|
+
SyntaxKind
|
|
2182
|
+
} from "ts-morph";
|
|
2183
|
+
function decompile(code, options = {}) {
|
|
2184
|
+
const errors = [];
|
|
2185
|
+
const { fileName = "input.ts", audit: enableAudit = true } = options;
|
|
2186
|
+
try {
|
|
2187
|
+
const project = new Project2({ useInMemoryFileSystem: true });
|
|
2188
|
+
const sourceFile = project.createSourceFile(fileName, code);
|
|
2189
|
+
const targetFn = findTargetFunction(sourceFile, options.functionName);
|
|
2190
|
+
if (!targetFn) {
|
|
2191
|
+
errors.push("No exported function found to decompile");
|
|
2192
|
+
return { success: false, errors, confidence: 0 };
|
|
2193
|
+
}
|
|
2194
|
+
const ctx = createDecompileContext();
|
|
2195
|
+
const trigger = createTriggerFromFunction(targetFn, sourceFile, ctx);
|
|
2196
|
+
ctx.addNode(trigger);
|
|
2197
|
+
const body = targetFn.fn.getBody();
|
|
2198
|
+
if (body && body.getKind() === SyntaxKind.Block) {
|
|
2199
|
+
walkBlock(body, trigger.id, ctx);
|
|
2200
|
+
}
|
|
2201
|
+
buildEdges(ctx);
|
|
2202
|
+
const auditHints = enableAudit ? computeAuditHints(ctx) : [];
|
|
2203
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2204
|
+
const ir = {
|
|
2205
|
+
version: CURRENT_IR_VERSION,
|
|
2206
|
+
meta: {
|
|
2207
|
+
name: targetFn.name ?? fileName.replace(/\.(ts|tsx|js|jsx)$/, ""),
|
|
2208
|
+
description: `Decompiled from ${fileName}`,
|
|
2209
|
+
createdAt: now,
|
|
2210
|
+
updatedAt: now
|
|
2211
|
+
},
|
|
2212
|
+
nodes: ctx.getNodes(),
|
|
2213
|
+
edges: ctx.getEdges()
|
|
2214
|
+
};
|
|
2215
|
+
const nodeCount = ir.nodes.length;
|
|
2216
|
+
const hasControlFlow = ir.nodes.some((n) => n.category === "logic" /* LOGIC */);
|
|
2217
|
+
const confidence = Math.min(
|
|
2218
|
+
0.95,
|
|
2219
|
+
0.3 + nodeCount * 0.08 + (hasControlFlow ? 0.15 : 0)
|
|
2220
|
+
);
|
|
2221
|
+
return {
|
|
2222
|
+
success: true,
|
|
2223
|
+
ir,
|
|
2224
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
2225
|
+
confidence,
|
|
2226
|
+
audit: auditHints.length > 0 ? auditHints : void 0
|
|
2227
|
+
};
|
|
2228
|
+
} catch (err) {
|
|
2229
|
+
return {
|
|
2230
|
+
success: false,
|
|
2231
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
2232
|
+
confidence: 0
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
function createDecompileContext() {
|
|
2237
|
+
const ctx = {
|
|
2238
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
2239
|
+
edgeList: [],
|
|
2240
|
+
varDefs: /* @__PURE__ */ new Map(),
|
|
2241
|
+
varUses: /* @__PURE__ */ new Map(),
|
|
2242
|
+
seqPredecessors: /* @__PURE__ */ new Map(),
|
|
2243
|
+
controlFlowChildren: /* @__PURE__ */ new Map(),
|
|
2244
|
+
auditData: [],
|
|
2245
|
+
counters: {},
|
|
2246
|
+
nodeLines: /* @__PURE__ */ new Map(),
|
|
2247
|
+
addNode(node, line) {
|
|
2248
|
+
ctx.nodes.set(node.id, node);
|
|
2249
|
+
if (line !== void 0) ctx.nodeLines.set(node.id, line);
|
|
2250
|
+
},
|
|
2251
|
+
getNodes() {
|
|
2252
|
+
return Array.from(ctx.nodes.values());
|
|
2253
|
+
},
|
|
2254
|
+
getEdges() {
|
|
2255
|
+
return ctx.edgeList;
|
|
2256
|
+
},
|
|
2257
|
+
nextId(prefix) {
|
|
2258
|
+
ctx.counters[prefix] = (ctx.counters[prefix] ?? 0) + 1;
|
|
2259
|
+
return `${prefix}_${ctx.counters[prefix]}`;
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
return ctx;
|
|
2263
|
+
}
|
|
2264
|
+
function findTargetFunction(sourceFile, targetName) {
|
|
2265
|
+
const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
2266
|
+
if (targetName) {
|
|
2267
|
+
const fn = sourceFile.getFunction(targetName);
|
|
2268
|
+
if (fn) {
|
|
2269
|
+
const name = fn.getName();
|
|
2270
|
+
const httpMethod = name && httpMethods.includes(name.toUpperCase()) ? name.toUpperCase() : void 0;
|
|
2271
|
+
return { name: targetName, fn, httpMethod, isExported: fn.isExported() };
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
for (const fn of sourceFile.getFunctions()) {
|
|
2275
|
+
if (!fn.isExported()) continue;
|
|
2276
|
+
const name = fn.getName();
|
|
2277
|
+
if (name && httpMethods.includes(name.toUpperCase())) {
|
|
2278
|
+
return { name, fn, httpMethod: name.toUpperCase(), isExported: true };
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
for (const fn of sourceFile.getFunctions()) {
|
|
2282
|
+
if (fn.isExported()) {
|
|
2283
|
+
return { name: fn.getName(), fn, isExported: true };
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const allFns = sourceFile.getFunctions();
|
|
2287
|
+
if (allFns.length > 0) {
|
|
2288
|
+
const fn = allFns[0];
|
|
2289
|
+
return { name: fn.getName(), fn, isExported: fn.isExported() };
|
|
2290
|
+
}
|
|
2291
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
2292
|
+
if (!stmt.isExported()) continue;
|
|
2293
|
+
for (const decl of stmt.getDeclarations()) {
|
|
2294
|
+
const init = decl.getInitializer();
|
|
2295
|
+
if (init && init.getKind() === SyntaxKind.ArrowFunction) {
|
|
2296
|
+
return {
|
|
2297
|
+
name: decl.getName(),
|
|
2298
|
+
fn: init,
|
|
2299
|
+
isExported: true
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return null;
|
|
2305
|
+
}
|
|
2306
|
+
function createTriggerFromFunction(target, sourceFile, ctx) {
|
|
2307
|
+
const id = ctx.nextId("trigger");
|
|
2308
|
+
if (target.httpMethod) {
|
|
2309
|
+
const routePath = inferRoutePath(sourceFile);
|
|
2310
|
+
const method = target.httpMethod;
|
|
2311
|
+
const parseBody = method !== "GET";
|
|
2312
|
+
return {
|
|
2313
|
+
id,
|
|
2314
|
+
nodeType: "http_webhook" /* HTTP_WEBHOOK */,
|
|
2315
|
+
category: "trigger" /* TRIGGER */,
|
|
2316
|
+
label: `${method} ${routePath}`,
|
|
2317
|
+
params: { method, routePath, parseBody },
|
|
2318
|
+
inputs: [],
|
|
2319
|
+
outputs: [
|
|
2320
|
+
{ id: "request", label: "Request", dataType: "object" },
|
|
2321
|
+
{ id: "body", label: "Body", dataType: "object" },
|
|
2322
|
+
{ id: "query", label: "Query", dataType: "object" }
|
|
2323
|
+
]
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
return {
|
|
2327
|
+
id,
|
|
2328
|
+
nodeType: "manual" /* MANUAL */,
|
|
2329
|
+
category: "trigger" /* TRIGGER */,
|
|
2330
|
+
label: target.name ?? "Entry Point",
|
|
2331
|
+
params: {
|
|
2332
|
+
functionName: target.name ?? "handler",
|
|
2333
|
+
args: target.fn.getParameters?.() ? target.fn.getParameters().map((p) => ({
|
|
2334
|
+
name: p.getName(),
|
|
2335
|
+
type: inferDataType(p.getType().getText())
|
|
2336
|
+
})) : []
|
|
2337
|
+
},
|
|
2338
|
+
inputs: [],
|
|
2339
|
+
outputs: [{ id: "output", label: "Output", dataType: "any" }]
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
function walkBlock(block, parentNodeId, ctx, controlFlowPort) {
|
|
2343
|
+
const statements = block.getStatements();
|
|
2344
|
+
let lastNodeId = parentNodeId;
|
|
2345
|
+
for (const stmt of statements) {
|
|
2346
|
+
const nodeId = processStatement(stmt, lastNodeId, ctx);
|
|
2347
|
+
if (nodeId) {
|
|
2348
|
+
if (lastNodeId !== parentNodeId || !controlFlowPort) {
|
|
2349
|
+
ctx.seqPredecessors.set(nodeId, lastNodeId);
|
|
2350
|
+
}
|
|
2351
|
+
if (lastNodeId === parentNodeId && controlFlowPort) {
|
|
2352
|
+
if (!ctx.controlFlowChildren.has(parentNodeId)) {
|
|
2353
|
+
ctx.controlFlowChildren.set(parentNodeId, []);
|
|
2354
|
+
}
|
|
2355
|
+
ctx.controlFlowChildren.get(parentNodeId).push({
|
|
2356
|
+
portId: controlFlowPort,
|
|
2357
|
+
nodeId
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
lastNodeId = nodeId;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
function processStatement(stmt, prevNodeId, ctx) {
|
|
2365
|
+
const kind = stmt.getKind();
|
|
2366
|
+
const line = stmt.getStartLineNumber();
|
|
2367
|
+
switch (kind) {
|
|
2368
|
+
case SyntaxKind.VariableStatement:
|
|
2369
|
+
return processVariableStatement(stmt, prevNodeId, ctx, line);
|
|
2370
|
+
case SyntaxKind.IfStatement:
|
|
2371
|
+
return processIfStatement(stmt, prevNodeId, ctx, line);
|
|
2372
|
+
case SyntaxKind.ForOfStatement:
|
|
2373
|
+
return processForOfStatement(stmt, prevNodeId, ctx, line);
|
|
2374
|
+
case SyntaxKind.ForInStatement:
|
|
2375
|
+
return processForInStatement(stmt, prevNodeId, ctx, line);
|
|
2376
|
+
case SyntaxKind.ForStatement:
|
|
2377
|
+
return processForStatement(stmt, prevNodeId, ctx, line);
|
|
2378
|
+
case SyntaxKind.TryStatement:
|
|
2379
|
+
return processTryStatement(stmt, prevNodeId, ctx, line);
|
|
2380
|
+
case SyntaxKind.ReturnStatement:
|
|
2381
|
+
return processReturnStatement(stmt, prevNodeId, ctx, line);
|
|
2382
|
+
case SyntaxKind.ExpressionStatement:
|
|
2383
|
+
return processExpressionStatement(stmt, prevNodeId, ctx, line);
|
|
2384
|
+
default:
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function processVariableStatement(stmt, _prevNodeId, ctx, line) {
|
|
2389
|
+
const declarations = stmt.getDeclarations();
|
|
2390
|
+
if (declarations.length === 0) return null;
|
|
2391
|
+
const decl = declarations[0];
|
|
2392
|
+
return processVariableDeclaration(decl, ctx, line);
|
|
2393
|
+
}
|
|
2394
|
+
function processVariableDeclaration(decl, ctx, line) {
|
|
2395
|
+
const varName = decl.getName();
|
|
2396
|
+
const init = decl.getInitializer();
|
|
2397
|
+
if (!init) return null;
|
|
2398
|
+
const initText = init.getText();
|
|
2399
|
+
if (isFetchCall(init)) {
|
|
2400
|
+
const nodeId2 = ctx.nextId("fetch");
|
|
2401
|
+
const node = parseFetchNode(nodeId2, initText, varName);
|
|
2402
|
+
ctx.addNode(node, line);
|
|
2403
|
+
ctx.varDefs.set(varName, { nodeId: nodeId2, portId: "data", varName });
|
|
2404
|
+
return nodeId2;
|
|
2405
|
+
}
|
|
2406
|
+
if (hasAwaitExpression(init)) {
|
|
2407
|
+
const nodeId2 = ctx.nextId("async_op");
|
|
2408
|
+
const awaitedCode = extractAwaitedExpression(init);
|
|
2409
|
+
ctx.addNode({
|
|
2410
|
+
id: nodeId2,
|
|
2411
|
+
nodeType: "custom_code" /* CUSTOM_CODE */,
|
|
2412
|
+
category: "action" /* ACTION */,
|
|
2413
|
+
label: inferLabel(awaitedCode, varName),
|
|
2414
|
+
params: { code: awaitedCode, returnVariable: varName },
|
|
2415
|
+
inputs: [{ id: "input", label: "Input", dataType: "any", required: false }],
|
|
2416
|
+
outputs: [{ id: "result", label: "Result", dataType: "any" }]
|
|
2417
|
+
}, line);
|
|
2418
|
+
ctx.varDefs.set(varName, { nodeId: nodeId2, portId: "result", varName });
|
|
2419
|
+
trackVariableUses(nodeId2, initText, ctx);
|
|
2420
|
+
return nodeId2;
|
|
2421
|
+
}
|
|
2422
|
+
if (isSimpleDeclaration(init)) {
|
|
2423
|
+
const nodeId2 = ctx.nextId("var");
|
|
2424
|
+
const dataType = inferDataType(decl.getType().getText());
|
|
2425
|
+
const isConst = decl.getVariableStatement()?.getDeclarationKind()?.toString() === "const";
|
|
2426
|
+
ctx.addNode({
|
|
2427
|
+
id: nodeId2,
|
|
2428
|
+
nodeType: "declare" /* DECLARE */,
|
|
2429
|
+
category: "variable" /* VARIABLE */,
|
|
2430
|
+
label: varName,
|
|
2431
|
+
params: {
|
|
2432
|
+
name: varName,
|
|
2433
|
+
dataType,
|
|
2434
|
+
initialValue: initText,
|
|
2435
|
+
isConst: isConst ?? true
|
|
2436
|
+
},
|
|
2437
|
+
inputs: [],
|
|
2438
|
+
outputs: [{ id: "value", label: "Value", dataType }]
|
|
2439
|
+
}, line);
|
|
2440
|
+
ctx.varDefs.set(varName, { nodeId: nodeId2, portId: "value", varName });
|
|
2441
|
+
trackVariableUses(nodeId2, initText, ctx);
|
|
2442
|
+
return nodeId2;
|
|
2443
|
+
}
|
|
2444
|
+
const nodeId = ctx.nextId("transform");
|
|
2445
|
+
ctx.addNode({
|
|
2446
|
+
id: nodeId,
|
|
2447
|
+
nodeType: "transform" /* TRANSFORM */,
|
|
2448
|
+
category: "variable" /* VARIABLE */,
|
|
2449
|
+
label: varName,
|
|
2450
|
+
params: { expression: initText },
|
|
2451
|
+
inputs: [{ id: "input", label: "Input", dataType: "any", required: true }],
|
|
2452
|
+
outputs: [{ id: "output", label: "Output", dataType: "any" }]
|
|
2453
|
+
}, line);
|
|
2454
|
+
ctx.varDefs.set(varName, { nodeId, portId: "output", varName });
|
|
2455
|
+
trackVariableUses(nodeId, initText, ctx);
|
|
2456
|
+
return nodeId;
|
|
2457
|
+
}
|
|
2458
|
+
function processIfStatement(stmt, _prevNodeId, ctx, line) {
|
|
2459
|
+
const nodeId = ctx.nextId("if");
|
|
2460
|
+
const condition = stmt.getExpression().getText();
|
|
2461
|
+
ctx.addNode({
|
|
2462
|
+
id: nodeId,
|
|
2463
|
+
nodeType: "if_else" /* IF_ELSE */,
|
|
2464
|
+
category: "logic" /* LOGIC */,
|
|
2465
|
+
label: `if (${truncate(condition, 40)})`,
|
|
2466
|
+
params: { condition },
|
|
2467
|
+
inputs: [{ id: "input", label: "Input", dataType: "any", required: true }],
|
|
2468
|
+
outputs: [
|
|
2469
|
+
{ id: "true", label: "True", dataType: "any" },
|
|
2470
|
+
{ id: "false", label: "False", dataType: "any" }
|
|
2471
|
+
]
|
|
2472
|
+
}, line);
|
|
2473
|
+
trackVariableUses(nodeId, condition, ctx);
|
|
2474
|
+
const thenBlock = stmt.getThenStatement();
|
|
2475
|
+
if (thenBlock.getKind() === SyntaxKind.Block) {
|
|
2476
|
+
walkBlock(thenBlock, nodeId, ctx, "true");
|
|
2477
|
+
}
|
|
2478
|
+
const elseStmt = stmt.getElseStatement();
|
|
2479
|
+
if (elseStmt) {
|
|
2480
|
+
if (elseStmt.getKind() === SyntaxKind.Block) {
|
|
2481
|
+
walkBlock(elseStmt, nodeId, ctx, "false");
|
|
2482
|
+
} else if (elseStmt.getKind() === SyntaxKind.IfStatement) {
|
|
2483
|
+
const nestedId = processIfStatement(elseStmt, nodeId, ctx, elseStmt.getStartLineNumber());
|
|
2484
|
+
if (!ctx.controlFlowChildren.has(nodeId)) {
|
|
2485
|
+
ctx.controlFlowChildren.set(nodeId, []);
|
|
2486
|
+
}
|
|
2487
|
+
ctx.controlFlowChildren.get(nodeId).push({ portId: "false", nodeId: nestedId });
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
if (!elseStmt) {
|
|
2491
|
+
ctx.auditData.push({
|
|
2492
|
+
nodeId,
|
|
2493
|
+
severity: "info",
|
|
2494
|
+
message: "If statement has no else branch \u2014 consider handling the negative case",
|
|
2495
|
+
line
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
return nodeId;
|
|
2499
|
+
}
|
|
2500
|
+
function processForOfStatement(stmt, _prevNodeId, ctx, line) {
|
|
2501
|
+
const nodeId = ctx.nextId("loop");
|
|
2502
|
+
const initText = stmt.getInitializer().getText();
|
|
2503
|
+
const itemVar = initText.replace(/^(const|let|var)\s+/, "");
|
|
2504
|
+
const iterableExpr = stmt.getExpression().getText();
|
|
2505
|
+
ctx.addNode({
|
|
2506
|
+
id: nodeId,
|
|
2507
|
+
nodeType: "for_loop" /* FOR_LOOP */,
|
|
2508
|
+
category: "logic" /* LOGIC */,
|
|
2509
|
+
label: `for (${itemVar} of ${truncate(iterableExpr, 30)})`,
|
|
2510
|
+
params: {
|
|
2511
|
+
iterableExpression: iterableExpr,
|
|
2512
|
+
itemVariable: itemVar
|
|
2513
|
+
},
|
|
2514
|
+
inputs: [{ id: "iterable", label: "Iterable", dataType: "array", required: true }],
|
|
2515
|
+
outputs: [
|
|
2516
|
+
{ id: "item", label: "Item", dataType: "any" },
|
|
2517
|
+
{ id: "result", label: "Result", dataType: "array" }
|
|
2518
|
+
]
|
|
2519
|
+
}, line);
|
|
2520
|
+
trackVariableUses(nodeId, iterableExpr, ctx);
|
|
2521
|
+
const body = stmt.getStatement();
|
|
2522
|
+
if (body.getKind() === SyntaxKind.Block) {
|
|
2523
|
+
walkBlock(body, nodeId, ctx, "item");
|
|
2524
|
+
}
|
|
2525
|
+
return nodeId;
|
|
2526
|
+
}
|
|
2527
|
+
function processForInStatement(stmt, _prevNodeId, ctx, line) {
|
|
2528
|
+
const nodeId = ctx.nextId("loop");
|
|
2529
|
+
const initText = stmt.getInitializer().getText();
|
|
2530
|
+
const itemVar = initText.replace(/^(const|let|var)\s+/, "");
|
|
2531
|
+
const iterableExpr = stmt.getExpression().getText();
|
|
2532
|
+
ctx.addNode({
|
|
2533
|
+
id: nodeId,
|
|
2534
|
+
nodeType: "for_loop" /* FOR_LOOP */,
|
|
2535
|
+
category: "logic" /* LOGIC */,
|
|
2536
|
+
label: `for (${itemVar} in ${truncate(iterableExpr, 30)})`,
|
|
2537
|
+
params: {
|
|
2538
|
+
iterableExpression: `Object.keys(${iterableExpr})`,
|
|
2539
|
+
itemVariable: itemVar
|
|
2540
|
+
},
|
|
2541
|
+
inputs: [{ id: "iterable", label: "Iterable", dataType: "array", required: true }],
|
|
2542
|
+
outputs: [
|
|
2543
|
+
{ id: "item", label: "Item", dataType: "any" },
|
|
2544
|
+
{ id: "result", label: "Result", dataType: "array" }
|
|
2545
|
+
]
|
|
2546
|
+
}, line);
|
|
2547
|
+
trackVariableUses(nodeId, iterableExpr, ctx);
|
|
2548
|
+
const body = stmt.getStatement();
|
|
2549
|
+
if (body.getKind() === SyntaxKind.Block) {
|
|
2550
|
+
walkBlock(body, nodeId, ctx, "item");
|
|
2551
|
+
}
|
|
2552
|
+
return nodeId;
|
|
2553
|
+
}
|
|
2554
|
+
function processForStatement(stmt, _prevNodeId, ctx, line) {
|
|
2555
|
+
const nodeId = ctx.nextId("loop");
|
|
2556
|
+
const fullText = stmt.getText().split("{")[0].trim();
|
|
2557
|
+
ctx.addNode({
|
|
2558
|
+
id: nodeId,
|
|
2559
|
+
nodeType: "for_loop" /* FOR_LOOP */,
|
|
2560
|
+
category: "logic" /* LOGIC */,
|
|
2561
|
+
label: truncate(fullText, 50),
|
|
2562
|
+
params: {
|
|
2563
|
+
iterableExpression: fullText,
|
|
2564
|
+
itemVariable: "i"
|
|
2565
|
+
},
|
|
2566
|
+
inputs: [{ id: "iterable", label: "Iterable", dataType: "array", required: true }],
|
|
2567
|
+
outputs: [
|
|
2568
|
+
{ id: "item", label: "Item", dataType: "any" },
|
|
2569
|
+
{ id: "result", label: "Result", dataType: "array" }
|
|
2570
|
+
]
|
|
2571
|
+
}, line);
|
|
2572
|
+
const body = stmt.getStatement();
|
|
2573
|
+
if (body.getKind() === SyntaxKind.Block) {
|
|
2574
|
+
walkBlock(body, nodeId, ctx, "item");
|
|
2575
|
+
}
|
|
2576
|
+
return nodeId;
|
|
2577
|
+
}
|
|
2578
|
+
function processTryStatement(stmt, _prevNodeId, ctx, line) {
|
|
2579
|
+
const nodeId = ctx.nextId("trycatch");
|
|
2580
|
+
const catchClause = stmt.getCatchClause();
|
|
2581
|
+
const errorVar = catchClause?.getVariableDeclaration()?.getName() ?? "error";
|
|
2582
|
+
ctx.addNode({
|
|
2583
|
+
id: nodeId,
|
|
2584
|
+
nodeType: "try_catch" /* TRY_CATCH */,
|
|
2585
|
+
category: "logic" /* LOGIC */,
|
|
2586
|
+
label: "Try / Catch",
|
|
2587
|
+
params: { errorVariable: errorVar },
|
|
2588
|
+
inputs: [{ id: "input", label: "Input", dataType: "any", required: true }],
|
|
2589
|
+
outputs: [
|
|
2590
|
+
{ id: "success", label: "Success", dataType: "any" },
|
|
2591
|
+
{ id: "error", label: "Error", dataType: "object" }
|
|
2592
|
+
]
|
|
2593
|
+
}, line);
|
|
2594
|
+
const tryBlock = stmt.getTryBlock();
|
|
2595
|
+
walkBlock(tryBlock, nodeId, ctx, "success");
|
|
2596
|
+
if (catchClause) {
|
|
2597
|
+
const catchBlock = catchClause.getBlock();
|
|
2598
|
+
walkBlock(catchBlock, nodeId, ctx, "error");
|
|
2599
|
+
}
|
|
2600
|
+
return nodeId;
|
|
2601
|
+
}
|
|
2602
|
+
function processReturnStatement(stmt, _prevNodeId, ctx, line) {
|
|
2603
|
+
const nodeId = ctx.nextId("response");
|
|
2604
|
+
const returnExpr = stmt.getExpression();
|
|
2605
|
+
const returnText = returnExpr?.getText() ?? "";
|
|
2606
|
+
const isHttpResponse = returnText.includes("NextResponse") || returnText.includes("Response(") || returnText.includes(".json(") || returnText.includes("res.status") || returnText.includes("res.json");
|
|
2607
|
+
if (isHttpResponse) {
|
|
2608
|
+
const statusMatch = returnText.match(/status[:\s(]+(\d{3})/);
|
|
2609
|
+
const bodyMatch = returnText.match(/\.json\((.+?)(?:,|\))/s);
|
|
2610
|
+
ctx.addNode({
|
|
2611
|
+
id: nodeId,
|
|
2612
|
+
nodeType: "return_response" /* RETURN_RESPONSE */,
|
|
2613
|
+
category: "output" /* OUTPUT */,
|
|
2614
|
+
label: `Response ${statusMatch?.[1] ?? "200"}`,
|
|
2615
|
+
params: {
|
|
2616
|
+
statusCode: statusMatch ? parseInt(statusMatch[1]) : 200,
|
|
2617
|
+
bodyExpression: bodyMatch?.[1]?.trim() ?? returnText
|
|
2618
|
+
},
|
|
2619
|
+
inputs: [{ id: "data", label: "Data", dataType: "any", required: true }],
|
|
2620
|
+
outputs: []
|
|
2621
|
+
}, line);
|
|
2622
|
+
} else {
|
|
2623
|
+
ctx.addNode({
|
|
2624
|
+
id: nodeId,
|
|
2625
|
+
nodeType: "return_response" /* RETURN_RESPONSE */,
|
|
2626
|
+
category: "output" /* OUTPUT */,
|
|
2627
|
+
label: "Return",
|
|
2628
|
+
params: {
|
|
2629
|
+
statusCode: 200,
|
|
2630
|
+
bodyExpression: returnText || "undefined"
|
|
2631
|
+
},
|
|
2632
|
+
inputs: [{ id: "data", label: "Data", dataType: "any", required: true }],
|
|
2633
|
+
outputs: []
|
|
2634
|
+
}, line);
|
|
2635
|
+
}
|
|
2636
|
+
trackVariableUses(nodeId, returnText, ctx);
|
|
2637
|
+
return nodeId;
|
|
2638
|
+
}
|
|
2639
|
+
function processExpressionStatement(stmt, _prevNodeId, ctx, line) {
|
|
2640
|
+
const expr = stmt.getExpression();
|
|
2641
|
+
const exprText = expr.getText();
|
|
2642
|
+
if (isFetchCall(expr)) {
|
|
2643
|
+
const nodeId = ctx.nextId("fetch");
|
|
2644
|
+
const node = parseFetchNode(nodeId, exprText, void 0);
|
|
2645
|
+
ctx.addNode(node, line);
|
|
2646
|
+
return nodeId;
|
|
2647
|
+
}
|
|
2648
|
+
if (hasAwaitExpression(expr)) {
|
|
2649
|
+
const nodeId = ctx.nextId("async_op");
|
|
2650
|
+
const awaitedCode = extractAwaitedExpression(expr);
|
|
2651
|
+
ctx.addNode({
|
|
2652
|
+
id: nodeId,
|
|
2653
|
+
nodeType: "custom_code" /* CUSTOM_CODE */,
|
|
2654
|
+
category: "action" /* ACTION */,
|
|
2655
|
+
label: inferLabel(awaitedCode, void 0),
|
|
2656
|
+
params: { code: awaitedCode },
|
|
2657
|
+
inputs: [{ id: "input", label: "Input", dataType: "any", required: false }],
|
|
2658
|
+
outputs: [{ id: "result", label: "Result", dataType: "any" }]
|
|
2659
|
+
}, line);
|
|
2660
|
+
trackVariableUses(nodeId, exprText, ctx);
|
|
2661
|
+
return nodeId;
|
|
2662
|
+
}
|
|
2663
|
+
const flowStateMatch = exprText.match(/flowState\['([^']+)'\]\s*=\s*(.+)/s);
|
|
2664
|
+
if (flowStateMatch) {
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2667
|
+
if (exprText.includes("res.status") || exprText.includes("res.json")) {
|
|
2668
|
+
const nodeId = ctx.nextId("response");
|
|
2669
|
+
const statusMatch = exprText.match(/status\((\d+)\)/);
|
|
2670
|
+
ctx.addNode({
|
|
2671
|
+
id: nodeId,
|
|
2672
|
+
nodeType: "return_response" /* RETURN_RESPONSE */,
|
|
2673
|
+
category: "output" /* OUTPUT */,
|
|
2674
|
+
label: `Response ${statusMatch?.[1] ?? "200"}`,
|
|
2675
|
+
params: {
|
|
2676
|
+
statusCode: statusMatch ? parseInt(statusMatch[1]) : 200,
|
|
2677
|
+
bodyExpression: exprText
|
|
2678
|
+
},
|
|
2679
|
+
inputs: [{ id: "data", label: "Data", dataType: "any", required: true }],
|
|
2680
|
+
outputs: []
|
|
2681
|
+
}, line);
|
|
2682
|
+
trackVariableUses(nodeId, exprText, ctx);
|
|
2683
|
+
return nodeId;
|
|
2684
|
+
}
|
|
2685
|
+
return null;
|
|
2686
|
+
}
|
|
2687
|
+
function buildEdges(ctx) {
|
|
2688
|
+
let edgeCounter = 0;
|
|
2689
|
+
const addEdge = (source, sourcePort, target, targetPort) => {
|
|
2690
|
+
const exists = ctx.edgeList.some(
|
|
2691
|
+
(e) => e.sourceNodeId === source && e.targetNodeId === target && e.sourcePortId === sourcePort
|
|
2692
|
+
);
|
|
2693
|
+
if (exists) return;
|
|
2694
|
+
ctx.edgeList.push({
|
|
2695
|
+
id: `e${++edgeCounter}`,
|
|
2696
|
+
sourceNodeId: source,
|
|
2697
|
+
sourcePortId: sourcePort,
|
|
2698
|
+
targetNodeId: target,
|
|
2699
|
+
targetPortId: targetPort
|
|
2700
|
+
});
|
|
2701
|
+
};
|
|
2702
|
+
const connectedTargets = /* @__PURE__ */ new Set();
|
|
2703
|
+
for (const [nodeId, uses] of ctx.varUses) {
|
|
2704
|
+
for (const use of uses) {
|
|
2705
|
+
const def = ctx.varDefs.get(use.varName);
|
|
2706
|
+
if (def && def.nodeId !== nodeId) {
|
|
2707
|
+
addEdge(def.nodeId, def.portId, nodeId, use.portId);
|
|
2708
|
+
connectedTargets.add(nodeId);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
for (const [parentId, children] of ctx.controlFlowChildren) {
|
|
2713
|
+
for (const child of children) {
|
|
2714
|
+
addEdge(parentId, child.portId, child.nodeId, "input");
|
|
2715
|
+
connectedTargets.add(child.nodeId);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
for (const [nodeId, predId] of ctx.seqPredecessors) {
|
|
2719
|
+
if (connectedTargets.has(nodeId)) continue;
|
|
2720
|
+
const predNode = ctx.nodes.get(predId);
|
|
2721
|
+
if (!predNode) continue;
|
|
2722
|
+
const sourcePort = predNode.outputs[0]?.id ?? "output";
|
|
2723
|
+
addEdge(predId, sourcePort, nodeId, "input");
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
function computeAuditHints(ctx) {
|
|
2727
|
+
const hints = [...ctx.auditData];
|
|
2728
|
+
for (const [nodeId, node] of ctx.nodes) {
|
|
2729
|
+
const line = ctx.nodeLines.get(nodeId);
|
|
2730
|
+
if (node.nodeType === "custom_code" /* CUSTOM_CODE */ || node.nodeType === "fetch_api" /* FETCH_API */) {
|
|
2731
|
+
const isInsideTryCatch = Array.from(ctx.controlFlowChildren.entries()).some(
|
|
2732
|
+
([parentId, children]) => {
|
|
2733
|
+
const parent = ctx.nodes.get(parentId);
|
|
2734
|
+
return parent?.nodeType === "try_catch" /* TRY_CATCH */ && children.some((c) => c.nodeId === nodeId);
|
|
2735
|
+
}
|
|
2736
|
+
);
|
|
2737
|
+
let ancestorId = ctx.seqPredecessors.get(nodeId);
|
|
2738
|
+
let foundTryCatch = isInsideTryCatch;
|
|
2739
|
+
while (ancestorId && !foundTryCatch) {
|
|
2740
|
+
const ancestor = ctx.nodes.get(ancestorId);
|
|
2741
|
+
if (ancestor?.nodeType === "try_catch" /* TRY_CATCH */) foundTryCatch = true;
|
|
2742
|
+
for (const [parentId, children] of ctx.controlFlowChildren) {
|
|
2743
|
+
const parent = ctx.nodes.get(parentId);
|
|
2744
|
+
if (parent?.nodeType === "try_catch" /* TRY_CATCH */ && children.some((c) => c.nodeId === nodeId)) {
|
|
2745
|
+
foundTryCatch = true;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
ancestorId = ctx.seqPredecessors.get(ancestorId);
|
|
2749
|
+
}
|
|
2750
|
+
if (!foundTryCatch) {
|
|
2751
|
+
hints.push({
|
|
2752
|
+
nodeId,
|
|
2753
|
+
severity: "warning",
|
|
2754
|
+
message: `Async operation "${node.label}" has no error handling (missing try/catch)`,
|
|
2755
|
+
line
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
if (node.nodeType === "fetch_api" /* FETCH_API */) {
|
|
2760
|
+
hints.push({
|
|
2761
|
+
nodeId,
|
|
2762
|
+
severity: "info",
|
|
2763
|
+
message: "Consider checking response.ok or response.status after fetch",
|
|
2764
|
+
line
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return hints;
|
|
2769
|
+
}
|
|
2770
|
+
function isFetchCall(node) {
|
|
2771
|
+
const text = node.getText();
|
|
2772
|
+
return text.includes("fetch(") && (text.includes("await") || node.getKind() === SyntaxKind.AwaitExpression);
|
|
2773
|
+
}
|
|
2774
|
+
function hasAwaitExpression(node) {
|
|
2775
|
+
if (node.getKind() === SyntaxKind.AwaitExpression) return true;
|
|
2776
|
+
return node.getText().startsWith("await ");
|
|
2777
|
+
}
|
|
2778
|
+
function extractAwaitedExpression(node) {
|
|
2779
|
+
const text = node.getText();
|
|
2780
|
+
if (text.startsWith("await ")) return text.slice(6);
|
|
2781
|
+
return text;
|
|
2782
|
+
}
|
|
2783
|
+
function isSimpleDeclaration(node) {
|
|
2784
|
+
const text = node.getText();
|
|
2785
|
+
return !text.includes("await") && !text.includes("fetch(") && (node.getKind() === SyntaxKind.StringLiteral || node.getKind() === SyntaxKind.NumericLiteral || node.getKind() === SyntaxKind.TrueKeyword || node.getKind() === SyntaxKind.FalseKeyword || node.getKind() === SyntaxKind.NullKeyword || node.getKind() === SyntaxKind.ArrayLiteralExpression || node.getKind() === SyntaxKind.ObjectLiteralExpression || node.getKind() === SyntaxKind.NewExpression);
|
|
2786
|
+
}
|
|
2787
|
+
function parseFetchNode(nodeId, text, varName) {
|
|
2788
|
+
const urlMatch = text.match(/fetch\(([^,)]+)/);
|
|
2789
|
+
let url = urlMatch?.[1]?.trim() ?? '""';
|
|
2790
|
+
url = url.replace(/^[`"']|[`"']$/g, "");
|
|
2791
|
+
const methodMatch = text.match(/method:\s*["'](\w+)["']/);
|
|
2792
|
+
const method = methodMatch?.[1]?.toUpperCase() ?? "GET";
|
|
2793
|
+
const headerMatch = text.match(/headers:\s*(\{[^}]+\})/);
|
|
2794
|
+
let headers;
|
|
2795
|
+
if (headerMatch) {
|
|
2796
|
+
headers = {};
|
|
2797
|
+
const headerPairs = headerMatch[1].matchAll(/"([^"]+)":\s*"([^"]+)"/g);
|
|
2798
|
+
for (const pair of headerPairs) {
|
|
2799
|
+
headers[pair[1]] = pair[2];
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
return {
|
|
2803
|
+
id: nodeId,
|
|
2804
|
+
nodeType: "fetch_api" /* FETCH_API */,
|
|
2805
|
+
category: "action" /* ACTION */,
|
|
2806
|
+
label: `Fetch ${truncate(url, 30) || varName || "API"}`,
|
|
2807
|
+
params: {
|
|
2808
|
+
url,
|
|
2809
|
+
method,
|
|
2810
|
+
headers,
|
|
2811
|
+
parseJson: text.includes(".json()")
|
|
2812
|
+
},
|
|
2813
|
+
inputs: [{ id: "input", label: "Input", dataType: "any", required: false }],
|
|
2814
|
+
outputs: [
|
|
2815
|
+
{ id: "response", label: "Response", dataType: "object" },
|
|
2816
|
+
{ id: "data", label: "Data", dataType: "any" }
|
|
2817
|
+
]
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
function inferRoutePath(sourceFile) {
|
|
2821
|
+
const filePath = sourceFile.getFilePath();
|
|
2822
|
+
const appRouterMatch = filePath.match(/\/app\/api\/(.+?)\/route\.(ts|js)/);
|
|
2823
|
+
if (appRouterMatch) return `/api/${appRouterMatch[1]}`;
|
|
2824
|
+
const pagesMatch = filePath.match(/\/pages\/api\/(.+?)\.(ts|js)/);
|
|
2825
|
+
if (pagesMatch) return `/api/${pagesMatch[1]}`;
|
|
2826
|
+
const fullText = sourceFile.getFullText();
|
|
2827
|
+
const routeMatch = fullText.match(/\/api\/\S+/);
|
|
2828
|
+
if (routeMatch) return routeMatch[0];
|
|
2829
|
+
return "/api/handler";
|
|
2830
|
+
}
|
|
2831
|
+
function inferDataType(tsType) {
|
|
2832
|
+
if (tsType.includes("string")) return "string";
|
|
2833
|
+
if (tsType.includes("number")) return "number";
|
|
2834
|
+
if (tsType.includes("boolean")) return "boolean";
|
|
2835
|
+
if (tsType.includes("[]") || tsType.includes("Array")) return "array";
|
|
2836
|
+
if (tsType.includes("{") || tsType.includes("Record") || tsType.includes("object")) return "object";
|
|
2837
|
+
return "any";
|
|
2838
|
+
}
|
|
2839
|
+
function inferLabel(code, varName) {
|
|
2840
|
+
const callMatch = code.match(/^(\w+(?:\.\w+)*)\(/);
|
|
2841
|
+
if (callMatch) return callMatch[1];
|
|
2842
|
+
if (varName) return varName;
|
|
2843
|
+
return truncate(code, 30);
|
|
2844
|
+
}
|
|
2845
|
+
function trackVariableUses(nodeId, expression, ctx) {
|
|
2846
|
+
const identifiers = expression.match(/\b([a-zA-Z_]\w*)\b/g);
|
|
2847
|
+
if (!identifiers) return;
|
|
2848
|
+
const uses = [];
|
|
2849
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2850
|
+
for (const ident of identifiers) {
|
|
2851
|
+
if (SKIP_IDENTIFIERS.has(ident)) continue;
|
|
2852
|
+
if (seen.has(ident)) continue;
|
|
2853
|
+
seen.add(ident);
|
|
2854
|
+
if (ctx.varDefs.has(ident)) {
|
|
2855
|
+
uses.push({ nodeId, portId: "input", varName: ident });
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
if (uses.length > 0) {
|
|
2859
|
+
const existing = ctx.varUses.get(nodeId) ?? [];
|
|
2860
|
+
ctx.varUses.set(nodeId, [...existing, ...uses]);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
var SKIP_IDENTIFIERS = /* @__PURE__ */ new Set([
|
|
2864
|
+
// JS keywords
|
|
2865
|
+
"const",
|
|
2866
|
+
"let",
|
|
2867
|
+
"var",
|
|
2868
|
+
"function",
|
|
2869
|
+
"return",
|
|
2870
|
+
"if",
|
|
2871
|
+
"else",
|
|
2872
|
+
"for",
|
|
2873
|
+
"while",
|
|
2874
|
+
"do",
|
|
2875
|
+
"switch",
|
|
2876
|
+
"case",
|
|
2877
|
+
"break",
|
|
2878
|
+
"continue",
|
|
2879
|
+
"throw",
|
|
2880
|
+
"try",
|
|
2881
|
+
"catch",
|
|
2882
|
+
"finally",
|
|
2883
|
+
"new",
|
|
2884
|
+
"delete",
|
|
2885
|
+
"typeof",
|
|
2886
|
+
"void",
|
|
2887
|
+
"in",
|
|
2888
|
+
"of",
|
|
2889
|
+
"instanceof",
|
|
2890
|
+
"this",
|
|
2891
|
+
"super",
|
|
2892
|
+
"class",
|
|
2893
|
+
"extends",
|
|
2894
|
+
"import",
|
|
2895
|
+
"export",
|
|
2896
|
+
"default",
|
|
2897
|
+
"from",
|
|
2898
|
+
"as",
|
|
2899
|
+
"async",
|
|
2900
|
+
"await",
|
|
2901
|
+
"yield",
|
|
2902
|
+
"true",
|
|
2903
|
+
"false",
|
|
2904
|
+
"null",
|
|
2905
|
+
"undefined",
|
|
2906
|
+
// Common globals
|
|
2907
|
+
"console",
|
|
2908
|
+
"JSON",
|
|
2909
|
+
"Math",
|
|
2910
|
+
"Date",
|
|
2911
|
+
"Error",
|
|
2912
|
+
"Promise",
|
|
2913
|
+
"Array",
|
|
2914
|
+
"Object",
|
|
2915
|
+
"String",
|
|
2916
|
+
"Number",
|
|
2917
|
+
"Boolean",
|
|
2918
|
+
"Map",
|
|
2919
|
+
"Set",
|
|
2920
|
+
"RegExp",
|
|
2921
|
+
"Symbol",
|
|
2922
|
+
"Buffer",
|
|
2923
|
+
"process",
|
|
2924
|
+
"setTimeout",
|
|
2925
|
+
"setInterval",
|
|
2926
|
+
"clearTimeout",
|
|
2927
|
+
"clearInterval",
|
|
2928
|
+
"fetch",
|
|
2929
|
+
"Response",
|
|
2930
|
+
"Request",
|
|
2931
|
+
"Headers",
|
|
2932
|
+
"URL",
|
|
2933
|
+
"URLSearchParams",
|
|
2934
|
+
"NextResponse",
|
|
2935
|
+
"NextRequest",
|
|
2936
|
+
// Common methods
|
|
2937
|
+
"log",
|
|
2938
|
+
"error",
|
|
2939
|
+
"warn",
|
|
2940
|
+
"stringify",
|
|
2941
|
+
"parse",
|
|
2942
|
+
"json",
|
|
2943
|
+
"text",
|
|
2944
|
+
"toString",
|
|
2945
|
+
"map",
|
|
2946
|
+
"filter",
|
|
2947
|
+
"reduce",
|
|
2948
|
+
"forEach",
|
|
2949
|
+
"find",
|
|
2950
|
+
"some",
|
|
2951
|
+
"every",
|
|
2952
|
+
"includes",
|
|
2953
|
+
"push",
|
|
2954
|
+
"pop",
|
|
2955
|
+
"shift",
|
|
2956
|
+
"unshift",
|
|
2957
|
+
"slice",
|
|
2958
|
+
"splice",
|
|
2959
|
+
"concat",
|
|
2960
|
+
"join",
|
|
2961
|
+
"keys",
|
|
2962
|
+
"values",
|
|
2963
|
+
"entries",
|
|
2964
|
+
"assign",
|
|
2965
|
+
"freeze",
|
|
2966
|
+
"status",
|
|
2967
|
+
"ok",
|
|
2968
|
+
"headers",
|
|
2969
|
+
"body",
|
|
2970
|
+
"method",
|
|
2971
|
+
"url",
|
|
2972
|
+
"length",
|
|
2973
|
+
"trim",
|
|
2974
|
+
"split",
|
|
2975
|
+
"replace",
|
|
2976
|
+
"match",
|
|
2977
|
+
"test",
|
|
2978
|
+
"exec",
|
|
2979
|
+
"parseInt",
|
|
2980
|
+
"parseFloat",
|
|
2981
|
+
"isNaN",
|
|
2982
|
+
"isFinite",
|
|
2983
|
+
"then",
|
|
2984
|
+
"catch",
|
|
2985
|
+
"finally"
|
|
2986
|
+
]);
|
|
2987
|
+
function truncate(str, maxLen) {
|
|
2988
|
+
if (str.length <= maxLen) return str;
|
|
2989
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2178
2992
|
// src/lib/ir/security.ts
|
|
2179
2993
|
var SECURITY_PATTERNS = [
|
|
2180
2994
|
// ── Critical: Remote Code Execution / System Access ──
|
|
@@ -2208,7 +3022,7 @@ var SECURITY_PATTERNS = [
|
|
|
2208
3022
|
{ pattern: /\bwhile\s*\(\s*true\s*\)/, desc: "while(true) \u2014 possible infinite loop", severity: "info" },
|
|
2209
3023
|
{ pattern: /\bfor\s*\(\s*;\s*;\s*\)/, desc: "for(;;) \u2014 possible infinite loop", severity: "info" }
|
|
2210
3024
|
];
|
|
2211
|
-
function
|
|
3025
|
+
function truncate2(str, maxLen) {
|
|
2212
3026
|
return str.length > maxLen ? str.slice(0, maxLen) + "\u2026" : str;
|
|
2213
3027
|
}
|
|
2214
3028
|
function scanCode(nodeId, nodeLabel, code) {
|
|
@@ -2222,7 +3036,7 @@ function scanCode(nodeId, nodeLabel, code) {
|
|
|
2222
3036
|
nodeId,
|
|
2223
3037
|
nodeLabel,
|
|
2224
3038
|
pattern: desc,
|
|
2225
|
-
match:
|
|
3039
|
+
match: truncate2(match[0], 80)
|
|
2226
3040
|
});
|
|
2227
3041
|
}
|
|
2228
3042
|
}
|
|
@@ -2822,6 +3636,50 @@ function handleImportOpenAPI(body) {
|
|
|
2822
3636
|
};
|
|
2823
3637
|
}
|
|
2824
3638
|
}
|
|
3639
|
+
function handleDecompile(body) {
|
|
3640
|
+
try {
|
|
3641
|
+
const { code, fileName, functionName } = body;
|
|
3642
|
+
if (!code || typeof code !== "string") {
|
|
3643
|
+
return { status: 400, body: { success: false, error: "Missing 'code' string in request body" } };
|
|
3644
|
+
}
|
|
3645
|
+
if (code.trim().length === 0) {
|
|
3646
|
+
return { status: 400, body: { success: false, error: "Code is empty" } };
|
|
3647
|
+
}
|
|
3648
|
+
const result = decompile(code, {
|
|
3649
|
+
fileName: fileName ?? "input.ts",
|
|
3650
|
+
functionName,
|
|
3651
|
+
audit: true
|
|
3652
|
+
});
|
|
3653
|
+
if (!result.success) {
|
|
3654
|
+
return {
|
|
3655
|
+
status: 422,
|
|
3656
|
+
body: {
|
|
3657
|
+
success: false,
|
|
3658
|
+
errors: result.errors ?? ["Decompile failed"],
|
|
3659
|
+
confidence: result.confidence
|
|
3660
|
+
}
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
return {
|
|
3664
|
+
status: 200,
|
|
3665
|
+
body: {
|
|
3666
|
+
success: true,
|
|
3667
|
+
ir: result.ir,
|
|
3668
|
+
confidence: result.confidence,
|
|
3669
|
+
errors: result.errors ?? [],
|
|
3670
|
+
audit: result.audit ?? []
|
|
3671
|
+
}
|
|
3672
|
+
};
|
|
3673
|
+
} catch (err) {
|
|
3674
|
+
return {
|
|
3675
|
+
status: 500,
|
|
3676
|
+
body: {
|
|
3677
|
+
success: false,
|
|
3678
|
+
error: `Decompile error: ${err instanceof Error ? err.message : String(err)}`
|
|
3679
|
+
}
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
2825
3683
|
|
|
2826
3684
|
// src/server/index.ts
|
|
2827
3685
|
var __filename = fileURLToPath(import.meta.url);
|
|
@@ -2986,6 +3844,11 @@ async function handleRequest(req, res, staticDir, projectRoot) {
|
|
|
2986
3844
|
sendJson(res, result.status, result.body);
|
|
2987
3845
|
return;
|
|
2988
3846
|
}
|
|
3847
|
+
if (pathname === "/api/decompile") {
|
|
3848
|
+
const result = handleDecompile(body);
|
|
3849
|
+
sendJson(res, result.status, result.body);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
2989
3852
|
sendJson(res, 404, { error: `Unknown API route: ${pathname}` });
|
|
2990
3853
|
return;
|
|
2991
3854
|
}
|
|
@@ -3013,6 +3876,7 @@ function startServer(options = {}) {
|
|
|
3013
3876
|
res.end("Internal Server Error");
|
|
3014
3877
|
});
|
|
3015
3878
|
});
|
|
3879
|
+
const hasUI = existsSync2(join2(staticDir, "index.html"));
|
|
3016
3880
|
server.listen(port, host, () => {
|
|
3017
3881
|
const url = `http://localhost:${port}`;
|
|
3018
3882
|
if (options.onReady) {
|
|
@@ -3023,14 +3887,22 @@ function startServer(options = {}) {
|
|
|
3023
3887
|
console.log(` \u251C\u2500 Local: ${url}`);
|
|
3024
3888
|
console.log(` \u251C\u2500 API: ${url}/api/compile`);
|
|
3025
3889
|
console.log(` \u251C\u2500 Static: ${staticDir}`);
|
|
3026
|
-
console.log(` \u2514\u2500 Project: ${projectRoot}
|
|
3027
|
-
|
|
3890
|
+
console.log(` \u2514\u2500 Project: ${projectRoot}`);
|
|
3891
|
+
if (!hasUI) {
|
|
3892
|
+
console.log();
|
|
3893
|
+
console.log(` \u26A0\uFE0F UI files not found (out/index.html missing).`);
|
|
3894
|
+
console.log(` The API endpoints still work, but the visual editor won't load.`);
|
|
3895
|
+
console.log(` To fix: run "pnpm build:ui" in the flow2code source directory,`);
|
|
3896
|
+
console.log(` or reinstall from npm: npm i @timo9378/flow2code@latest`);
|
|
3897
|
+
}
|
|
3898
|
+
console.log();
|
|
3028
3899
|
}
|
|
3029
3900
|
});
|
|
3030
3901
|
return server;
|
|
3031
3902
|
}
|
|
3032
3903
|
export {
|
|
3033
3904
|
handleCompile,
|
|
3905
|
+
handleDecompile,
|
|
3034
3906
|
handleGenerate,
|
|
3035
3907
|
handleImportOpenAPI,
|
|
3036
3908
|
startServer
|