@timo9378/flow2code 0.1.7 → 0.1.8
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 +25 -0
- package/dist/cli.js +65 -28
- package/dist/compiler.cjs +51 -25
- package/dist/compiler.js +51 -25
- package/dist/server.js +64 -27
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +2 -2
- package/out/__next._full.txt +2 -2
- package/out/__next._head.txt +1 -1
- package/out/__next._index.txt +1 -1
- package/out/__next._tree.txt +1 -1
- package/out/_next/static/chunks/{fd3c7cf5ea219c74.js → 06054f68c210e89c.js} +1 -1
- package/out/_next/static/chunks/{2fd98ca28cbab9a6.js → 0bc0a50347ee5f3c.js} +14 -12
- package/out/_not-found/__next._full.txt +1 -1
- package/out/_not-found/__next._head.txt +1 -1
- package/out/_not-found/__next._index.txt +1 -1
- package/out/_not-found/__next._not-found/__PAGE__.txt +1 -1
- package/out/_not-found/__next._not-found.txt +1 -1
- package/out/_not-found/__next._tree.txt +1 -1
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +1 -1
- package/out/index.html +2 -2
- package/out/index.txt +2 -2
- package/package.json +1 -1
- /package/out/_next/static/{4eCLtLp-IPL1tppPGnHZE → EFK7prtbW4K3cbFdFFkDA}/_buildManifest.js +0 -0
- /package/out/_next/static/{4eCLtLp-IPL1tppPGnHZE → EFK7prtbW4K3cbFdFFkDA}/_clientMiddlewareManifest.json +0 -0
- /package/out/_next/static/{4eCLtLp-IPL1tppPGnHZE → EFK7prtbW4K3cbFdFFkDA}/_ssgManifest.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.1.8] — 2026-03-05
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- **HTTP server request timeout** — Added `headersTimeout` (30s) and `requestTimeout` (60s) to prevent Slowloris-style DoS attacks (#16)
|
|
12
|
+
- **Server `headersSent` guard** — Error handler now checks `res.headersSent` before writing 500 response, preventing `ERR_HTTP_HEADERS_SENT` crash (#14)
|
|
13
|
+
- **Validate IR from server** — `handleDecompileTS` and `handleSelectOpenAPIFlow` now call `validateFlowIR()` before `loadIR()`, matching `handleLoadIRFromJSON` behavior (#22)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Validator iterative DFS** — Replaced recursive `dfs()` with explicit stack-based iteration to prevent stack overflow on deep graphs (5000+ nodes) (#11)
|
|
17
|
+
- **`isFetchCall` AST-based detection** — Replaced `text.includes("fetch(")` string matching with proper AST walk using `CallExpression` + `Identifier` node types (#12)
|
|
18
|
+
- **`hasAwaitExpression` AST walk** — Replaced `text.startsWith("await ")` fallback with recursive `forEachChild` AST traversal to detect nested awaits (#23)
|
|
19
|
+
- **`trackVariableUses` strip strings** — Expression string now has string literals (`"..."`, `'...'`, `` `...` ``) stripped before identifier regex scan, preventing false-positive variable references (#13)
|
|
20
|
+
- **`_nodeCounter` max ID parse** — After `loadIR`, counter is set to max numeric ID from existing node IDs (not `nodes.length`), preventing ID collisions (#15)
|
|
21
|
+
- **`resolveTriggerRef` cache** — Trigger node lookup cached per IR reference, eliminating O(N) linear scan on every `$trigger` expression evaluation (#17)
|
|
22
|
+
- **`$input` ambiguity warning** — `resolveInputRef` now emits `console.warn` when a node has multiple non-trigger upstream edges, identifying which source is used (#18)
|
|
23
|
+
- **OpenAPI tags filter** — `handleImportOpenAPI` now implements `filter.tags` (case-insensitive match against `meta.tags`), which was previously accepted but silently ignored (#20)
|
|
24
|
+
- **Clipboard fallback** — `handleExportIR` now catches clipboard write errors gracefully instead of letting promise rejection propagate (#21)
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **`createPlatformRegistry` factory** — New function for creating isolated platform registry instances, enabling safe test parallelism and concurrent compilation (#19)
|
|
28
|
+
|
|
29
|
+
### Tests
|
|
30
|
+
- Added iterative DFS deep graph test (5000 nodes, verifies no stack overflow)
|
|
31
|
+
- Test count: 411 tests / 33 test files
|
|
32
|
+
|
|
8
33
|
## [0.1.7] — 2026-03-05
|
|
9
34
|
|
|
10
35
|
### Performance
|
package/dist/cli.js
CHANGED
|
@@ -218,25 +218,32 @@ function detectCycles(nodes, edges) {
|
|
|
218
218
|
for (const node of nodes) {
|
|
219
219
|
color.set(node.id, WHITE);
|
|
220
220
|
}
|
|
221
|
-
|
|
222
|
-
color.
|
|
223
|
-
|
|
221
|
+
for (const node of nodes) {
|
|
222
|
+
if (color.get(node.id) !== WHITE) continue;
|
|
223
|
+
const stack = [[node.id, 0]];
|
|
224
|
+
color.set(node.id, GRAY);
|
|
225
|
+
while (stack.length > 0) {
|
|
226
|
+
const top = stack[stack.length - 1];
|
|
227
|
+
const [currentId, idx] = top;
|
|
228
|
+
const neighbors = adjacency.get(currentId) ?? [];
|
|
229
|
+
if (idx >= neighbors.length) {
|
|
230
|
+
color.set(currentId, BLACK);
|
|
231
|
+
stack.pop();
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
top[1] = idx + 1;
|
|
235
|
+
const neighbor = neighbors[idx];
|
|
224
236
|
if (color.get(neighbor) === GRAY) {
|
|
225
237
|
errors.push({
|
|
226
238
|
code: "CYCLE_DETECTED",
|
|
227
|
-
message: `Cycle detected: node "${
|
|
228
|
-
nodeId
|
|
239
|
+
message: `Cycle detected: node "${currentId}" \u2192 "${neighbor}"`,
|
|
240
|
+
nodeId: currentId
|
|
229
241
|
});
|
|
230
242
|
} else if (color.get(neighbor) === WHITE) {
|
|
231
|
-
|
|
243
|
+
color.set(neighbor, GRAY);
|
|
244
|
+
stack.push([neighbor, 0]);
|
|
232
245
|
}
|
|
233
246
|
}
|
|
234
|
-
color.set(nodeId, BLACK);
|
|
235
|
-
}
|
|
236
|
-
for (const node of nodes) {
|
|
237
|
-
if (color.get(node.id) === WHITE) {
|
|
238
|
-
dfs(node.id);
|
|
239
|
-
}
|
|
240
247
|
}
|
|
241
248
|
return errors;
|
|
242
249
|
}
|
|
@@ -450,10 +457,16 @@ function resolveInputRef(path, context) {
|
|
|
450
457
|
const incoming = context.ir.edges.filter(
|
|
451
458
|
(e) => e.targetNodeId === context.currentNodeId
|
|
452
459
|
);
|
|
453
|
-
const
|
|
460
|
+
const nonTriggerIncoming = incoming.filter((e) => {
|
|
454
461
|
const src = context.nodeMap.get(e.sourceNodeId);
|
|
455
462
|
return src && src.category !== "trigger" /* TRIGGER */;
|
|
456
|
-
})
|
|
463
|
+
});
|
|
464
|
+
if (nonTriggerIncoming.length > 1 && typeof console !== "undefined") {
|
|
465
|
+
console.warn(
|
|
466
|
+
`[flow2code] $input is ambiguous: node "${context.currentNodeId}" has ${nonTriggerIncoming.length} non-trigger upstream edges. Using first match "${nonTriggerIncoming[0].sourceNodeId}".`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const dataSource = nonTriggerIncoming[0] || incoming[0];
|
|
457
470
|
if (dataSource) {
|
|
458
471
|
const srcId = dataSource.sourceNodeId;
|
|
459
472
|
if (context.blockScopedNodeIds?.has(srcId)) {
|
|
@@ -469,21 +482,27 @@ function resolveInputRef(path, context) {
|
|
|
469
482
|
);
|
|
470
483
|
}
|
|
471
484
|
function resolveTriggerRef(path, context) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
485
|
+
if (_cachedTriggerIR !== context.ir) {
|
|
486
|
+
_cachedTriggerIR = context.ir;
|
|
487
|
+
const trigger = context.ir.nodes.find(
|
|
488
|
+
(n) => n.category === "trigger" /* TRIGGER */
|
|
489
|
+
);
|
|
490
|
+
_cachedTriggerId = trigger?.id ?? null;
|
|
491
|
+
}
|
|
492
|
+
if (_cachedTriggerId) {
|
|
493
|
+
if (context.symbolTable?.hasVar(_cachedTriggerId)) {
|
|
494
|
+
return `${context.symbolTable.getVarName(_cachedTriggerId)}${path}`;
|
|
478
495
|
}
|
|
479
|
-
return `flowState['${
|
|
496
|
+
return `flowState['${_cachedTriggerId}']${path}`;
|
|
480
497
|
}
|
|
481
498
|
return "undefined";
|
|
482
499
|
}
|
|
483
|
-
var ExpressionParseError;
|
|
500
|
+
var _cachedTriggerIR, _cachedTriggerId, ExpressionParseError;
|
|
484
501
|
var init_expression_parser = __esm({
|
|
485
502
|
"src/lib/compiler/expression-parser.ts"() {
|
|
486
503
|
"use strict";
|
|
504
|
+
_cachedTriggerIR = null;
|
|
505
|
+
_cachedTriggerId = null;
|
|
487
506
|
ExpressionParseError = class extends Error {
|
|
488
507
|
constructor(message, expression, position) {
|
|
489
508
|
super(message);
|
|
@@ -2987,12 +3006,18 @@ function computeAuditHints(ctx) {
|
|
|
2987
3006
|
return hints;
|
|
2988
3007
|
}
|
|
2989
3008
|
function isFetchCall(node) {
|
|
2990
|
-
|
|
2991
|
-
|
|
3009
|
+
if (node.getKind() === SyntaxKind.CallExpression) {
|
|
3010
|
+
const expr = node.getExpression();
|
|
3011
|
+
if (expr.getKind() === SyntaxKind.Identifier && expr.getText() === "fetch") return true;
|
|
3012
|
+
}
|
|
3013
|
+
if (node.getKind() === SyntaxKind.AwaitExpression) {
|
|
3014
|
+
return isFetchCall(node.getChildAtIndex(1) ?? node);
|
|
3015
|
+
}
|
|
3016
|
+
return node.forEachChild((child) => isFetchCall(child) || void 0) ?? false;
|
|
2992
3017
|
}
|
|
2993
3018
|
function hasAwaitExpression(node) {
|
|
2994
3019
|
if (node.getKind() === SyntaxKind.AwaitExpression) return true;
|
|
2995
|
-
return node.
|
|
3020
|
+
return node.forEachChild((child) => hasAwaitExpression(child) || void 0) ?? false;
|
|
2996
3021
|
}
|
|
2997
3022
|
function extractAwaitedExpression(node) {
|
|
2998
3023
|
const text = node.getText();
|
|
@@ -3062,7 +3087,8 @@ function inferLabel(code, varName) {
|
|
|
3062
3087
|
return truncate(code, 30);
|
|
3063
3088
|
}
|
|
3064
3089
|
function trackVariableUses(nodeId, expression, ctx) {
|
|
3065
|
-
const
|
|
3090
|
+
const stripped = expression.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, "");
|
|
3091
|
+
const identifiers = stripped.match(/\b([a-zA-Z_]\w*)\b/g);
|
|
3066
3092
|
if (!identifiers) return;
|
|
3067
3093
|
const uses = [];
|
|
3068
3094
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3868,6 +3894,13 @@ function handleImportOpenAPI(body) {
|
|
|
3868
3894
|
(flow) => paths.some((p) => flow.meta.name.includes(p))
|
|
3869
3895
|
);
|
|
3870
3896
|
}
|
|
3897
|
+
if (body.filter?.tags && Array.isArray(body.filter.tags)) {
|
|
3898
|
+
const tags = body.filter.tags.map((t) => t.toLowerCase());
|
|
3899
|
+
filteredFlows = filteredFlows.filter((flow) => {
|
|
3900
|
+
const flowTags = flow.meta.tags ?? [];
|
|
3901
|
+
return flowTags.some((t) => tags.includes(t.toLowerCase()));
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3871
3904
|
return {
|
|
3872
3905
|
status: 200,
|
|
3873
3906
|
body: {
|
|
@@ -4128,10 +4161,14 @@ function startServer(options = {}) {
|
|
|
4128
4161
|
const server = createServer((req, res) => {
|
|
4129
4162
|
handleRequest(req, res, staticDir, projectRoot).catch((err) => {
|
|
4130
4163
|
logger.error("Internal error:", err);
|
|
4131
|
-
res.
|
|
4132
|
-
|
|
4164
|
+
if (!res.headersSent) {
|
|
4165
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
4166
|
+
res.end("Internal Server Error");
|
|
4167
|
+
}
|
|
4133
4168
|
});
|
|
4134
4169
|
});
|
|
4170
|
+
server.headersTimeout = 3e4;
|
|
4171
|
+
server.requestTimeout = 6e4;
|
|
4135
4172
|
const hasUI = existsSync3(join3(staticDir, "index.html"));
|
|
4136
4173
|
server.listen(port, host, () => {
|
|
4137
4174
|
const url = `http://localhost:${port}`;
|
package/dist/compiler.cjs
CHANGED
|
@@ -294,25 +294,32 @@ function detectCycles(nodes, edges) {
|
|
|
294
294
|
for (const node of nodes) {
|
|
295
295
|
color.set(node.id, WHITE);
|
|
296
296
|
}
|
|
297
|
-
|
|
298
|
-
color.
|
|
299
|
-
|
|
297
|
+
for (const node of nodes) {
|
|
298
|
+
if (color.get(node.id) !== WHITE) continue;
|
|
299
|
+
const stack = [[node.id, 0]];
|
|
300
|
+
color.set(node.id, GRAY);
|
|
301
|
+
while (stack.length > 0) {
|
|
302
|
+
const top = stack[stack.length - 1];
|
|
303
|
+
const [currentId, idx] = top;
|
|
304
|
+
const neighbors = adjacency.get(currentId) ?? [];
|
|
305
|
+
if (idx >= neighbors.length) {
|
|
306
|
+
color.set(currentId, BLACK);
|
|
307
|
+
stack.pop();
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
top[1] = idx + 1;
|
|
311
|
+
const neighbor = neighbors[idx];
|
|
300
312
|
if (color.get(neighbor) === GRAY) {
|
|
301
313
|
errors.push({
|
|
302
314
|
code: "CYCLE_DETECTED",
|
|
303
|
-
message: `Cycle detected: node "${
|
|
304
|
-
nodeId
|
|
315
|
+
message: `Cycle detected: node "${currentId}" \u2192 "${neighbor}"`,
|
|
316
|
+
nodeId: currentId
|
|
305
317
|
});
|
|
306
318
|
} else if (color.get(neighbor) === WHITE) {
|
|
307
|
-
|
|
319
|
+
color.set(neighbor, GRAY);
|
|
320
|
+
stack.push([neighbor, 0]);
|
|
308
321
|
}
|
|
309
322
|
}
|
|
310
|
-
color.set(nodeId, BLACK);
|
|
311
|
-
}
|
|
312
|
-
for (const node of nodes) {
|
|
313
|
-
if (color.get(node.id) === WHITE) {
|
|
314
|
-
dfs(node.id);
|
|
315
|
-
}
|
|
316
323
|
}
|
|
317
324
|
return errors;
|
|
318
325
|
}
|
|
@@ -514,10 +521,16 @@ function resolveInputRef(path, context) {
|
|
|
514
521
|
const incoming = context.ir.edges.filter(
|
|
515
522
|
(e) => e.targetNodeId === context.currentNodeId
|
|
516
523
|
);
|
|
517
|
-
const
|
|
524
|
+
const nonTriggerIncoming = incoming.filter((e) => {
|
|
518
525
|
const src = context.nodeMap.get(e.sourceNodeId);
|
|
519
526
|
return src && src.category !== "trigger" /* TRIGGER */;
|
|
520
|
-
})
|
|
527
|
+
});
|
|
528
|
+
if (nonTriggerIncoming.length > 1 && typeof console !== "undefined") {
|
|
529
|
+
console.warn(
|
|
530
|
+
`[flow2code] $input is ambiguous: node "${context.currentNodeId}" has ${nonTriggerIncoming.length} non-trigger upstream edges. Using first match "${nonTriggerIncoming[0].sourceNodeId}".`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
const dataSource = nonTriggerIncoming[0] || incoming[0];
|
|
521
534
|
if (dataSource) {
|
|
522
535
|
const srcId = dataSource.sourceNodeId;
|
|
523
536
|
if (context.blockScopedNodeIds?.has(srcId)) {
|
|
@@ -532,15 +545,21 @@ function resolveInputRef(path, context) {
|
|
|
532
545
|
`Expression parser error: Node "${context.currentNodeId}" has no input connected`
|
|
533
546
|
);
|
|
534
547
|
}
|
|
548
|
+
var _cachedTriggerIR = null;
|
|
549
|
+
var _cachedTriggerId = null;
|
|
535
550
|
function resolveTriggerRef(path, context) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
551
|
+
if (_cachedTriggerIR !== context.ir) {
|
|
552
|
+
_cachedTriggerIR = context.ir;
|
|
553
|
+
const trigger = context.ir.nodes.find(
|
|
554
|
+
(n) => n.category === "trigger" /* TRIGGER */
|
|
555
|
+
);
|
|
556
|
+
_cachedTriggerId = trigger?.id ?? null;
|
|
557
|
+
}
|
|
558
|
+
if (_cachedTriggerId) {
|
|
559
|
+
if (context.symbolTable?.hasVar(_cachedTriggerId)) {
|
|
560
|
+
return `${context.symbolTable.getVarName(_cachedTriggerId)}${path}`;
|
|
542
561
|
}
|
|
543
|
-
return `flowState['${
|
|
562
|
+
return `flowState['${_cachedTriggerId}']${path}`;
|
|
544
563
|
}
|
|
545
564
|
return "undefined";
|
|
546
565
|
}
|
|
@@ -3041,12 +3060,18 @@ function computeAuditHints(ctx) {
|
|
|
3041
3060
|
return hints;
|
|
3042
3061
|
}
|
|
3043
3062
|
function isFetchCall(node) {
|
|
3044
|
-
|
|
3045
|
-
|
|
3063
|
+
if (node.getKind() === import_ts_morph2.SyntaxKind.CallExpression) {
|
|
3064
|
+
const expr = node.getExpression();
|
|
3065
|
+
if (expr.getKind() === import_ts_morph2.SyntaxKind.Identifier && expr.getText() === "fetch") return true;
|
|
3066
|
+
}
|
|
3067
|
+
if (node.getKind() === import_ts_morph2.SyntaxKind.AwaitExpression) {
|
|
3068
|
+
return isFetchCall(node.getChildAtIndex(1) ?? node);
|
|
3069
|
+
}
|
|
3070
|
+
return node.forEachChild((child) => isFetchCall(child) || void 0) ?? false;
|
|
3046
3071
|
}
|
|
3047
3072
|
function hasAwaitExpression(node) {
|
|
3048
3073
|
if (node.getKind() === import_ts_morph2.SyntaxKind.AwaitExpression) return true;
|
|
3049
|
-
return node.
|
|
3074
|
+
return node.forEachChild((child) => hasAwaitExpression(child) || void 0) ?? false;
|
|
3050
3075
|
}
|
|
3051
3076
|
function extractAwaitedExpression(node) {
|
|
3052
3077
|
const text = node.getText();
|
|
@@ -3116,7 +3141,8 @@ function inferLabel(code, varName) {
|
|
|
3116
3141
|
return truncate2(code, 30);
|
|
3117
3142
|
}
|
|
3118
3143
|
function trackVariableUses(nodeId, expression, ctx) {
|
|
3119
|
-
const
|
|
3144
|
+
const stripped = expression.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, "");
|
|
3145
|
+
const identifiers = stripped.match(/\b([a-zA-Z_]\w*)\b/g);
|
|
3120
3146
|
if (!identifiers) return;
|
|
3121
3147
|
const uses = [];
|
|
3122
3148
|
const seen = /* @__PURE__ */ new Set();
|
package/dist/compiler.js
CHANGED
|
@@ -227,25 +227,32 @@ function detectCycles(nodes, edges) {
|
|
|
227
227
|
for (const node of nodes) {
|
|
228
228
|
color.set(node.id, WHITE);
|
|
229
229
|
}
|
|
230
|
-
|
|
231
|
-
color.
|
|
232
|
-
|
|
230
|
+
for (const node of nodes) {
|
|
231
|
+
if (color.get(node.id) !== WHITE) continue;
|
|
232
|
+
const stack = [[node.id, 0]];
|
|
233
|
+
color.set(node.id, GRAY);
|
|
234
|
+
while (stack.length > 0) {
|
|
235
|
+
const top = stack[stack.length - 1];
|
|
236
|
+
const [currentId, idx] = top;
|
|
237
|
+
const neighbors = adjacency.get(currentId) ?? [];
|
|
238
|
+
if (idx >= neighbors.length) {
|
|
239
|
+
color.set(currentId, BLACK);
|
|
240
|
+
stack.pop();
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
top[1] = idx + 1;
|
|
244
|
+
const neighbor = neighbors[idx];
|
|
233
245
|
if (color.get(neighbor) === GRAY) {
|
|
234
246
|
errors.push({
|
|
235
247
|
code: "CYCLE_DETECTED",
|
|
236
|
-
message: `Cycle detected: node "${
|
|
237
|
-
nodeId
|
|
248
|
+
message: `Cycle detected: node "${currentId}" \u2192 "${neighbor}"`,
|
|
249
|
+
nodeId: currentId
|
|
238
250
|
});
|
|
239
251
|
} else if (color.get(neighbor) === WHITE) {
|
|
240
|
-
|
|
252
|
+
color.set(neighbor, GRAY);
|
|
253
|
+
stack.push([neighbor, 0]);
|
|
241
254
|
}
|
|
242
255
|
}
|
|
243
|
-
color.set(nodeId, BLACK);
|
|
244
|
-
}
|
|
245
|
-
for (const node of nodes) {
|
|
246
|
-
if (color.get(node.id) === WHITE) {
|
|
247
|
-
dfs(node.id);
|
|
248
|
-
}
|
|
249
256
|
}
|
|
250
257
|
return errors;
|
|
251
258
|
}
|
|
@@ -447,10 +454,16 @@ function resolveInputRef(path, context) {
|
|
|
447
454
|
const incoming = context.ir.edges.filter(
|
|
448
455
|
(e) => e.targetNodeId === context.currentNodeId
|
|
449
456
|
);
|
|
450
|
-
const
|
|
457
|
+
const nonTriggerIncoming = incoming.filter((e) => {
|
|
451
458
|
const src = context.nodeMap.get(e.sourceNodeId);
|
|
452
459
|
return src && src.category !== "trigger" /* TRIGGER */;
|
|
453
|
-
})
|
|
460
|
+
});
|
|
461
|
+
if (nonTriggerIncoming.length > 1 && typeof console !== "undefined") {
|
|
462
|
+
console.warn(
|
|
463
|
+
`[flow2code] $input is ambiguous: node "${context.currentNodeId}" has ${nonTriggerIncoming.length} non-trigger upstream edges. Using first match "${nonTriggerIncoming[0].sourceNodeId}".`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const dataSource = nonTriggerIncoming[0] || incoming[0];
|
|
454
467
|
if (dataSource) {
|
|
455
468
|
const srcId = dataSource.sourceNodeId;
|
|
456
469
|
if (context.blockScopedNodeIds?.has(srcId)) {
|
|
@@ -465,15 +478,21 @@ function resolveInputRef(path, context) {
|
|
|
465
478
|
`Expression parser error: Node "${context.currentNodeId}" has no input connected`
|
|
466
479
|
);
|
|
467
480
|
}
|
|
481
|
+
var _cachedTriggerIR = null;
|
|
482
|
+
var _cachedTriggerId = null;
|
|
468
483
|
function resolveTriggerRef(path, context) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
484
|
+
if (_cachedTriggerIR !== context.ir) {
|
|
485
|
+
_cachedTriggerIR = context.ir;
|
|
486
|
+
const trigger = context.ir.nodes.find(
|
|
487
|
+
(n) => n.category === "trigger" /* TRIGGER */
|
|
488
|
+
);
|
|
489
|
+
_cachedTriggerId = trigger?.id ?? null;
|
|
490
|
+
}
|
|
491
|
+
if (_cachedTriggerId) {
|
|
492
|
+
if (context.symbolTable?.hasVar(_cachedTriggerId)) {
|
|
493
|
+
return `${context.symbolTable.getVarName(_cachedTriggerId)}${path}`;
|
|
475
494
|
}
|
|
476
|
-
return `flowState['${
|
|
495
|
+
return `flowState['${_cachedTriggerId}']${path}`;
|
|
477
496
|
}
|
|
478
497
|
return "undefined";
|
|
479
498
|
}
|
|
@@ -2977,12 +2996,18 @@ function computeAuditHints(ctx) {
|
|
|
2977
2996
|
return hints;
|
|
2978
2997
|
}
|
|
2979
2998
|
function isFetchCall(node) {
|
|
2980
|
-
|
|
2981
|
-
|
|
2999
|
+
if (node.getKind() === SyntaxKind.CallExpression) {
|
|
3000
|
+
const expr = node.getExpression();
|
|
3001
|
+
if (expr.getKind() === SyntaxKind.Identifier && expr.getText() === "fetch") return true;
|
|
3002
|
+
}
|
|
3003
|
+
if (node.getKind() === SyntaxKind.AwaitExpression) {
|
|
3004
|
+
return isFetchCall(node.getChildAtIndex(1) ?? node);
|
|
3005
|
+
}
|
|
3006
|
+
return node.forEachChild((child) => isFetchCall(child) || void 0) ?? false;
|
|
2982
3007
|
}
|
|
2983
3008
|
function hasAwaitExpression(node) {
|
|
2984
3009
|
if (node.getKind() === SyntaxKind.AwaitExpression) return true;
|
|
2985
|
-
return node.
|
|
3010
|
+
return node.forEachChild((child) => hasAwaitExpression(child) || void 0) ?? false;
|
|
2986
3011
|
}
|
|
2987
3012
|
function extractAwaitedExpression(node) {
|
|
2988
3013
|
const text = node.getText();
|
|
@@ -3052,7 +3077,8 @@ function inferLabel(code, varName) {
|
|
|
3052
3077
|
return truncate2(code, 30);
|
|
3053
3078
|
}
|
|
3054
3079
|
function trackVariableUses(nodeId, expression, ctx) {
|
|
3055
|
-
const
|
|
3080
|
+
const stripped = expression.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, "");
|
|
3081
|
+
const identifiers = stripped.match(/\b([a-zA-Z_]\w*)\b/g);
|
|
3056
3082
|
if (!identifiers) return;
|
|
3057
3083
|
const uses = [];
|
|
3058
3084
|
const seen = /* @__PURE__ */ new Set();
|
package/dist/server.js
CHANGED
|
@@ -198,25 +198,32 @@ function detectCycles(nodes, edges) {
|
|
|
198
198
|
for (const node of nodes) {
|
|
199
199
|
color.set(node.id, WHITE);
|
|
200
200
|
}
|
|
201
|
-
|
|
202
|
-
color.
|
|
203
|
-
|
|
201
|
+
for (const node of nodes) {
|
|
202
|
+
if (color.get(node.id) !== WHITE) continue;
|
|
203
|
+
const stack = [[node.id, 0]];
|
|
204
|
+
color.set(node.id, GRAY);
|
|
205
|
+
while (stack.length > 0) {
|
|
206
|
+
const top = stack[stack.length - 1];
|
|
207
|
+
const [currentId, idx] = top;
|
|
208
|
+
const neighbors = adjacency.get(currentId) ?? [];
|
|
209
|
+
if (idx >= neighbors.length) {
|
|
210
|
+
color.set(currentId, BLACK);
|
|
211
|
+
stack.pop();
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
top[1] = idx + 1;
|
|
215
|
+
const neighbor = neighbors[idx];
|
|
204
216
|
if (color.get(neighbor) === GRAY) {
|
|
205
217
|
errors.push({
|
|
206
218
|
code: "CYCLE_DETECTED",
|
|
207
|
-
message: `Cycle detected: node "${
|
|
208
|
-
nodeId
|
|
219
|
+
message: `Cycle detected: node "${currentId}" \u2192 "${neighbor}"`,
|
|
220
|
+
nodeId: currentId
|
|
209
221
|
});
|
|
210
222
|
} else if (color.get(neighbor) === WHITE) {
|
|
211
|
-
|
|
223
|
+
color.set(neighbor, GRAY);
|
|
224
|
+
stack.push([neighbor, 0]);
|
|
212
225
|
}
|
|
213
226
|
}
|
|
214
|
-
color.set(nodeId, BLACK);
|
|
215
|
-
}
|
|
216
|
-
for (const node of nodes) {
|
|
217
|
-
if (color.get(node.id) === WHITE) {
|
|
218
|
-
dfs(node.id);
|
|
219
|
-
}
|
|
220
227
|
}
|
|
221
228
|
return errors;
|
|
222
229
|
}
|
|
@@ -418,10 +425,16 @@ function resolveInputRef(path, context) {
|
|
|
418
425
|
const incoming = context.ir.edges.filter(
|
|
419
426
|
(e) => e.targetNodeId === context.currentNodeId
|
|
420
427
|
);
|
|
421
|
-
const
|
|
428
|
+
const nonTriggerIncoming = incoming.filter((e) => {
|
|
422
429
|
const src = context.nodeMap.get(e.sourceNodeId);
|
|
423
430
|
return src && src.category !== "trigger" /* TRIGGER */;
|
|
424
|
-
})
|
|
431
|
+
});
|
|
432
|
+
if (nonTriggerIncoming.length > 1 && typeof console !== "undefined") {
|
|
433
|
+
console.warn(
|
|
434
|
+
`[flow2code] $input is ambiguous: node "${context.currentNodeId}" has ${nonTriggerIncoming.length} non-trigger upstream edges. Using first match "${nonTriggerIncoming[0].sourceNodeId}".`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const dataSource = nonTriggerIncoming[0] || incoming[0];
|
|
425
438
|
if (dataSource) {
|
|
426
439
|
const srcId = dataSource.sourceNodeId;
|
|
427
440
|
if (context.blockScopedNodeIds?.has(srcId)) {
|
|
@@ -436,15 +449,21 @@ function resolveInputRef(path, context) {
|
|
|
436
449
|
`Expression parser error: Node "${context.currentNodeId}" has no input connected`
|
|
437
450
|
);
|
|
438
451
|
}
|
|
452
|
+
var _cachedTriggerIR = null;
|
|
453
|
+
var _cachedTriggerId = null;
|
|
439
454
|
function resolveTriggerRef(path, context) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
455
|
+
if (_cachedTriggerIR !== context.ir) {
|
|
456
|
+
_cachedTriggerIR = context.ir;
|
|
457
|
+
const trigger = context.ir.nodes.find(
|
|
458
|
+
(n) => n.category === "trigger" /* TRIGGER */
|
|
459
|
+
);
|
|
460
|
+
_cachedTriggerId = trigger?.id ?? null;
|
|
461
|
+
}
|
|
462
|
+
if (_cachedTriggerId) {
|
|
463
|
+
if (context.symbolTable?.hasVar(_cachedTriggerId)) {
|
|
464
|
+
return `${context.symbolTable.getVarName(_cachedTriggerId)}${path}`;
|
|
446
465
|
}
|
|
447
|
-
return `flowState['${
|
|
466
|
+
return `flowState['${_cachedTriggerId}']${path}`;
|
|
448
467
|
}
|
|
449
468
|
return "undefined";
|
|
450
469
|
}
|
|
@@ -2790,12 +2809,18 @@ function computeAuditHints(ctx) {
|
|
|
2790
2809
|
return hints;
|
|
2791
2810
|
}
|
|
2792
2811
|
function isFetchCall(node) {
|
|
2793
|
-
|
|
2794
|
-
|
|
2812
|
+
if (node.getKind() === SyntaxKind.CallExpression) {
|
|
2813
|
+
const expr = node.getExpression();
|
|
2814
|
+
if (expr.getKind() === SyntaxKind.Identifier && expr.getText() === "fetch") return true;
|
|
2815
|
+
}
|
|
2816
|
+
if (node.getKind() === SyntaxKind.AwaitExpression) {
|
|
2817
|
+
return isFetchCall(node.getChildAtIndex(1) ?? node);
|
|
2818
|
+
}
|
|
2819
|
+
return node.forEachChild((child) => isFetchCall(child) || void 0) ?? false;
|
|
2795
2820
|
}
|
|
2796
2821
|
function hasAwaitExpression(node) {
|
|
2797
2822
|
if (node.getKind() === SyntaxKind.AwaitExpression) return true;
|
|
2798
|
-
return node.
|
|
2823
|
+
return node.forEachChild((child) => hasAwaitExpression(child) || void 0) ?? false;
|
|
2799
2824
|
}
|
|
2800
2825
|
function extractAwaitedExpression(node) {
|
|
2801
2826
|
const text = node.getText();
|
|
@@ -2865,7 +2890,8 @@ function inferLabel(code, varName) {
|
|
|
2865
2890
|
return truncate(code, 30);
|
|
2866
2891
|
}
|
|
2867
2892
|
function trackVariableUses(nodeId, expression, ctx) {
|
|
2868
|
-
const
|
|
2893
|
+
const stripped = expression.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, "");
|
|
2894
|
+
const identifiers = stripped.match(/\b([a-zA-Z_]\w*)\b/g);
|
|
2869
2895
|
if (!identifiers) return;
|
|
2870
2896
|
const uses = [];
|
|
2871
2897
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3646,6 +3672,13 @@ function handleImportOpenAPI(body) {
|
|
|
3646
3672
|
(flow) => paths.some((p) => flow.meta.name.includes(p))
|
|
3647
3673
|
);
|
|
3648
3674
|
}
|
|
3675
|
+
if (body.filter?.tags && Array.isArray(body.filter.tags)) {
|
|
3676
|
+
const tags = body.filter.tags.map((t) => t.toLowerCase());
|
|
3677
|
+
filteredFlows = filteredFlows.filter((flow) => {
|
|
3678
|
+
const flowTags = flow.meta.tags ?? [];
|
|
3679
|
+
return flowTags.some((t) => tags.includes(t.toLowerCase()));
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3649
3682
|
return {
|
|
3650
3683
|
status: 200,
|
|
3651
3684
|
body: {
|
|
@@ -3964,10 +3997,14 @@ function startServer(options = {}) {
|
|
|
3964
3997
|
const server = createServer((req, res) => {
|
|
3965
3998
|
handleRequest(req, res, staticDir, projectRoot).catch((err) => {
|
|
3966
3999
|
logger.error("Internal error:", err);
|
|
3967
|
-
res.
|
|
3968
|
-
|
|
4000
|
+
if (!res.headersSent) {
|
|
4001
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
4002
|
+
res.end("Internal Server Error");
|
|
4003
|
+
}
|
|
3969
4004
|
});
|
|
3970
4005
|
});
|
|
4006
|
+
server.headersTimeout = 3e4;
|
|
4007
|
+
server.requestTimeout = 6e4;
|
|
3971
4008
|
const hasUI = existsSync2(join2(staticDir, "index.html"));
|
|
3972
4009
|
server.listen(port, host, () => {
|
|
3973
4010
|
const url = `http://localhost:${port}`;
|