@timo9378/flow2code 0.1.7 → 0.1.9
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 +43 -0
- package/dist/cli.js +107 -36
- package/dist/compiler.cjs +85 -33
- package/dist/compiler.js +85 -33
- package/dist/server.js +80 -30
- 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/{2fd98ca28cbab9a6.js → 0bc0a50347ee5f3c.js} +14 -12
- package/out/_next/static/chunks/{fd3c7cf5ea219c74.js → 58bf94a9d7047ec0.js} +1 -1
- 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 → Ma0MmC8j1mxpQbtLwNajF}/_buildManifest.js +0 -0
- /package/out/_next/static/{4eCLtLp-IPL1tppPGnHZE → Ma0MmC8j1mxpQbtLwNajF}/_clientMiddlewareManifest.json +0 -0
- /package/out/_next/static/{4eCLtLp-IPL1tppPGnHZE → Ma0MmC8j1mxpQbtLwNajF}/_ssgManifest.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ 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.9] — 2026-03-05
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`resolveEnvVars` backtick escaping** — Backtick and backslash characters in URLs are now escaped before wrapping in template literals, preventing template literal injection (#24)
|
|
12
|
+
- **`traceLineToNode` binary search** — Replaced linear scan with sorted ranges + binary search for O(log N) source map lookups (#25)
|
|
13
|
+
- **`parseReference` hyphen support** — Regex now matches node IDs containing hyphens (e.g. `node-1`) via `[\w-]+` character class (#27)
|
|
14
|
+
- **Validator duplicate edge ID check** — Added step 4 to detect duplicate `edge.id` values, analogous to the existing duplicate node ID check (#28)
|
|
15
|
+
- **Watch mode SIGINT cleanup** — Added `process.on('SIGINT'/'SIGTERM')` handler to close chokidar watcher and clear pending timers on shutdown (#29)
|
|
16
|
+
- **Split storage colon-in-port-ID** — Edge deserialization now splits on first colon only via `indexOf`, preserving port IDs that contain colons (#30)
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
- **Dead `generateFlowStateDeclaration`** — Removed unused exported function from `type-inference.ts` (zero callers) (#26)
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
- Added duplicate edge ID detection test
|
|
23
|
+
- Added colon-in-port-ID round-trip test
|
|
24
|
+
- Test count: 413 tests / 33 test files
|
|
25
|
+
|
|
26
|
+
## [0.1.8] — 2026-03-05
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
- **HTTP server request timeout** — Added `headersTimeout` (30s) and `requestTimeout` (60s) to prevent Slowloris-style DoS attacks (#16)
|
|
30
|
+
- **Server `headersSent` guard** — Error handler now checks `res.headersSent` before writing 500 response, preventing `ERR_HTTP_HEADERS_SENT` crash (#14)
|
|
31
|
+
- **Validate IR from server** — `handleDecompileTS` and `handleSelectOpenAPIFlow` now call `validateFlowIR()` before `loadIR()`, matching `handleLoadIRFromJSON` behavior (#22)
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **Validator iterative DFS** — Replaced recursive `dfs()` with explicit stack-based iteration to prevent stack overflow on deep graphs (5000+ nodes) (#11)
|
|
35
|
+
- **`isFetchCall` AST-based detection** — Replaced `text.includes("fetch(")` string matching with proper AST walk using `CallExpression` + `Identifier` node types (#12)
|
|
36
|
+
- **`hasAwaitExpression` AST walk** — Replaced `text.startsWith("await ")` fallback with recursive `forEachChild` AST traversal to detect nested awaits (#23)
|
|
37
|
+
- **`trackVariableUses` strip strings** — Expression string now has string literals (`"..."`, `'...'`, `` `...` ``) stripped before identifier regex scan, preventing false-positive variable references (#13)
|
|
38
|
+
- **`_nodeCounter` max ID parse** — After `loadIR`, counter is set to max numeric ID from existing node IDs (not `nodes.length`), preventing ID collisions (#15)
|
|
39
|
+
- **`resolveTriggerRef` cache** — Trigger node lookup cached per IR reference, eliminating O(N) linear scan on every `$trigger` expression evaluation (#17)
|
|
40
|
+
- **`$input` ambiguity warning** — `resolveInputRef` now emits `console.warn` when a node has multiple non-trigger upstream edges, identifying which source is used (#18)
|
|
41
|
+
- **OpenAPI tags filter** — `handleImportOpenAPI` now implements `filter.tags` (case-insensitive match against `meta.tags`), which was previously accepted but silently ignored (#20)
|
|
42
|
+
- **Clipboard fallback** — `handleExportIR` now catches clipboard write errors gracefully instead of letting promise rejection propagate (#21)
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- **`createPlatformRegistry` factory** — New function for creating isolated platform registry instances, enabling safe test parallelism and concurrent compilation (#19)
|
|
46
|
+
|
|
47
|
+
### Tests
|
|
48
|
+
- Added iterative DFS deep graph test (5000 nodes, verifies no stack overflow)
|
|
49
|
+
- Test count: 411 tests / 33 test files
|
|
50
|
+
|
|
8
51
|
## [0.1.7] — 2026-03-05
|
|
9
52
|
|
|
10
53
|
### Performance
|
package/dist/cli.js
CHANGED
|
@@ -162,6 +162,17 @@ function validateFlowIR(ir) {
|
|
|
162
162
|
}
|
|
163
163
|
idSet.add(node.id);
|
|
164
164
|
}
|
|
165
|
+
const edgeIdSet = /* @__PURE__ */ new Set();
|
|
166
|
+
for (const edge of workingIR.edges) {
|
|
167
|
+
if (edgeIdSet.has(edge.id)) {
|
|
168
|
+
errors.push({
|
|
169
|
+
code: "DUPLICATE_EDGE_ID",
|
|
170
|
+
message: `Duplicate edge ID: ${edge.id}`,
|
|
171
|
+
edgeId: edge.id
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
edgeIdSet.add(edge.id);
|
|
175
|
+
}
|
|
165
176
|
for (const edge of workingIR.edges) {
|
|
166
177
|
if (!workingNodeMap.has(edge.sourceNodeId)) {
|
|
167
178
|
errors.push({
|
|
@@ -218,25 +229,32 @@ function detectCycles(nodes, edges) {
|
|
|
218
229
|
for (const node of nodes) {
|
|
219
230
|
color.set(node.id, WHITE);
|
|
220
231
|
}
|
|
221
|
-
|
|
222
|
-
color.
|
|
223
|
-
|
|
232
|
+
for (const node of nodes) {
|
|
233
|
+
if (color.get(node.id) !== WHITE) continue;
|
|
234
|
+
const stack = [[node.id, 0]];
|
|
235
|
+
color.set(node.id, GRAY);
|
|
236
|
+
while (stack.length > 0) {
|
|
237
|
+
const top = stack[stack.length - 1];
|
|
238
|
+
const [currentId, idx] = top;
|
|
239
|
+
const neighbors = adjacency.get(currentId) ?? [];
|
|
240
|
+
if (idx >= neighbors.length) {
|
|
241
|
+
color.set(currentId, BLACK);
|
|
242
|
+
stack.pop();
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
top[1] = idx + 1;
|
|
246
|
+
const neighbor = neighbors[idx];
|
|
224
247
|
if (color.get(neighbor) === GRAY) {
|
|
225
248
|
errors.push({
|
|
226
249
|
code: "CYCLE_DETECTED",
|
|
227
|
-
message: `Cycle detected: node "${
|
|
228
|
-
nodeId
|
|
250
|
+
message: `Cycle detected: node "${currentId}" \u2192 "${neighbor}"`,
|
|
251
|
+
nodeId: currentId
|
|
229
252
|
});
|
|
230
253
|
} else if (color.get(neighbor) === WHITE) {
|
|
231
|
-
|
|
254
|
+
color.set(neighbor, GRAY);
|
|
255
|
+
stack.push([neighbor, 0]);
|
|
232
256
|
}
|
|
233
257
|
}
|
|
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
258
|
}
|
|
241
259
|
return errors;
|
|
242
260
|
}
|
|
@@ -392,7 +410,7 @@ function tokenize(input) {
|
|
|
392
410
|
return tokens;
|
|
393
411
|
}
|
|
394
412
|
function parseReference(ref) {
|
|
395
|
-
const match = ref.match(/^(
|
|
413
|
+
const match = ref.match(/^(\$?[\w-]+)((?:\.[\w]+|\[.+?\])*)$/);
|
|
396
414
|
if (!match) {
|
|
397
415
|
const dotIndex = ref.indexOf(".");
|
|
398
416
|
const bracketIndex = ref.indexOf("[");
|
|
@@ -450,10 +468,16 @@ function resolveInputRef(path, context) {
|
|
|
450
468
|
const incoming = context.ir.edges.filter(
|
|
451
469
|
(e) => e.targetNodeId === context.currentNodeId
|
|
452
470
|
);
|
|
453
|
-
const
|
|
471
|
+
const nonTriggerIncoming = incoming.filter((e) => {
|
|
454
472
|
const src = context.nodeMap.get(e.sourceNodeId);
|
|
455
473
|
return src && src.category !== "trigger" /* TRIGGER */;
|
|
456
|
-
})
|
|
474
|
+
});
|
|
475
|
+
if (nonTriggerIncoming.length > 1 && typeof console !== "undefined") {
|
|
476
|
+
console.warn(
|
|
477
|
+
`[flow2code] $input is ambiguous: node "${context.currentNodeId}" has ${nonTriggerIncoming.length} non-trigger upstream edges. Using first match "${nonTriggerIncoming[0].sourceNodeId}".`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const dataSource = nonTriggerIncoming[0] || incoming[0];
|
|
457
481
|
if (dataSource) {
|
|
458
482
|
const srcId = dataSource.sourceNodeId;
|
|
459
483
|
if (context.blockScopedNodeIds?.has(srcId)) {
|
|
@@ -469,21 +493,27 @@ function resolveInputRef(path, context) {
|
|
|
469
493
|
);
|
|
470
494
|
}
|
|
471
495
|
function resolveTriggerRef(path, context) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
496
|
+
if (_cachedTriggerIR !== context.ir) {
|
|
497
|
+
_cachedTriggerIR = context.ir;
|
|
498
|
+
const trigger = context.ir.nodes.find(
|
|
499
|
+
(n) => n.category === "trigger" /* TRIGGER */
|
|
500
|
+
);
|
|
501
|
+
_cachedTriggerId = trigger?.id ?? null;
|
|
502
|
+
}
|
|
503
|
+
if (_cachedTriggerId) {
|
|
504
|
+
if (context.symbolTable?.hasVar(_cachedTriggerId)) {
|
|
505
|
+
return `${context.symbolTable.getVarName(_cachedTriggerId)}${path}`;
|
|
478
506
|
}
|
|
479
|
-
return `flowState['${
|
|
507
|
+
return `flowState['${_cachedTriggerId}']${path}`;
|
|
480
508
|
}
|
|
481
509
|
return "undefined";
|
|
482
510
|
}
|
|
483
|
-
var ExpressionParseError;
|
|
511
|
+
var _cachedTriggerIR, _cachedTriggerId, ExpressionParseError;
|
|
484
512
|
var init_expression_parser = __esm({
|
|
485
513
|
"src/lib/compiler/expression-parser.ts"() {
|
|
486
514
|
"use strict";
|
|
515
|
+
_cachedTriggerIR = null;
|
|
516
|
+
_cachedTriggerId = null;
|
|
487
517
|
ExpressionParseError = class extends Error {
|
|
488
518
|
constructor(message, expression, position) {
|
|
489
519
|
super(message);
|
|
@@ -2222,13 +2252,15 @@ function sanitizeId(id) {
|
|
|
2222
2252
|
function resolveEnvVars(url, context) {
|
|
2223
2253
|
const hasEnvVar = /\$\{(\w+)\}/.test(url);
|
|
2224
2254
|
if (hasEnvVar) {
|
|
2225
|
-
|
|
2255
|
+
const escaped = url.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
2256
|
+
return "`" + escaped.replace(/\$\{(\w+)\}/g, (_match, varName) => {
|
|
2226
2257
|
context.envVars.add(varName);
|
|
2227
2258
|
return "${process.env." + varName + "}";
|
|
2228
2259
|
}) + "`";
|
|
2229
2260
|
}
|
|
2230
2261
|
if (url.includes("${")) {
|
|
2231
|
-
|
|
2262
|
+
const escaped = url.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
2263
|
+
return "`" + escaped + "`";
|
|
2232
2264
|
}
|
|
2233
2265
|
return `"${url}"`;
|
|
2234
2266
|
}
|
|
@@ -2290,9 +2322,18 @@ function buildSourceMap(code, ir, filePath) {
|
|
|
2290
2322
|
};
|
|
2291
2323
|
}
|
|
2292
2324
|
function traceLineToNode(sourceMap, line) {
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2325
|
+
const entries = Object.entries(sourceMap.mappings).map(([nodeId, range]) => ({ nodeId, ...range })).sort((a, b) => a.startLine - b.startLine);
|
|
2326
|
+
let lo = 0;
|
|
2327
|
+
let hi = entries.length - 1;
|
|
2328
|
+
while (lo <= hi) {
|
|
2329
|
+
const mid = lo + hi >>> 1;
|
|
2330
|
+
const e = entries[mid];
|
|
2331
|
+
if (line < e.startLine) {
|
|
2332
|
+
hi = mid - 1;
|
|
2333
|
+
} else if (line > e.endLine) {
|
|
2334
|
+
lo = mid + 1;
|
|
2335
|
+
} else {
|
|
2336
|
+
return e;
|
|
2296
2337
|
}
|
|
2297
2338
|
}
|
|
2298
2339
|
return null;
|
|
@@ -2987,12 +3028,18 @@ function computeAuditHints(ctx) {
|
|
|
2987
3028
|
return hints;
|
|
2988
3029
|
}
|
|
2989
3030
|
function isFetchCall(node) {
|
|
2990
|
-
|
|
2991
|
-
|
|
3031
|
+
if (node.getKind() === SyntaxKind.CallExpression) {
|
|
3032
|
+
const expr = node.getExpression();
|
|
3033
|
+
if (expr.getKind() === SyntaxKind.Identifier && expr.getText() === "fetch") return true;
|
|
3034
|
+
}
|
|
3035
|
+
if (node.getKind() === SyntaxKind.AwaitExpression) {
|
|
3036
|
+
return isFetchCall(node.getChildAtIndex(1) ?? node);
|
|
3037
|
+
}
|
|
3038
|
+
return node.forEachChild((child) => isFetchCall(child) || void 0) ?? false;
|
|
2992
3039
|
}
|
|
2993
3040
|
function hasAwaitExpression(node) {
|
|
2994
3041
|
if (node.getKind() === SyntaxKind.AwaitExpression) return true;
|
|
2995
|
-
return node.
|
|
3042
|
+
return node.forEachChild((child) => hasAwaitExpression(child) || void 0) ?? false;
|
|
2996
3043
|
}
|
|
2997
3044
|
function extractAwaitedExpression(node) {
|
|
2998
3045
|
const text = node.getText();
|
|
@@ -3062,7 +3109,8 @@ function inferLabel(code, varName) {
|
|
|
3062
3109
|
return truncate(code, 30);
|
|
3063
3110
|
}
|
|
3064
3111
|
function trackVariableUses(nodeId, expression, ctx) {
|
|
3065
|
-
const
|
|
3112
|
+
const stripped = expression.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, "");
|
|
3113
|
+
const identifiers = stripped.match(/\b([a-zA-Z_]\w*)\b/g);
|
|
3066
3114
|
if (!identifiers) return;
|
|
3067
3115
|
const uses = [];
|
|
3068
3116
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3868,6 +3916,13 @@ function handleImportOpenAPI(body) {
|
|
|
3868
3916
|
(flow) => paths.some((p) => flow.meta.name.includes(p))
|
|
3869
3917
|
);
|
|
3870
3918
|
}
|
|
3919
|
+
if (body.filter?.tags && Array.isArray(body.filter.tags)) {
|
|
3920
|
+
const tags = body.filter.tags.map((t) => t.toLowerCase());
|
|
3921
|
+
filteredFlows = filteredFlows.filter((flow) => {
|
|
3922
|
+
const flowTags = flow.meta.tags ?? [];
|
|
3923
|
+
return flowTags.some((t) => tags.includes(t.toLowerCase()));
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3871
3926
|
return {
|
|
3872
3927
|
status: 200,
|
|
3873
3928
|
body: {
|
|
@@ -4128,10 +4183,14 @@ function startServer(options = {}) {
|
|
|
4128
4183
|
const server = createServer((req, res) => {
|
|
4129
4184
|
handleRequest(req, res, staticDir, projectRoot).catch((err) => {
|
|
4130
4185
|
logger.error("Internal error:", err);
|
|
4131
|
-
res.
|
|
4132
|
-
|
|
4186
|
+
if (!res.headersSent) {
|
|
4187
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
4188
|
+
res.end("Internal Server Error");
|
|
4189
|
+
}
|
|
4133
4190
|
});
|
|
4134
4191
|
});
|
|
4192
|
+
server.headersTimeout = 3e4;
|
|
4193
|
+
server.requestTimeout = 6e4;
|
|
4135
4194
|
const hasUI = existsSync3(join3(staticDir, "index.html"));
|
|
4136
4195
|
server.listen(port, host, () => {
|
|
4137
4196
|
const url = `http://localhost:${port}`;
|
|
@@ -4261,8 +4320,12 @@ function mergeIR(files) {
|
|
|
4261
4320
|
}
|
|
4262
4321
|
const edgesRaw = parse(files.edges);
|
|
4263
4322
|
const edges = (edgesRaw ?? []).map((e) => {
|
|
4264
|
-
const
|
|
4265
|
-
const
|
|
4323
|
+
const srcIdx = e.source.indexOf(":");
|
|
4324
|
+
const tgtIdx = e.target.indexOf(":");
|
|
4325
|
+
const sourceNodeId = srcIdx > 0 ? e.source.slice(0, srcIdx) : e.source;
|
|
4326
|
+
const sourcePortId = srcIdx > 0 ? e.source.slice(srcIdx + 1) : "output";
|
|
4327
|
+
const targetNodeId = tgtIdx > 0 ? e.target.slice(0, tgtIdx) : e.target;
|
|
4328
|
+
const targetPortId = tgtIdx > 0 ? e.target.slice(tgtIdx + 1) : "input";
|
|
4266
4329
|
return {
|
|
4267
4330
|
id: e.id,
|
|
4268
4331
|
sourceNodeId,
|
|
@@ -5056,6 +5119,14 @@ program.command("watch [dir]").description("Watch directory, auto-compile .flow.
|
|
|
5056
5119
|
watcher.on("error", (error) => {
|
|
5057
5120
|
console.error("\u274C Watch error:", error instanceof Error ? error.message : String(error));
|
|
5058
5121
|
});
|
|
5122
|
+
const cleanup = () => {
|
|
5123
|
+
console.log("\n\u{1F6D1} Stopping watcher...");
|
|
5124
|
+
for (const timer of pendingTimers.values()) clearTimeout(timer);
|
|
5125
|
+
pendingTimers.clear();
|
|
5126
|
+
watcher.close().then(() => process.exit(0));
|
|
5127
|
+
};
|
|
5128
|
+
process.on("SIGINT", cleanup);
|
|
5129
|
+
process.on("SIGTERM", cleanup);
|
|
5059
5130
|
});
|
|
5060
5131
|
program.command("init").description("Initialize Flow2Code in current project (Zero Pollution mode)").action(() => {
|
|
5061
5132
|
const flow2codeDir = resolve3(".flow2code");
|
package/dist/compiler.cjs
CHANGED
|
@@ -238,6 +238,17 @@ function validateFlowIR(ir) {
|
|
|
238
238
|
}
|
|
239
239
|
idSet.add(node.id);
|
|
240
240
|
}
|
|
241
|
+
const edgeIdSet = /* @__PURE__ */ new Set();
|
|
242
|
+
for (const edge of workingIR.edges) {
|
|
243
|
+
if (edgeIdSet.has(edge.id)) {
|
|
244
|
+
errors.push({
|
|
245
|
+
code: "DUPLICATE_EDGE_ID",
|
|
246
|
+
message: `Duplicate edge ID: ${edge.id}`,
|
|
247
|
+
edgeId: edge.id
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
edgeIdSet.add(edge.id);
|
|
251
|
+
}
|
|
241
252
|
for (const edge of workingIR.edges) {
|
|
242
253
|
if (!workingNodeMap.has(edge.sourceNodeId)) {
|
|
243
254
|
errors.push({
|
|
@@ -294,25 +305,32 @@ function detectCycles(nodes, edges) {
|
|
|
294
305
|
for (const node of nodes) {
|
|
295
306
|
color.set(node.id, WHITE);
|
|
296
307
|
}
|
|
297
|
-
|
|
298
|
-
color.
|
|
299
|
-
|
|
308
|
+
for (const node of nodes) {
|
|
309
|
+
if (color.get(node.id) !== WHITE) continue;
|
|
310
|
+
const stack = [[node.id, 0]];
|
|
311
|
+
color.set(node.id, GRAY);
|
|
312
|
+
while (stack.length > 0) {
|
|
313
|
+
const top = stack[stack.length - 1];
|
|
314
|
+
const [currentId, idx] = top;
|
|
315
|
+
const neighbors = adjacency.get(currentId) ?? [];
|
|
316
|
+
if (idx >= neighbors.length) {
|
|
317
|
+
color.set(currentId, BLACK);
|
|
318
|
+
stack.pop();
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
top[1] = idx + 1;
|
|
322
|
+
const neighbor = neighbors[idx];
|
|
300
323
|
if (color.get(neighbor) === GRAY) {
|
|
301
324
|
errors.push({
|
|
302
325
|
code: "CYCLE_DETECTED",
|
|
303
|
-
message: `Cycle detected: node "${
|
|
304
|
-
nodeId
|
|
326
|
+
message: `Cycle detected: node "${currentId}" \u2192 "${neighbor}"`,
|
|
327
|
+
nodeId: currentId
|
|
305
328
|
});
|
|
306
329
|
} else if (color.get(neighbor) === WHITE) {
|
|
307
|
-
|
|
330
|
+
color.set(neighbor, GRAY);
|
|
331
|
+
stack.push([neighbor, 0]);
|
|
308
332
|
}
|
|
309
333
|
}
|
|
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
334
|
}
|
|
317
335
|
return errors;
|
|
318
336
|
}
|
|
@@ -456,7 +474,7 @@ function tokenize(input) {
|
|
|
456
474
|
return tokens;
|
|
457
475
|
}
|
|
458
476
|
function parseReference(ref) {
|
|
459
|
-
const match = ref.match(/^(
|
|
477
|
+
const match = ref.match(/^(\$?[\w-]+)((?:\.[\w]+|\[.+?\])*)$/);
|
|
460
478
|
if (!match) {
|
|
461
479
|
const dotIndex = ref.indexOf(".");
|
|
462
480
|
const bracketIndex = ref.indexOf("[");
|
|
@@ -514,10 +532,16 @@ function resolveInputRef(path, context) {
|
|
|
514
532
|
const incoming = context.ir.edges.filter(
|
|
515
533
|
(e) => e.targetNodeId === context.currentNodeId
|
|
516
534
|
);
|
|
517
|
-
const
|
|
535
|
+
const nonTriggerIncoming = incoming.filter((e) => {
|
|
518
536
|
const src = context.nodeMap.get(e.sourceNodeId);
|
|
519
537
|
return src && src.category !== "trigger" /* TRIGGER */;
|
|
520
|
-
})
|
|
538
|
+
});
|
|
539
|
+
if (nonTriggerIncoming.length > 1 && typeof console !== "undefined") {
|
|
540
|
+
console.warn(
|
|
541
|
+
`[flow2code] $input is ambiguous: node "${context.currentNodeId}" has ${nonTriggerIncoming.length} non-trigger upstream edges. Using first match "${nonTriggerIncoming[0].sourceNodeId}".`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
const dataSource = nonTriggerIncoming[0] || incoming[0];
|
|
521
545
|
if (dataSource) {
|
|
522
546
|
const srcId = dataSource.sourceNodeId;
|
|
523
547
|
if (context.blockScopedNodeIds?.has(srcId)) {
|
|
@@ -532,15 +556,21 @@ function resolveInputRef(path, context) {
|
|
|
532
556
|
`Expression parser error: Node "${context.currentNodeId}" has no input connected`
|
|
533
557
|
);
|
|
534
558
|
}
|
|
559
|
+
var _cachedTriggerIR = null;
|
|
560
|
+
var _cachedTriggerId = null;
|
|
535
561
|
function resolveTriggerRef(path, context) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
562
|
+
if (_cachedTriggerIR !== context.ir) {
|
|
563
|
+
_cachedTriggerIR = context.ir;
|
|
564
|
+
const trigger = context.ir.nodes.find(
|
|
565
|
+
(n) => n.category === "trigger" /* TRIGGER */
|
|
566
|
+
);
|
|
567
|
+
_cachedTriggerId = trigger?.id ?? null;
|
|
568
|
+
}
|
|
569
|
+
if (_cachedTriggerId) {
|
|
570
|
+
if (context.symbolTable?.hasVar(_cachedTriggerId)) {
|
|
571
|
+
return `${context.symbolTable.getVarName(_cachedTriggerId)}${path}`;
|
|
542
572
|
}
|
|
543
|
-
return `flowState['${
|
|
573
|
+
return `flowState['${_cachedTriggerId}']${path}`;
|
|
544
574
|
}
|
|
545
575
|
return "undefined";
|
|
546
576
|
}
|
|
@@ -2236,13 +2266,15 @@ function sanitizeId(id) {
|
|
|
2236
2266
|
function resolveEnvVars(url, context) {
|
|
2237
2267
|
const hasEnvVar = /\$\{(\w+)\}/.test(url);
|
|
2238
2268
|
if (hasEnvVar) {
|
|
2239
|
-
|
|
2269
|
+
const escaped = url.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
2270
|
+
return "`" + escaped.replace(/\$\{(\w+)\}/g, (_match, varName) => {
|
|
2240
2271
|
context.envVars.add(varName);
|
|
2241
2272
|
return "${process.env." + varName + "}";
|
|
2242
2273
|
}) + "`";
|
|
2243
2274
|
}
|
|
2244
2275
|
if (url.includes("${")) {
|
|
2245
|
-
|
|
2276
|
+
const escaped = url.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
2277
|
+
return "`" + escaped + "`";
|
|
2246
2278
|
}
|
|
2247
2279
|
return `"${url}"`;
|
|
2248
2280
|
}
|
|
@@ -2304,9 +2336,18 @@ function buildSourceMap(code, ir, filePath) {
|
|
|
2304
2336
|
};
|
|
2305
2337
|
}
|
|
2306
2338
|
function traceLineToNode(sourceMap, line) {
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2339
|
+
const entries = Object.entries(sourceMap.mappings).map(([nodeId, range]) => ({ nodeId, ...range })).sort((a, b) => a.startLine - b.startLine);
|
|
2340
|
+
let lo = 0;
|
|
2341
|
+
let hi = entries.length - 1;
|
|
2342
|
+
while (lo <= hi) {
|
|
2343
|
+
const mid = lo + hi >>> 1;
|
|
2344
|
+
const e = entries[mid];
|
|
2345
|
+
if (line < e.startLine) {
|
|
2346
|
+
hi = mid - 1;
|
|
2347
|
+
} else if (line > e.endLine) {
|
|
2348
|
+
lo = mid + 1;
|
|
2349
|
+
} else {
|
|
2350
|
+
return e;
|
|
2310
2351
|
}
|
|
2311
2352
|
}
|
|
2312
2353
|
return null;
|
|
@@ -3041,12 +3082,18 @@ function computeAuditHints(ctx) {
|
|
|
3041
3082
|
return hints;
|
|
3042
3083
|
}
|
|
3043
3084
|
function isFetchCall(node) {
|
|
3044
|
-
|
|
3045
|
-
|
|
3085
|
+
if (node.getKind() === import_ts_morph2.SyntaxKind.CallExpression) {
|
|
3086
|
+
const expr = node.getExpression();
|
|
3087
|
+
if (expr.getKind() === import_ts_morph2.SyntaxKind.Identifier && expr.getText() === "fetch") return true;
|
|
3088
|
+
}
|
|
3089
|
+
if (node.getKind() === import_ts_morph2.SyntaxKind.AwaitExpression) {
|
|
3090
|
+
return isFetchCall(node.getChildAtIndex(1) ?? node);
|
|
3091
|
+
}
|
|
3092
|
+
return node.forEachChild((child) => isFetchCall(child) || void 0) ?? false;
|
|
3046
3093
|
}
|
|
3047
3094
|
function hasAwaitExpression(node) {
|
|
3048
3095
|
if (node.getKind() === import_ts_morph2.SyntaxKind.AwaitExpression) return true;
|
|
3049
|
-
return node.
|
|
3096
|
+
return node.forEachChild((child) => hasAwaitExpression(child) || void 0) ?? false;
|
|
3050
3097
|
}
|
|
3051
3098
|
function extractAwaitedExpression(node) {
|
|
3052
3099
|
const text = node.getText();
|
|
@@ -3116,7 +3163,8 @@ function inferLabel(code, varName) {
|
|
|
3116
3163
|
return truncate2(code, 30);
|
|
3117
3164
|
}
|
|
3118
3165
|
function trackVariableUses(nodeId, expression, ctx) {
|
|
3119
|
-
const
|
|
3166
|
+
const stripped = expression.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, "");
|
|
3167
|
+
const identifiers = stripped.match(/\b([a-zA-Z_]\w*)\b/g);
|
|
3120
3168
|
if (!identifiers) return;
|
|
3121
3169
|
const uses = [];
|
|
3122
3170
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3737,8 +3785,12 @@ function mergeIR(files) {
|
|
|
3737
3785
|
}
|
|
3738
3786
|
const edgesRaw = (0, import_yaml.parse)(files.edges);
|
|
3739
3787
|
const edges = (edgesRaw ?? []).map((e) => {
|
|
3740
|
-
const
|
|
3741
|
-
const
|
|
3788
|
+
const srcIdx = e.source.indexOf(":");
|
|
3789
|
+
const tgtIdx = e.target.indexOf(":");
|
|
3790
|
+
const sourceNodeId = srcIdx > 0 ? e.source.slice(0, srcIdx) : e.source;
|
|
3791
|
+
const sourcePortId = srcIdx > 0 ? e.source.slice(srcIdx + 1) : "output";
|
|
3792
|
+
const targetNodeId = tgtIdx > 0 ? e.target.slice(0, tgtIdx) : e.target;
|
|
3793
|
+
const targetPortId = tgtIdx > 0 ? e.target.slice(tgtIdx + 1) : "input";
|
|
3742
3794
|
return {
|
|
3743
3795
|
id: e.id,
|
|
3744
3796
|
sourceNodeId,
|