@timo9378/flow2code 0.1.6 → 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 +41 -0
- package/dist/cli.js +119 -46
- package/dist/compiler.cjs +68 -37
- package/dist/compiler.js +68 -37
- package/dist/server.js +80 -39
- 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/{5f1a9fec0e69c483.js → 06054f68c210e89c.js} +1 -1
- package/out/_next/static/chunks/{bfa275b7488d8b70.js → 0bc0a50347ee5f3c.js} +14 -12
- package/out/_next/static/chunks/4ce13068a7e61854.js +25 -0
- 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/{wFX9tOZWtEdER7XrtbgHQ → EFK7prtbW4K3cbFdFFkDA}/_buildManifest.js +0 -0
- /package/out/_next/static/{wFX9tOZWtEdER7XrtbgHQ → EFK7prtbW4K3cbFdFFkDA}/_clientMiddlewareManifest.json +0 -0
- /package/out/_next/static/{wFX9tOZWtEdER7XrtbgHQ → EFK7prtbW4K3cbFdFFkDA}/_ssgManifest.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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
|
+
|
|
33
|
+
## [0.1.7] — 2026-03-05
|
|
34
|
+
|
|
35
|
+
### Performance
|
|
36
|
+
- **Precompute `edgeSuccessors` map** — Eliminated O(N×E) per-call rebuild in `generateBlockContinuation`; successor lookup is now O(1) via pre-built map in `CompilerContext`
|
|
37
|
+
- **Reuse `nodeMap` in control-flow analysis** — `computeControlFlowDescendants` now receives the existing `nodeMap` instead of rebuilding a redundant `new Map()` each call
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **Decompiler `processForStatement` AST correctness** — Replaced fragile `.split("{")` string hack with proper AST methods (`getInitializer()`, `getCondition()`, `getIncrementor()`); fixes incorrect parsing when loop body contains object literals
|
|
41
|
+
- **OpenAPI YAML import** — `handleImportOpenAPI` now supports `.yaml`/`.yml` files via dynamic `import("yaml")` instead of silently failing with `JSON.parse`
|
|
42
|
+
- **`revokeObjectURL` download race** — Deferred `URL.revokeObjectURL` by 10 seconds after `click()` to prevent Safari/slow-browser download failures
|
|
43
|
+
- **CLI watch mode async I/O** — New `loadFlowProjectAsync` reads flow projects with `fs/promises` (`readFile`/`readdir`); `compileFlowDirAsync` no longer blocks the event loop with sync file I/O
|
|
44
|
+
|
|
45
|
+
### Tests
|
|
46
|
+
- Added `loadFlowProjectAsync` parity tests (split + JSON + error case)
|
|
47
|
+
- Test count: 410 tests / 33 test files
|
|
48
|
+
|
|
8
49
|
## [0.1.6] — 2026-03-05
|
|
9
50
|
|
|
10
51
|
### Fixed (Critical)
|
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);
|
|
@@ -1852,10 +1871,18 @@ function compile(ir, options) {
|
|
|
1852
1871
|
dagMode: false,
|
|
1853
1872
|
symbolTableExclusions: /* @__PURE__ */ new Set(),
|
|
1854
1873
|
generatedBlockNodeIds: /* @__PURE__ */ new Set(),
|
|
1855
|
-
pluginRegistry
|
|
1874
|
+
pluginRegistry,
|
|
1875
|
+
edgeSuccessors: (() => {
|
|
1876
|
+
const map = /* @__PURE__ */ new Map();
|
|
1877
|
+
for (const edge of workingIR.edges) {
|
|
1878
|
+
if (!map.has(edge.sourceNodeId)) map.set(edge.sourceNodeId, []);
|
|
1879
|
+
map.get(edge.sourceNodeId).push(edge.targetNodeId);
|
|
1880
|
+
}
|
|
1881
|
+
return map;
|
|
1882
|
+
})()
|
|
1856
1883
|
};
|
|
1857
1884
|
const trigger = workingIR.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
|
|
1858
|
-
const preComputedBlockNodes = computeControlFlowDescendants(workingIR, trigger.id);
|
|
1885
|
+
const preComputedBlockNodes = computeControlFlowDescendants(workingIR, trigger.id, nodeMap);
|
|
1859
1886
|
for (const nodeId of preComputedBlockNodes) {
|
|
1860
1887
|
context.childBlockNodeIds.add(nodeId);
|
|
1861
1888
|
context.symbolTableExclusions.add(nodeId);
|
|
@@ -1935,8 +1962,7 @@ function isControlFlowEdge(edge, nodeMap) {
|
|
|
1935
1962
|
const controlPorts = CONTROL_FLOW_PORT_MAP[sourceNode.nodeType];
|
|
1936
1963
|
return controlPorts !== void 0 && controlPorts.has(edge.sourcePortId);
|
|
1937
1964
|
}
|
|
1938
|
-
function computeControlFlowDescendants(ir, triggerId) {
|
|
1939
|
-
const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
1965
|
+
function computeControlFlowDescendants(ir, triggerId, nodeMap) {
|
|
1940
1966
|
const strippedSuccessors = /* @__PURE__ */ new Map();
|
|
1941
1967
|
for (const node of ir.nodes) {
|
|
1942
1968
|
strippedSuccessors.set(node.id, /* @__PURE__ */ new Set());
|
|
@@ -1969,13 +1995,7 @@ function computeControlFlowDescendants(ir, triggerId) {
|
|
|
1969
1995
|
function generateBlockContinuation(writer, fromNodeId, context) {
|
|
1970
1996
|
const reachable = /* @__PURE__ */ new Set();
|
|
1971
1997
|
const bfsQueue = [fromNodeId];
|
|
1972
|
-
const edgeSuccessors =
|
|
1973
|
-
for (const edge of context.ir.edges) {
|
|
1974
|
-
if (!edgeSuccessors.has(edge.sourceNodeId)) {
|
|
1975
|
-
edgeSuccessors.set(edge.sourceNodeId, []);
|
|
1976
|
-
}
|
|
1977
|
-
edgeSuccessors.get(edge.sourceNodeId).push(edge.targetNodeId);
|
|
1978
|
-
}
|
|
1998
|
+
const edgeSuccessors = context.edgeSuccessors;
|
|
1979
1999
|
while (bfsQueue.length > 0) {
|
|
1980
2000
|
const id = bfsQueue.shift();
|
|
1981
2001
|
if (reachable.has(id)) continue;
|
|
@@ -2764,7 +2784,10 @@ function processForInStatement(stmt, _prevNodeId, ctx, line) {
|
|
|
2764
2784
|
}
|
|
2765
2785
|
function processForStatement(stmt, _prevNodeId, ctx, line) {
|
|
2766
2786
|
const nodeId = ctx.nextId("loop");
|
|
2767
|
-
const
|
|
2787
|
+
const initText = stmt.getInitializer()?.getText() ?? "";
|
|
2788
|
+
const condText = stmt.getCondition()?.getText() ?? "";
|
|
2789
|
+
const incrText = stmt.getIncrementor()?.getText() ?? "";
|
|
2790
|
+
const fullText = `for (${initText}; ${condText}; ${incrText})`;
|
|
2768
2791
|
ctx.addNode({
|
|
2769
2792
|
id: nodeId,
|
|
2770
2793
|
nodeType: "for_loop" /* FOR_LOOP */,
|
|
@@ -2983,12 +3006,18 @@ function computeAuditHints(ctx) {
|
|
|
2983
3006
|
return hints;
|
|
2984
3007
|
}
|
|
2985
3008
|
function isFetchCall(node) {
|
|
2986
|
-
|
|
2987
|
-
|
|
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;
|
|
2988
3017
|
}
|
|
2989
3018
|
function hasAwaitExpression(node) {
|
|
2990
3019
|
if (node.getKind() === SyntaxKind.AwaitExpression) return true;
|
|
2991
|
-
return node.
|
|
3020
|
+
return node.forEachChild((child) => hasAwaitExpression(child) || void 0) ?? false;
|
|
2992
3021
|
}
|
|
2993
3022
|
function extractAwaitedExpression(node) {
|
|
2994
3023
|
const text = node.getText();
|
|
@@ -3058,7 +3087,8 @@ function inferLabel(code, varName) {
|
|
|
3058
3087
|
return truncate(code, 30);
|
|
3059
3088
|
}
|
|
3060
3089
|
function trackVariableUses(nodeId, expression, ctx) {
|
|
3061
|
-
const
|
|
3090
|
+
const stripped = expression.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, "");
|
|
3091
|
+
const identifiers = stripped.match(/\b([a-zA-Z_]\w*)\b/g);
|
|
3062
3092
|
if (!identifiers) return;
|
|
3063
3093
|
const uses = [];
|
|
3064
3094
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3864,6 +3894,13 @@ function handleImportOpenAPI(body) {
|
|
|
3864
3894
|
(flow) => paths.some((p) => flow.meta.name.includes(p))
|
|
3865
3895
|
);
|
|
3866
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
|
+
}
|
|
3867
3904
|
return {
|
|
3868
3905
|
status: 200,
|
|
3869
3906
|
body: {
|
|
@@ -3946,7 +3983,7 @@ __export(server_exports, {
|
|
|
3946
3983
|
startServer: () => startServer
|
|
3947
3984
|
});
|
|
3948
3985
|
import { createServer } from "http";
|
|
3949
|
-
import { readFile, stat } from "fs/promises";
|
|
3986
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
3950
3987
|
import { join as join3, extname as extname2, dirname as dirname3, resolve as resolve2 } from "path";
|
|
3951
3988
|
import { fileURLToPath } from "url";
|
|
3952
3989
|
import { existsSync as existsSync3 } from "fs";
|
|
@@ -4044,7 +4081,7 @@ async function serveStatic(staticDir, pathname, res) {
|
|
|
4044
4081
|
if (!s.isFile()) return false;
|
|
4045
4082
|
const ext = extname2(filePath).toLowerCase();
|
|
4046
4083
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
4047
|
-
const content = await
|
|
4084
|
+
const content = await readFile2(filePath);
|
|
4048
4085
|
res.writeHead(200, { "Content-Type": contentType });
|
|
4049
4086
|
res.end(content);
|
|
4050
4087
|
return true;
|
|
@@ -4108,7 +4145,7 @@ async function handleRequest(req, res, staticDir, projectRoot) {
|
|
|
4108
4145
|
if (served) return;
|
|
4109
4146
|
const indexPath = join3(staticDir, "index.html");
|
|
4110
4147
|
try {
|
|
4111
|
-
const content = await
|
|
4148
|
+
const content = await readFile2(indexPath, "utf-8");
|
|
4112
4149
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
4113
4150
|
res.end(content);
|
|
4114
4151
|
} catch {
|
|
@@ -4124,10 +4161,14 @@ function startServer(options = {}) {
|
|
|
4124
4161
|
const server = createServer((req, res) => {
|
|
4125
4162
|
handleRequest(req, res, staticDir, projectRoot).catch((err) => {
|
|
4126
4163
|
logger.error("Internal error:", err);
|
|
4127
|
-
res.
|
|
4128
|
-
|
|
4164
|
+
if (!res.headersSent) {
|
|
4165
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
4166
|
+
res.end("Internal Server Error");
|
|
4167
|
+
}
|
|
4129
4168
|
});
|
|
4130
4169
|
});
|
|
4170
|
+
server.headersTimeout = 3e4;
|
|
4171
|
+
server.requestTimeout = 6e4;
|
|
4131
4172
|
const hasUI = existsSync3(join3(staticDir, "index.html"));
|
|
4132
4173
|
server.listen(port, host, () => {
|
|
4133
4174
|
const url = `http://localhost:${port}`;
|
|
@@ -4190,7 +4231,7 @@ init_logger();
|
|
|
4190
4231
|
init_validator();
|
|
4191
4232
|
import { Command } from "commander";
|
|
4192
4233
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4, readdirSync as readdirSync2, rmSync as rmSync2 } from "fs";
|
|
4193
|
-
import { readFile as
|
|
4234
|
+
import { readFile as readFile3, writeFile, mkdir } from "fs/promises";
|
|
4194
4235
|
import { join as join4, dirname as dirname4, resolve as resolve3, basename as basename2 } from "path";
|
|
4195
4236
|
import { watch } from "chokidar";
|
|
4196
4237
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -4329,6 +4370,7 @@ function sanitizeFilename(id) {
|
|
|
4329
4370
|
|
|
4330
4371
|
// src/lib/storage/flow-project.ts
|
|
4331
4372
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, statSync } from "fs";
|
|
4373
|
+
import { readFile, readdir } from "fs/promises";
|
|
4332
4374
|
import { join, dirname } from "path";
|
|
4333
4375
|
function detectFormat(inputPath) {
|
|
4334
4376
|
if (inputPath.endsWith(".flow.json") && existsSync(inputPath)) {
|
|
@@ -4379,6 +4421,37 @@ function loadFlowProject(inputPath) {
|
|
|
4379
4421
|
const ir = mergeIR({ meta, edges, nodes });
|
|
4380
4422
|
return { path: resolvedPath, format, ir };
|
|
4381
4423
|
}
|
|
4424
|
+
async function loadFlowProjectAsync(inputPath) {
|
|
4425
|
+
const { resolvedPath, format } = detectFormat(inputPath);
|
|
4426
|
+
if (format === "json") {
|
|
4427
|
+
if (!existsSync(resolvedPath)) {
|
|
4428
|
+
throw new Error(`Flow file not found: ${resolvedPath}`);
|
|
4429
|
+
}
|
|
4430
|
+
const raw = await readFile(resolvedPath, "utf-8");
|
|
4431
|
+
const ir2 = JSON.parse(raw);
|
|
4432
|
+
return { path: resolvedPath, format, ir: ir2 };
|
|
4433
|
+
}
|
|
4434
|
+
if (!existsSync(resolvedPath)) {
|
|
4435
|
+
throw new Error(`Flow directory not found: ${resolvedPath}`);
|
|
4436
|
+
}
|
|
4437
|
+
const metaPath = join(resolvedPath, "meta.yaml");
|
|
4438
|
+
if (!existsSync(metaPath)) {
|
|
4439
|
+
throw new Error(`meta.yaml not found in ${resolvedPath} \u2014 not a valid Flow directory`);
|
|
4440
|
+
}
|
|
4441
|
+
const meta = await readFile(metaPath, "utf-8");
|
|
4442
|
+
const edgesPath = join(resolvedPath, "edges.yaml");
|
|
4443
|
+
const edges = existsSync(edgesPath) ? await readFile(edgesPath, "utf-8") : "";
|
|
4444
|
+
const nodesDir = join(resolvedPath, "nodes");
|
|
4445
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
4446
|
+
if (existsSync(nodesDir)) {
|
|
4447
|
+
const nodeFiles = (await readdir(nodesDir)).filter((f) => f.endsWith(".yaml"));
|
|
4448
|
+
for (const file of nodeFiles) {
|
|
4449
|
+
nodes.set(file, await readFile(join(nodesDir, file), "utf-8"));
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
const ir = mergeIR({ meta, edges, nodes });
|
|
4453
|
+
return { path: resolvedPath, format, ir };
|
|
4454
|
+
}
|
|
4382
4455
|
function saveFlowProject(ir, outputPath, options = {}) {
|
|
4383
4456
|
const { format = "split", cleanOrphanNodes = true } = options;
|
|
4384
4457
|
if (format === "json") {
|
|
@@ -5310,7 +5383,7 @@ function generateEnvExample() {
|
|
|
5310
5383
|
async function compileFileAsync(filePath, projectRoot) {
|
|
5311
5384
|
const startTime = Date.now();
|
|
5312
5385
|
try {
|
|
5313
|
-
const raw = await
|
|
5386
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
5314
5387
|
const ir = JSON.parse(raw);
|
|
5315
5388
|
const validation = validateFlowIR(ir);
|
|
5316
5389
|
if (!validation.valid) {
|
|
@@ -5345,7 +5418,7 @@ async function compileFileAsync(filePath, projectRoot) {
|
|
|
5345
5418
|
async function compileFlowDirAsync(dirPath, projectRoot) {
|
|
5346
5419
|
const startTime = Date.now();
|
|
5347
5420
|
try {
|
|
5348
|
-
const project =
|
|
5421
|
+
const project = await loadFlowProjectAsync(dirPath);
|
|
5349
5422
|
const ir = project.ir;
|
|
5350
5423
|
const validation = validateFlowIR(ir);
|
|
5351
5424
|
if (!validation.valid) {
|
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
|
}
|
|
@@ -1861,10 +1880,18 @@ function compile(ir, options) {
|
|
|
1861
1880
|
dagMode: false,
|
|
1862
1881
|
symbolTableExclusions: /* @__PURE__ */ new Set(),
|
|
1863
1882
|
generatedBlockNodeIds: /* @__PURE__ */ new Set(),
|
|
1864
|
-
pluginRegistry
|
|
1883
|
+
pluginRegistry,
|
|
1884
|
+
edgeSuccessors: (() => {
|
|
1885
|
+
const map = /* @__PURE__ */ new Map();
|
|
1886
|
+
for (const edge of workingIR.edges) {
|
|
1887
|
+
if (!map.has(edge.sourceNodeId)) map.set(edge.sourceNodeId, []);
|
|
1888
|
+
map.get(edge.sourceNodeId).push(edge.targetNodeId);
|
|
1889
|
+
}
|
|
1890
|
+
return map;
|
|
1891
|
+
})()
|
|
1865
1892
|
};
|
|
1866
1893
|
const trigger = workingIR.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
|
|
1867
|
-
const preComputedBlockNodes = computeControlFlowDescendants(workingIR, trigger.id);
|
|
1894
|
+
const preComputedBlockNodes = computeControlFlowDescendants(workingIR, trigger.id, nodeMap);
|
|
1868
1895
|
for (const nodeId of preComputedBlockNodes) {
|
|
1869
1896
|
context.childBlockNodeIds.add(nodeId);
|
|
1870
1897
|
context.symbolTableExclusions.add(nodeId);
|
|
@@ -1949,8 +1976,7 @@ function isControlFlowEdge(edge, nodeMap) {
|
|
|
1949
1976
|
const controlPorts = CONTROL_FLOW_PORT_MAP[sourceNode.nodeType];
|
|
1950
1977
|
return controlPorts !== void 0 && controlPorts.has(edge.sourcePortId);
|
|
1951
1978
|
}
|
|
1952
|
-
function computeControlFlowDescendants(ir, triggerId) {
|
|
1953
|
-
const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
1979
|
+
function computeControlFlowDescendants(ir, triggerId, nodeMap) {
|
|
1954
1980
|
const strippedSuccessors = /* @__PURE__ */ new Map();
|
|
1955
1981
|
for (const node of ir.nodes) {
|
|
1956
1982
|
strippedSuccessors.set(node.id, /* @__PURE__ */ new Set());
|
|
@@ -1983,13 +2009,7 @@ function computeControlFlowDescendants(ir, triggerId) {
|
|
|
1983
2009
|
function generateBlockContinuation(writer, fromNodeId, context) {
|
|
1984
2010
|
const reachable = /* @__PURE__ */ new Set();
|
|
1985
2011
|
const bfsQueue = [fromNodeId];
|
|
1986
|
-
const edgeSuccessors =
|
|
1987
|
-
for (const edge of context.ir.edges) {
|
|
1988
|
-
if (!edgeSuccessors.has(edge.sourceNodeId)) {
|
|
1989
|
-
edgeSuccessors.set(edge.sourceNodeId, []);
|
|
1990
|
-
}
|
|
1991
|
-
edgeSuccessors.get(edge.sourceNodeId).push(edge.targetNodeId);
|
|
1992
|
-
}
|
|
2012
|
+
const edgeSuccessors = context.edgeSuccessors;
|
|
1993
2013
|
while (bfsQueue.length > 0) {
|
|
1994
2014
|
const id = bfsQueue.shift();
|
|
1995
2015
|
if (reachable.has(id)) continue;
|
|
@@ -2818,7 +2838,10 @@ function processForInStatement(stmt, _prevNodeId, ctx, line) {
|
|
|
2818
2838
|
}
|
|
2819
2839
|
function processForStatement(stmt, _prevNodeId, ctx, line) {
|
|
2820
2840
|
const nodeId = ctx.nextId("loop");
|
|
2821
|
-
const
|
|
2841
|
+
const initText = stmt.getInitializer()?.getText() ?? "";
|
|
2842
|
+
const condText = stmt.getCondition()?.getText() ?? "";
|
|
2843
|
+
const incrText = stmt.getIncrementor()?.getText() ?? "";
|
|
2844
|
+
const fullText = `for (${initText}; ${condText}; ${incrText})`;
|
|
2822
2845
|
ctx.addNode({
|
|
2823
2846
|
id: nodeId,
|
|
2824
2847
|
nodeType: "for_loop" /* FOR_LOOP */,
|
|
@@ -3037,12 +3060,18 @@ function computeAuditHints(ctx) {
|
|
|
3037
3060
|
return hints;
|
|
3038
3061
|
}
|
|
3039
3062
|
function isFetchCall(node) {
|
|
3040
|
-
|
|
3041
|
-
|
|
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;
|
|
3042
3071
|
}
|
|
3043
3072
|
function hasAwaitExpression(node) {
|
|
3044
3073
|
if (node.getKind() === import_ts_morph2.SyntaxKind.AwaitExpression) return true;
|
|
3045
|
-
return node.
|
|
3074
|
+
return node.forEachChild((child) => hasAwaitExpression(child) || void 0) ?? false;
|
|
3046
3075
|
}
|
|
3047
3076
|
function extractAwaitedExpression(node) {
|
|
3048
3077
|
const text = node.getText();
|
|
@@ -3112,7 +3141,8 @@ function inferLabel(code, varName) {
|
|
|
3112
3141
|
return truncate2(code, 30);
|
|
3113
3142
|
}
|
|
3114
3143
|
function trackVariableUses(nodeId, expression, ctx) {
|
|
3115
|
-
const
|
|
3144
|
+
const stripped = expression.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, "");
|
|
3145
|
+
const identifiers = stripped.match(/\b([a-zA-Z_]\w*)\b/g);
|
|
3116
3146
|
if (!identifiers) return;
|
|
3117
3147
|
const uses = [];
|
|
3118
3148
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3767,6 +3797,7 @@ function sanitizeFilename(id) {
|
|
|
3767
3797
|
|
|
3768
3798
|
// src/lib/storage/flow-project.ts
|
|
3769
3799
|
var import_node_fs = require("fs");
|
|
3800
|
+
var import_promises = require("fs/promises");
|
|
3770
3801
|
var import_node_path = require("path");
|
|
3771
3802
|
function detectFormat(inputPath) {
|
|
3772
3803
|
if (inputPath.endsWith(".flow.json") && (0, import_node_fs.existsSync)(inputPath)) {
|