@timo9378/flow2code 0.1.5 → 0.1.7

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 CHANGED
@@ -5,6 +5,34 @@ 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.7] — 2026-03-05
9
+
10
+ ### Performance
11
+ - **Precompute `edgeSuccessors` map** — Eliminated O(N×E) per-call rebuild in `generateBlockContinuation`; successor lookup is now O(1) via pre-built map in `CompilerContext`
12
+ - **Reuse `nodeMap` in control-flow analysis** — `computeControlFlowDescendants` now receives the existing `nodeMap` instead of rebuilding a redundant `new Map()` each call
13
+
14
+ ### Fixed
15
+ - **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
16
+ - **OpenAPI YAML import** — `handleImportOpenAPI` now supports `.yaml`/`.yml` files via dynamic `import("yaml")` instead of silently failing with `JSON.parse`
17
+ - **`revokeObjectURL` download race** — Deferred `URL.revokeObjectURL` by 10 seconds after `click()` to prevent Safari/slow-browser download failures
18
+ - **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
19
+
20
+ ### Tests
21
+ - Added `loadFlowProjectAsync` parity tests (split + JSON + error case)
22
+ - Test count: 410 tests / 33 test files
23
+
24
+ ## [0.1.6] — 2026-03-05
25
+
26
+ ### Fixed (Critical)
27
+ - **Compiler uses migratedIR** — `compile()` now uses validator's auto-migrated IR instead of the stale original, preventing silent data corruption on older IR versions
28
+ - **`generateConcurrentNodes` flowState overwrite** — Removed `flowState[nodeId] = rN` overwrite that replaced correct values with `undefined` (task functions already populate flowState internally)
29
+ - **`handleCompile` path traversal** — Added separator suffix to `startsWith()` check, preventing directory-prefix attacks (e.g. `/home/user` → `/home/user2/`)
30
+ - **Undo/Redo snapshot deep clone** — `createSnapshot()` now uses `structuredClone(n.data)` instead of shallow spread, preventing nested mutation from corrupting all snapshots sharing a reference
31
+
32
+ ### Tests
33
+ - Added path traversal regression test, IR migration integration test
34
+ - Test count: 407 tests / 33 test files
35
+
8
36
  ## [0.1.5] — 2026-03-05
9
37
 
10
38
  ### Security
package/dist/cli.js CHANGED
@@ -1822,21 +1822,22 @@ function compile(ir, options) {
1822
1822
  errors: validation.errors.map((e) => `[${e.code}] ${e.message}`)
1823
1823
  };
1824
1824
  }
1825
+ const workingIR = validation.migrated && validation.migratedIR ? validation.migratedIR : ir;
1825
1826
  let plan;
1826
1827
  try {
1827
- plan = topologicalSort(ir);
1828
+ plan = topologicalSort(workingIR);
1828
1829
  } catch (err) {
1829
1830
  return {
1830
1831
  success: false,
1831
1832
  errors: [err instanceof Error ? err.message : String(err)]
1832
1833
  };
1833
1834
  }
1834
- const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1835
+ const nodeMap = new Map(workingIR.nodes.map((n) => [n.id, n]));
1835
1836
  const platformName = options?.platform ?? "nextjs";
1836
1837
  const platform = getPlatform(platformName);
1837
- const symbolTable = buildSymbolTable(ir);
1838
+ const symbolTable = buildSymbolTable(workingIR);
1838
1839
  const context = {
1839
- ir,
1840
+ ir: workingIR,
1840
1841
  plan,
1841
1842
  nodeMap,
1842
1843
  envVars: /* @__PURE__ */ new Set(),
@@ -1851,10 +1852,18 @@ function compile(ir, options) {
1851
1852
  dagMode: false,
1852
1853
  symbolTableExclusions: /* @__PURE__ */ new Set(),
1853
1854
  generatedBlockNodeIds: /* @__PURE__ */ new Set(),
1854
- pluginRegistry
1855
+ pluginRegistry,
1856
+ edgeSuccessors: (() => {
1857
+ const map = /* @__PURE__ */ new Map();
1858
+ for (const edge of workingIR.edges) {
1859
+ if (!map.has(edge.sourceNodeId)) map.set(edge.sourceNodeId, []);
1860
+ map.get(edge.sourceNodeId).push(edge.targetNodeId);
1861
+ }
1862
+ return map;
1863
+ })()
1855
1864
  };
1856
- const trigger = ir.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
1857
- const preComputedBlockNodes = computeControlFlowDescendants(ir, trigger.id);
1865
+ const trigger = workingIR.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
1866
+ const preComputedBlockNodes = computeControlFlowDescendants(workingIR, trigger.id, nodeMap);
1858
1867
  for (const nodeId of preComputedBlockNodes) {
1859
1868
  context.childBlockNodeIds.add(nodeId);
1860
1869
  context.symbolTableExclusions.add(nodeId);
@@ -1864,7 +1873,7 @@ function compile(ir, options) {
1864
1873
  );
1865
1874
  if (hasConcurrency) {
1866
1875
  context.dagMode = true;
1867
- for (const node of ir.nodes) {
1876
+ for (const node of workingIR.nodes) {
1868
1877
  if (node.category !== "trigger" /* TRIGGER */) {
1869
1878
  context.symbolTableExclusions.add(node.id);
1870
1879
  }
@@ -1879,8 +1888,8 @@ function compile(ir, options) {
1879
1888
  });
1880
1889
  const code = sourceFile.getFullText();
1881
1890
  const filePath = platform.getOutputFilePath(trigger);
1882
- collectRequiredPackages(ir, context);
1883
- const sourceMap = buildSourceMap(code, ir, filePath);
1891
+ collectRequiredPackages(workingIR, context);
1892
+ const sourceMap = buildSourceMap(code, workingIR, filePath);
1884
1893
  const dependencies = {
1885
1894
  all: [...context.requiredPackages].sort(),
1886
1895
  missing: [...context.requiredPackages].sort(),
@@ -1934,8 +1943,7 @@ function isControlFlowEdge(edge, nodeMap) {
1934
1943
  const controlPorts = CONTROL_FLOW_PORT_MAP[sourceNode.nodeType];
1935
1944
  return controlPorts !== void 0 && controlPorts.has(edge.sourcePortId);
1936
1945
  }
1937
- function computeControlFlowDescendants(ir, triggerId) {
1938
- const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1946
+ function computeControlFlowDescendants(ir, triggerId, nodeMap) {
1939
1947
  const strippedSuccessors = /* @__PURE__ */ new Map();
1940
1948
  for (const node of ir.nodes) {
1941
1949
  strippedSuccessors.set(node.id, /* @__PURE__ */ new Set());
@@ -1968,13 +1976,7 @@ function computeControlFlowDescendants(ir, triggerId) {
1968
1976
  function generateBlockContinuation(writer, fromNodeId, context) {
1969
1977
  const reachable = /* @__PURE__ */ new Set();
1970
1978
  const bfsQueue = [fromNodeId];
1971
- const edgeSuccessors = /* @__PURE__ */ new Map();
1972
- for (const edge of context.ir.edges) {
1973
- if (!edgeSuccessors.has(edge.sourceNodeId)) {
1974
- edgeSuccessors.set(edge.sourceNodeId, []);
1975
- }
1976
- edgeSuccessors.get(edge.sourceNodeId).push(edge.targetNodeId);
1977
- }
1979
+ const edgeSuccessors = context.edgeSuccessors;
1978
1980
  while (bfsQueue.length > 0) {
1979
1981
  const id = bfsQueue.shift();
1980
1982
  if (reachable.has(id)) continue;
@@ -2129,11 +2131,8 @@ function generateConcurrentNodes(writer, nodeIds, context) {
2129
2131
  writer.writeLine(";");
2130
2132
  }
2131
2133
  writer.writeLine(
2132
- `const [${taskNames.map((_, i) => `r${i}`).join(", ")}] = await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
2134
+ `await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
2133
2135
  );
2134
- activeNodeIds.forEach((nodeId, i) => {
2135
- writer.writeLine(`flowState['${nodeId}'] = r${i};`);
2136
- });
2137
2136
  activeNodeIds.forEach((nodeId) => {
2138
2137
  const varName = context.symbolTable.getVarName(nodeId);
2139
2138
  writer.writeLine(`const ${varName} = flowState['${nodeId}'];`);
@@ -2766,7 +2765,10 @@ function processForInStatement(stmt, _prevNodeId, ctx, line) {
2766
2765
  }
2767
2766
  function processForStatement(stmt, _prevNodeId, ctx, line) {
2768
2767
  const nodeId = ctx.nextId("loop");
2769
- const fullText = stmt.getText().split("{")[0].trim();
2768
+ const initText = stmt.getInitializer()?.getText() ?? "";
2769
+ const condText = stmt.getCondition()?.getText() ?? "";
2770
+ const incrText = stmt.getIncrementor()?.getText() ?? "";
2771
+ const fullText = `for (${initText}; ${condText}; ${incrText})`;
2770
2772
  ctx.addNode({
2771
2773
  id: nodeId,
2772
2774
  nodeType: "for_loop" /* FOR_LOOP */,
@@ -3707,7 +3709,9 @@ function handleCompile(body, projectRoot) {
3707
3709
  let writtenPath = null;
3708
3710
  if (shouldWrite && result.filePath && result.code) {
3709
3711
  const fullPath = resolve(join2(projectRoot, result.filePath));
3710
- if (!fullPath.startsWith(resolve(projectRoot))) {
3712
+ const resolvedRoot = resolve(projectRoot);
3713
+ const sep = resolvedRoot.endsWith("/") || resolvedRoot.endsWith("\\") ? "" : process.platform === "win32" ? "\\" : "/";
3714
+ if (!fullPath.startsWith(resolvedRoot + sep)) {
3711
3715
  return {
3712
3716
  status: 400,
3713
3717
  body: { success: false, error: "Output path escapes project root" }
@@ -3946,7 +3950,7 @@ __export(server_exports, {
3946
3950
  startServer: () => startServer
3947
3951
  });
3948
3952
  import { createServer } from "http";
3949
- import { readFile, stat } from "fs/promises";
3953
+ import { readFile as readFile2, stat } from "fs/promises";
3950
3954
  import { join as join3, extname as extname2, dirname as dirname3, resolve as resolve2 } from "path";
3951
3955
  import { fileURLToPath } from "url";
3952
3956
  import { existsSync as existsSync3 } from "fs";
@@ -4044,7 +4048,7 @@ async function serveStatic(staticDir, pathname, res) {
4044
4048
  if (!s.isFile()) return false;
4045
4049
  const ext = extname2(filePath).toLowerCase();
4046
4050
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
4047
- const content = await readFile(filePath);
4051
+ const content = await readFile2(filePath);
4048
4052
  res.writeHead(200, { "Content-Type": contentType });
4049
4053
  res.end(content);
4050
4054
  return true;
@@ -4108,7 +4112,7 @@ async function handleRequest(req, res, staticDir, projectRoot) {
4108
4112
  if (served) return;
4109
4113
  const indexPath = join3(staticDir, "index.html");
4110
4114
  try {
4111
- const content = await readFile(indexPath, "utf-8");
4115
+ const content = await readFile2(indexPath, "utf-8");
4112
4116
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
4113
4117
  res.end(content);
4114
4118
  } catch {
@@ -4190,7 +4194,7 @@ init_logger();
4190
4194
  init_validator();
4191
4195
  import { Command } from "commander";
4192
4196
  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 readFile2, writeFile, mkdir } from "fs/promises";
4197
+ import { readFile as readFile3, writeFile, mkdir } from "fs/promises";
4194
4198
  import { join as join4, dirname as dirname4, resolve as resolve3, basename as basename2 } from "path";
4195
4199
  import { watch } from "chokidar";
4196
4200
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -4329,6 +4333,7 @@ function sanitizeFilename(id) {
4329
4333
 
4330
4334
  // src/lib/storage/flow-project.ts
4331
4335
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, statSync } from "fs";
4336
+ import { readFile, readdir } from "fs/promises";
4332
4337
  import { join, dirname } from "path";
4333
4338
  function detectFormat(inputPath) {
4334
4339
  if (inputPath.endsWith(".flow.json") && existsSync(inputPath)) {
@@ -4379,6 +4384,37 @@ function loadFlowProject(inputPath) {
4379
4384
  const ir = mergeIR({ meta, edges, nodes });
4380
4385
  return { path: resolvedPath, format, ir };
4381
4386
  }
4387
+ async function loadFlowProjectAsync(inputPath) {
4388
+ const { resolvedPath, format } = detectFormat(inputPath);
4389
+ if (format === "json") {
4390
+ if (!existsSync(resolvedPath)) {
4391
+ throw new Error(`Flow file not found: ${resolvedPath}`);
4392
+ }
4393
+ const raw = await readFile(resolvedPath, "utf-8");
4394
+ const ir2 = JSON.parse(raw);
4395
+ return { path: resolvedPath, format, ir: ir2 };
4396
+ }
4397
+ if (!existsSync(resolvedPath)) {
4398
+ throw new Error(`Flow directory not found: ${resolvedPath}`);
4399
+ }
4400
+ const metaPath = join(resolvedPath, "meta.yaml");
4401
+ if (!existsSync(metaPath)) {
4402
+ throw new Error(`meta.yaml not found in ${resolvedPath} \u2014 not a valid Flow directory`);
4403
+ }
4404
+ const meta = await readFile(metaPath, "utf-8");
4405
+ const edgesPath = join(resolvedPath, "edges.yaml");
4406
+ const edges = existsSync(edgesPath) ? await readFile(edgesPath, "utf-8") : "";
4407
+ const nodesDir = join(resolvedPath, "nodes");
4408
+ const nodes = /* @__PURE__ */ new Map();
4409
+ if (existsSync(nodesDir)) {
4410
+ const nodeFiles = (await readdir(nodesDir)).filter((f) => f.endsWith(".yaml"));
4411
+ for (const file of nodeFiles) {
4412
+ nodes.set(file, await readFile(join(nodesDir, file), "utf-8"));
4413
+ }
4414
+ }
4415
+ const ir = mergeIR({ meta, edges, nodes });
4416
+ return { path: resolvedPath, format, ir };
4417
+ }
4382
4418
  function saveFlowProject(ir, outputPath, options = {}) {
4383
4419
  const { format = "split", cleanOrphanNodes = true } = options;
4384
4420
  if (format === "json") {
@@ -5310,7 +5346,7 @@ function generateEnvExample() {
5310
5346
  async function compileFileAsync(filePath, projectRoot) {
5311
5347
  const startTime = Date.now();
5312
5348
  try {
5313
- const raw = await readFile2(filePath, "utf-8");
5349
+ const raw = await readFile3(filePath, "utf-8");
5314
5350
  const ir = JSON.parse(raw);
5315
5351
  const validation = validateFlowIR(ir);
5316
5352
  if (!validation.valid) {
@@ -5345,7 +5381,7 @@ async function compileFileAsync(filePath, projectRoot) {
5345
5381
  async function compileFlowDirAsync(dirPath, projectRoot) {
5346
5382
  const startTime = Date.now();
5347
5383
  try {
5348
- const project = loadFlowProject(dirPath);
5384
+ const project = await loadFlowProjectAsync(dirPath);
5349
5385
  const ir = project.ir;
5350
5386
  const validation = validateFlowIR(ir);
5351
5387
  if (!validation.valid) {
package/dist/compiler.cjs CHANGED
@@ -1831,21 +1831,22 @@ function compile(ir, options) {
1831
1831
  errors: validation.errors.map((e) => `[${e.code}] ${e.message}`)
1832
1832
  };
1833
1833
  }
1834
+ const workingIR = validation.migrated && validation.migratedIR ? validation.migratedIR : ir;
1834
1835
  let plan;
1835
1836
  try {
1836
- plan = topologicalSort(ir);
1837
+ plan = topologicalSort(workingIR);
1837
1838
  } catch (err) {
1838
1839
  return {
1839
1840
  success: false,
1840
1841
  errors: [err instanceof Error ? err.message : String(err)]
1841
1842
  };
1842
1843
  }
1843
- const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1844
+ const nodeMap = new Map(workingIR.nodes.map((n) => [n.id, n]));
1844
1845
  const platformName = options?.platform ?? "nextjs";
1845
1846
  const platform = getPlatform(platformName);
1846
- const symbolTable = buildSymbolTable(ir);
1847
+ const symbolTable = buildSymbolTable(workingIR);
1847
1848
  const context = {
1848
- ir,
1849
+ ir: workingIR,
1849
1850
  plan,
1850
1851
  nodeMap,
1851
1852
  envVars: /* @__PURE__ */ new Set(),
@@ -1860,10 +1861,18 @@ function compile(ir, options) {
1860
1861
  dagMode: false,
1861
1862
  symbolTableExclusions: /* @__PURE__ */ new Set(),
1862
1863
  generatedBlockNodeIds: /* @__PURE__ */ new Set(),
1863
- pluginRegistry
1864
+ pluginRegistry,
1865
+ edgeSuccessors: (() => {
1866
+ const map = /* @__PURE__ */ new Map();
1867
+ for (const edge of workingIR.edges) {
1868
+ if (!map.has(edge.sourceNodeId)) map.set(edge.sourceNodeId, []);
1869
+ map.get(edge.sourceNodeId).push(edge.targetNodeId);
1870
+ }
1871
+ return map;
1872
+ })()
1864
1873
  };
1865
- const trigger = ir.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
1866
- const preComputedBlockNodes = computeControlFlowDescendants(ir, trigger.id);
1874
+ const trigger = workingIR.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
1875
+ const preComputedBlockNodes = computeControlFlowDescendants(workingIR, trigger.id, nodeMap);
1867
1876
  for (const nodeId of preComputedBlockNodes) {
1868
1877
  context.childBlockNodeIds.add(nodeId);
1869
1878
  context.symbolTableExclusions.add(nodeId);
@@ -1873,7 +1882,7 @@ function compile(ir, options) {
1873
1882
  );
1874
1883
  if (hasConcurrency) {
1875
1884
  context.dagMode = true;
1876
- for (const node of ir.nodes) {
1885
+ for (const node of workingIR.nodes) {
1877
1886
  if (node.category !== "trigger" /* TRIGGER */) {
1878
1887
  context.symbolTableExclusions.add(node.id);
1879
1888
  }
@@ -1888,8 +1897,8 @@ function compile(ir, options) {
1888
1897
  });
1889
1898
  const code = sourceFile.getFullText();
1890
1899
  const filePath = platform.getOutputFilePath(trigger);
1891
- collectRequiredPackages(ir, context);
1892
- const sourceMap = buildSourceMap(code, ir, filePath);
1900
+ collectRequiredPackages(workingIR, context);
1901
+ const sourceMap = buildSourceMap(code, workingIR, filePath);
1893
1902
  const dependencies = {
1894
1903
  all: [...context.requiredPackages].sort(),
1895
1904
  missing: [...context.requiredPackages].sort(),
@@ -1948,8 +1957,7 @@ function isControlFlowEdge(edge, nodeMap) {
1948
1957
  const controlPorts = CONTROL_FLOW_PORT_MAP[sourceNode.nodeType];
1949
1958
  return controlPorts !== void 0 && controlPorts.has(edge.sourcePortId);
1950
1959
  }
1951
- function computeControlFlowDescendants(ir, triggerId) {
1952
- const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1960
+ function computeControlFlowDescendants(ir, triggerId, nodeMap) {
1953
1961
  const strippedSuccessors = /* @__PURE__ */ new Map();
1954
1962
  for (const node of ir.nodes) {
1955
1963
  strippedSuccessors.set(node.id, /* @__PURE__ */ new Set());
@@ -1982,13 +1990,7 @@ function computeControlFlowDescendants(ir, triggerId) {
1982
1990
  function generateBlockContinuation(writer, fromNodeId, context) {
1983
1991
  const reachable = /* @__PURE__ */ new Set();
1984
1992
  const bfsQueue = [fromNodeId];
1985
- const edgeSuccessors = /* @__PURE__ */ new Map();
1986
- for (const edge of context.ir.edges) {
1987
- if (!edgeSuccessors.has(edge.sourceNodeId)) {
1988
- edgeSuccessors.set(edge.sourceNodeId, []);
1989
- }
1990
- edgeSuccessors.get(edge.sourceNodeId).push(edge.targetNodeId);
1991
- }
1993
+ const edgeSuccessors = context.edgeSuccessors;
1992
1994
  while (bfsQueue.length > 0) {
1993
1995
  const id = bfsQueue.shift();
1994
1996
  if (reachable.has(id)) continue;
@@ -2143,11 +2145,8 @@ function generateConcurrentNodes(writer, nodeIds, context) {
2143
2145
  writer.writeLine(";");
2144
2146
  }
2145
2147
  writer.writeLine(
2146
- `const [${taskNames.map((_, i) => `r${i}`).join(", ")}] = await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
2148
+ `await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
2147
2149
  );
2148
- activeNodeIds.forEach((nodeId, i) => {
2149
- writer.writeLine(`flowState['${nodeId}'] = r${i};`);
2150
- });
2151
2150
  activeNodeIds.forEach((nodeId) => {
2152
2151
  const varName = context.symbolTable.getVarName(nodeId);
2153
2152
  writer.writeLine(`const ${varName} = flowState['${nodeId}'];`);
@@ -2820,7 +2819,10 @@ function processForInStatement(stmt, _prevNodeId, ctx, line) {
2820
2819
  }
2821
2820
  function processForStatement(stmt, _prevNodeId, ctx, line) {
2822
2821
  const nodeId = ctx.nextId("loop");
2823
- const fullText = stmt.getText().split("{")[0].trim();
2822
+ const initText = stmt.getInitializer()?.getText() ?? "";
2823
+ const condText = stmt.getCondition()?.getText() ?? "";
2824
+ const incrText = stmt.getIncrementor()?.getText() ?? "";
2825
+ const fullText = `for (${initText}; ${condText}; ${incrText})`;
2824
2826
  ctx.addNode({
2825
2827
  id: nodeId,
2826
2828
  nodeType: "for_loop" /* FOR_LOOP */,
@@ -3769,6 +3771,7 @@ function sanitizeFilename(id) {
3769
3771
 
3770
3772
  // src/lib/storage/flow-project.ts
3771
3773
  var import_node_fs = require("fs");
3774
+ var import_promises = require("fs/promises");
3772
3775
  var import_node_path = require("path");
3773
3776
  function detectFormat(inputPath) {
3774
3777
  if (inputPath.endsWith(".flow.json") && (0, import_node_fs.existsSync)(inputPath)) {
package/dist/compiler.js CHANGED
@@ -1764,21 +1764,22 @@ function compile(ir, options) {
1764
1764
  errors: validation.errors.map((e) => `[${e.code}] ${e.message}`)
1765
1765
  };
1766
1766
  }
1767
+ const workingIR = validation.migrated && validation.migratedIR ? validation.migratedIR : ir;
1767
1768
  let plan;
1768
1769
  try {
1769
- plan = topologicalSort(ir);
1770
+ plan = topologicalSort(workingIR);
1770
1771
  } catch (err) {
1771
1772
  return {
1772
1773
  success: false,
1773
1774
  errors: [err instanceof Error ? err.message : String(err)]
1774
1775
  };
1775
1776
  }
1776
- const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1777
+ const nodeMap = new Map(workingIR.nodes.map((n) => [n.id, n]));
1777
1778
  const platformName = options?.platform ?? "nextjs";
1778
1779
  const platform = getPlatform(platformName);
1779
- const symbolTable = buildSymbolTable(ir);
1780
+ const symbolTable = buildSymbolTable(workingIR);
1780
1781
  const context = {
1781
- ir,
1782
+ ir: workingIR,
1782
1783
  plan,
1783
1784
  nodeMap,
1784
1785
  envVars: /* @__PURE__ */ new Set(),
@@ -1793,10 +1794,18 @@ function compile(ir, options) {
1793
1794
  dagMode: false,
1794
1795
  symbolTableExclusions: /* @__PURE__ */ new Set(),
1795
1796
  generatedBlockNodeIds: /* @__PURE__ */ new Set(),
1796
- pluginRegistry
1797
+ pluginRegistry,
1798
+ edgeSuccessors: (() => {
1799
+ const map = /* @__PURE__ */ new Map();
1800
+ for (const edge of workingIR.edges) {
1801
+ if (!map.has(edge.sourceNodeId)) map.set(edge.sourceNodeId, []);
1802
+ map.get(edge.sourceNodeId).push(edge.targetNodeId);
1803
+ }
1804
+ return map;
1805
+ })()
1797
1806
  };
1798
- const trigger = ir.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
1799
- const preComputedBlockNodes = computeControlFlowDescendants(ir, trigger.id);
1807
+ const trigger = workingIR.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
1808
+ const preComputedBlockNodes = computeControlFlowDescendants(workingIR, trigger.id, nodeMap);
1800
1809
  for (const nodeId of preComputedBlockNodes) {
1801
1810
  context.childBlockNodeIds.add(nodeId);
1802
1811
  context.symbolTableExclusions.add(nodeId);
@@ -1806,7 +1815,7 @@ function compile(ir, options) {
1806
1815
  );
1807
1816
  if (hasConcurrency) {
1808
1817
  context.dagMode = true;
1809
- for (const node of ir.nodes) {
1818
+ for (const node of workingIR.nodes) {
1810
1819
  if (node.category !== "trigger" /* TRIGGER */) {
1811
1820
  context.symbolTableExclusions.add(node.id);
1812
1821
  }
@@ -1821,8 +1830,8 @@ function compile(ir, options) {
1821
1830
  });
1822
1831
  const code = sourceFile.getFullText();
1823
1832
  const filePath = platform.getOutputFilePath(trigger);
1824
- collectRequiredPackages(ir, context);
1825
- const sourceMap = buildSourceMap(code, ir, filePath);
1833
+ collectRequiredPackages(workingIR, context);
1834
+ const sourceMap = buildSourceMap(code, workingIR, filePath);
1826
1835
  const dependencies = {
1827
1836
  all: [...context.requiredPackages].sort(),
1828
1837
  missing: [...context.requiredPackages].sort(),
@@ -1881,8 +1890,7 @@ function isControlFlowEdge(edge, nodeMap) {
1881
1890
  const controlPorts = CONTROL_FLOW_PORT_MAP[sourceNode.nodeType];
1882
1891
  return controlPorts !== void 0 && controlPorts.has(edge.sourcePortId);
1883
1892
  }
1884
- function computeControlFlowDescendants(ir, triggerId) {
1885
- const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1893
+ function computeControlFlowDescendants(ir, triggerId, nodeMap) {
1886
1894
  const strippedSuccessors = /* @__PURE__ */ new Map();
1887
1895
  for (const node of ir.nodes) {
1888
1896
  strippedSuccessors.set(node.id, /* @__PURE__ */ new Set());
@@ -1915,13 +1923,7 @@ function computeControlFlowDescendants(ir, triggerId) {
1915
1923
  function generateBlockContinuation(writer, fromNodeId, context) {
1916
1924
  const reachable = /* @__PURE__ */ new Set();
1917
1925
  const bfsQueue = [fromNodeId];
1918
- const edgeSuccessors = /* @__PURE__ */ new Map();
1919
- for (const edge of context.ir.edges) {
1920
- if (!edgeSuccessors.has(edge.sourceNodeId)) {
1921
- edgeSuccessors.set(edge.sourceNodeId, []);
1922
- }
1923
- edgeSuccessors.get(edge.sourceNodeId).push(edge.targetNodeId);
1924
- }
1926
+ const edgeSuccessors = context.edgeSuccessors;
1925
1927
  while (bfsQueue.length > 0) {
1926
1928
  const id = bfsQueue.shift();
1927
1929
  if (reachable.has(id)) continue;
@@ -2076,11 +2078,8 @@ function generateConcurrentNodes(writer, nodeIds, context) {
2076
2078
  writer.writeLine(";");
2077
2079
  }
2078
2080
  writer.writeLine(
2079
- `const [${taskNames.map((_, i) => `r${i}`).join(", ")}] = await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
2081
+ `await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
2080
2082
  );
2081
- activeNodeIds.forEach((nodeId, i) => {
2082
- writer.writeLine(`flowState['${nodeId}'] = r${i};`);
2083
- });
2084
2083
  activeNodeIds.forEach((nodeId) => {
2085
2084
  const varName = context.symbolTable.getVarName(nodeId);
2086
2085
  writer.writeLine(`const ${varName} = flowState['${nodeId}'];`);
@@ -2756,7 +2755,10 @@ function processForInStatement(stmt, _prevNodeId, ctx, line) {
2756
2755
  }
2757
2756
  function processForStatement(stmt, _prevNodeId, ctx, line) {
2758
2757
  const nodeId = ctx.nextId("loop");
2759
- const fullText = stmt.getText().split("{")[0].trim();
2758
+ const initText = stmt.getInitializer()?.getText() ?? "";
2759
+ const condText = stmt.getCondition()?.getText() ?? "";
2760
+ const incrText = stmt.getIncrementor()?.getText() ?? "";
2761
+ const fullText = `for (${initText}; ${condText}; ${incrText})`;
2760
2762
  ctx.addNode({
2761
2763
  id: nodeId,
2762
2764
  nodeType: "for_loop" /* FOR_LOOP */,
@@ -3705,6 +3707,7 @@ function sanitizeFilename(id) {
3705
3707
 
3706
3708
  // src/lib/storage/flow-project.ts
3707
3709
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, statSync } from "fs";
3710
+ import { readFile, readdir } from "fs/promises";
3708
3711
  import { join, dirname } from "path";
3709
3712
  function detectFormat(inputPath) {
3710
3713
  if (inputPath.endsWith(".flow.json") && existsSync(inputPath)) {
package/dist/server.js CHANGED
@@ -1717,21 +1717,22 @@ function compile(ir, options) {
1717
1717
  errors: validation.errors.map((e) => `[${e.code}] ${e.message}`)
1718
1718
  };
1719
1719
  }
1720
+ const workingIR = validation.migrated && validation.migratedIR ? validation.migratedIR : ir;
1720
1721
  let plan;
1721
1722
  try {
1722
- plan = topologicalSort(ir);
1723
+ plan = topologicalSort(workingIR);
1723
1724
  } catch (err) {
1724
1725
  return {
1725
1726
  success: false,
1726
1727
  errors: [err instanceof Error ? err.message : String(err)]
1727
1728
  };
1728
1729
  }
1729
- const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1730
+ const nodeMap = new Map(workingIR.nodes.map((n) => [n.id, n]));
1730
1731
  const platformName = options?.platform ?? "nextjs";
1731
1732
  const platform = getPlatform(platformName);
1732
- const symbolTable = buildSymbolTable(ir);
1733
+ const symbolTable = buildSymbolTable(workingIR);
1733
1734
  const context = {
1734
- ir,
1735
+ ir: workingIR,
1735
1736
  plan,
1736
1737
  nodeMap,
1737
1738
  envVars: /* @__PURE__ */ new Set(),
@@ -1746,10 +1747,18 @@ function compile(ir, options) {
1746
1747
  dagMode: false,
1747
1748
  symbolTableExclusions: /* @__PURE__ */ new Set(),
1748
1749
  generatedBlockNodeIds: /* @__PURE__ */ new Set(),
1749
- pluginRegistry
1750
+ pluginRegistry,
1751
+ edgeSuccessors: (() => {
1752
+ const map = /* @__PURE__ */ new Map();
1753
+ for (const edge of workingIR.edges) {
1754
+ if (!map.has(edge.sourceNodeId)) map.set(edge.sourceNodeId, []);
1755
+ map.get(edge.sourceNodeId).push(edge.targetNodeId);
1756
+ }
1757
+ return map;
1758
+ })()
1750
1759
  };
1751
- const trigger = ir.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
1752
- const preComputedBlockNodes = computeControlFlowDescendants(ir, trigger.id);
1760
+ const trigger = workingIR.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
1761
+ const preComputedBlockNodes = computeControlFlowDescendants(workingIR, trigger.id, nodeMap);
1753
1762
  for (const nodeId of preComputedBlockNodes) {
1754
1763
  context.childBlockNodeIds.add(nodeId);
1755
1764
  context.symbolTableExclusions.add(nodeId);
@@ -1759,7 +1768,7 @@ function compile(ir, options) {
1759
1768
  );
1760
1769
  if (hasConcurrency) {
1761
1770
  context.dagMode = true;
1762
- for (const node of ir.nodes) {
1771
+ for (const node of workingIR.nodes) {
1763
1772
  if (node.category !== "trigger" /* TRIGGER */) {
1764
1773
  context.symbolTableExclusions.add(node.id);
1765
1774
  }
@@ -1774,8 +1783,8 @@ function compile(ir, options) {
1774
1783
  });
1775
1784
  const code = sourceFile.getFullText();
1776
1785
  const filePath = platform.getOutputFilePath(trigger);
1777
- collectRequiredPackages(ir, context);
1778
- const sourceMap = buildSourceMap(code, ir, filePath);
1786
+ collectRequiredPackages(workingIR, context);
1787
+ const sourceMap = buildSourceMap(code, workingIR, filePath);
1779
1788
  const dependencies = {
1780
1789
  all: [...context.requiredPackages].sort(),
1781
1790
  missing: [...context.requiredPackages].sort(),
@@ -1834,8 +1843,7 @@ function isControlFlowEdge(edge, nodeMap) {
1834
1843
  const controlPorts = CONTROL_FLOW_PORT_MAP[sourceNode.nodeType];
1835
1844
  return controlPorts !== void 0 && controlPorts.has(edge.sourcePortId);
1836
1845
  }
1837
- function computeControlFlowDescendants(ir, triggerId) {
1838
- const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1846
+ function computeControlFlowDescendants(ir, triggerId, nodeMap) {
1839
1847
  const strippedSuccessors = /* @__PURE__ */ new Map();
1840
1848
  for (const node of ir.nodes) {
1841
1849
  strippedSuccessors.set(node.id, /* @__PURE__ */ new Set());
@@ -1868,13 +1876,7 @@ function computeControlFlowDescendants(ir, triggerId) {
1868
1876
  function generateBlockContinuation(writer, fromNodeId, context) {
1869
1877
  const reachable = /* @__PURE__ */ new Set();
1870
1878
  const bfsQueue = [fromNodeId];
1871
- const edgeSuccessors = /* @__PURE__ */ new Map();
1872
- for (const edge of context.ir.edges) {
1873
- if (!edgeSuccessors.has(edge.sourceNodeId)) {
1874
- edgeSuccessors.set(edge.sourceNodeId, []);
1875
- }
1876
- edgeSuccessors.get(edge.sourceNodeId).push(edge.targetNodeId);
1877
- }
1879
+ const edgeSuccessors = context.edgeSuccessors;
1878
1880
  while (bfsQueue.length > 0) {
1879
1881
  const id = bfsQueue.shift();
1880
1882
  if (reachable.has(id)) continue;
@@ -2029,11 +2031,8 @@ function generateConcurrentNodes(writer, nodeIds, context) {
2029
2031
  writer.writeLine(";");
2030
2032
  }
2031
2033
  writer.writeLine(
2032
- `const [${taskNames.map((_, i) => `r${i}`).join(", ")}] = await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
2034
+ `await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
2033
2035
  );
2034
- activeNodeIds.forEach((nodeId, i) => {
2035
- writer.writeLine(`flowState['${nodeId}'] = r${i};`);
2036
- });
2037
2036
  activeNodeIds.forEach((nodeId) => {
2038
2037
  const varName = context.symbolTable.getVarName(nodeId);
2039
2038
  writer.writeLine(`const ${varName} = flowState['${nodeId}'];`);
@@ -2569,7 +2568,10 @@ function processForInStatement(stmt, _prevNodeId, ctx, line) {
2569
2568
  }
2570
2569
  function processForStatement(stmt, _prevNodeId, ctx, line) {
2571
2570
  const nodeId = ctx.nextId("loop");
2572
- const fullText = stmt.getText().split("{")[0].trim();
2571
+ const initText = stmt.getInitializer()?.getText() ?? "";
2572
+ const condText = stmt.getCondition()?.getText() ?? "";
2573
+ const incrText = stmt.getIncrementor()?.getText() ?? "";
2574
+ const fullText = `for (${initText}; ${condText}; ${incrText})`;
2573
2575
  ctx.addNode({
2574
2576
  id: nodeId,
2575
2577
  nodeType: "for_loop" /* FOR_LOOP */,
@@ -3485,7 +3487,9 @@ function handleCompile(body, projectRoot) {
3485
3487
  let writtenPath = null;
3486
3488
  if (shouldWrite && result.filePath && result.code) {
3487
3489
  const fullPath = resolve(join(projectRoot, result.filePath));
3488
- if (!fullPath.startsWith(resolve(projectRoot))) {
3490
+ const resolvedRoot = resolve(projectRoot);
3491
+ const sep = resolvedRoot.endsWith("/") || resolvedRoot.endsWith("\\") ? "" : process.platform === "win32" ? "\\" : "/";
3492
+ if (!fullPath.startsWith(resolvedRoot + sep)) {
3489
3493
  return {
3490
3494
  status: 400,
3491
3495
  body: { success: false, error: "Output path escapes project root" }