@timo9378/flow2code 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/cli.js +5211 -0
- package/dist/compiler.cjs +4177 -0
- package/dist/compiler.d.cts +1230 -0
- package/dist/compiler.d.ts +1230 -0
- package/dist/compiler.js +4112 -0
- package/dist/server.d.ts +27 -0
- package/dist/server.js +3042 -0
- package/package.json +120 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,3042 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server/index.ts
|
|
4
|
+
import { createServer } from "http";
|
|
5
|
+
import { readFile, stat } from "fs/promises";
|
|
6
|
+
import { join as join2, extname, dirname as dirname2 } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { existsSync as existsSync2 } from "fs";
|
|
9
|
+
|
|
10
|
+
// src/lib/compiler/compiler.ts
|
|
11
|
+
import { Project } from "ts-morph";
|
|
12
|
+
|
|
13
|
+
// src/lib/ir/types.ts
|
|
14
|
+
var CURRENT_IR_VERSION = "1.0.0";
|
|
15
|
+
|
|
16
|
+
// src/lib/ir/migrations/engine.ts
|
|
17
|
+
var migrations = [];
|
|
18
|
+
function migrateIR(raw, targetVersion = CURRENT_IR_VERSION) {
|
|
19
|
+
const applied = [];
|
|
20
|
+
let current = { ...raw };
|
|
21
|
+
if (current.version === targetVersion) {
|
|
22
|
+
return { ir: current, applied, migrated: false };
|
|
23
|
+
}
|
|
24
|
+
const maxIterations = migrations.length + 1;
|
|
25
|
+
let iterations = 0;
|
|
26
|
+
while (current.version !== targetVersion) {
|
|
27
|
+
if (iterations++ > maxIterations) {
|
|
28
|
+
throw new MigrationError(
|
|
29
|
+
`Migration exceeded max iterations (${maxIterations}), possible circular migration`,
|
|
30
|
+
current.version,
|
|
31
|
+
targetVersion
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const migration = migrations.find(
|
|
35
|
+
(m) => m.fromVersion === current.version
|
|
36
|
+
);
|
|
37
|
+
if (!migration) {
|
|
38
|
+
throw new MigrationError(
|
|
39
|
+
`No migration path found from ${current.version} to ${targetVersion}`,
|
|
40
|
+
current.version,
|
|
41
|
+
targetVersion
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
current = migration.migrate(current);
|
|
45
|
+
applied.push(
|
|
46
|
+
`${migration.fromVersion} \u2192 ${migration.toVersion}: ${migration.description}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return { ir: current, applied, migrated: true };
|
|
50
|
+
}
|
|
51
|
+
function needsMigration(version, targetVersion = CURRENT_IR_VERSION) {
|
|
52
|
+
return version !== targetVersion;
|
|
53
|
+
}
|
|
54
|
+
var MigrationError = class extends Error {
|
|
55
|
+
constructor(message, fromVersion, targetVersion) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.fromVersion = fromVersion;
|
|
58
|
+
this.targetVersion = targetVersion;
|
|
59
|
+
this.name = "MigrationError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/lib/ir/validator.ts
|
|
64
|
+
function validateFlowIR(ir) {
|
|
65
|
+
const errors = [];
|
|
66
|
+
if (!ir || typeof ir !== "object") {
|
|
67
|
+
return {
|
|
68
|
+
valid: false,
|
|
69
|
+
errors: [{ code: "INVALID_INPUT", message: "IR input must be a non-null object" }]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(ir.nodes)) {
|
|
73
|
+
return {
|
|
74
|
+
valid: false,
|
|
75
|
+
errors: [{ code: "MISSING_NODES", message: "IR is missing required 'nodes' array" }]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(ir.edges)) {
|
|
79
|
+
return {
|
|
80
|
+
valid: false,
|
|
81
|
+
errors: [{ code: "MISSING_EDGES", message: "IR is missing required 'edges' array" }]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
let workingIR = ir;
|
|
85
|
+
let migrated = false;
|
|
86
|
+
let migrationLog;
|
|
87
|
+
if (needsMigration(ir.version)) {
|
|
88
|
+
try {
|
|
89
|
+
const result = migrateIR(
|
|
90
|
+
{ version: ir.version, meta: ir.meta, nodes: ir.nodes, edges: ir.edges },
|
|
91
|
+
CURRENT_IR_VERSION
|
|
92
|
+
);
|
|
93
|
+
if (result.migrated) {
|
|
94
|
+
workingIR = result.ir;
|
|
95
|
+
migrated = true;
|
|
96
|
+
migrationLog = result.applied;
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err instanceof MigrationError) {
|
|
100
|
+
errors.push({
|
|
101
|
+
code: "MIGRATION_FAILED",
|
|
102
|
+
message: `IR version migration failed: ${err.message}`
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
errors.push({
|
|
106
|
+
code: "INVALID_VERSION",
|
|
107
|
+
message: `Unsupported IR version: ${ir.version}`
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!migrated && workingIR.version !== CURRENT_IR_VERSION) {
|
|
113
|
+
errors.push({
|
|
114
|
+
code: "INVALID_VERSION",
|
|
115
|
+
message: `Unsupported IR version: ${workingIR.version} (current: ${CURRENT_IR_VERSION})`
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const workingNodeMap = new Map(workingIR.nodes.map((n) => [n.id, n]));
|
|
119
|
+
const triggers = workingIR.nodes.filter(
|
|
120
|
+
(n) => n.category === "trigger" /* TRIGGER */
|
|
121
|
+
);
|
|
122
|
+
if (triggers.length === 0) {
|
|
123
|
+
errors.push({
|
|
124
|
+
code: "NO_TRIGGER",
|
|
125
|
+
message: "Workflow must contain at least one trigger node"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (triggers.length > 1) {
|
|
129
|
+
errors.push({
|
|
130
|
+
code: "MULTIPLE_TRIGGERS",
|
|
131
|
+
message: `Workflow must have exactly one trigger, found ${triggers.length}`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const idSet = /* @__PURE__ */ new Set();
|
|
135
|
+
for (const node of workingIR.nodes) {
|
|
136
|
+
if (idSet.has(node.id)) {
|
|
137
|
+
errors.push({
|
|
138
|
+
code: "DUPLICATE_NODE_ID",
|
|
139
|
+
message: `Duplicate node ID: ${node.id}`,
|
|
140
|
+
nodeId: node.id
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
idSet.add(node.id);
|
|
144
|
+
}
|
|
145
|
+
for (const edge of workingIR.edges) {
|
|
146
|
+
if (!workingNodeMap.has(edge.sourceNodeId)) {
|
|
147
|
+
errors.push({
|
|
148
|
+
code: "INVALID_EDGE_SOURCE",
|
|
149
|
+
message: `Edge "${edge.id}" references non-existent source node "${edge.sourceNodeId}"`,
|
|
150
|
+
edgeId: edge.id
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (!workingNodeMap.has(edge.targetNodeId)) {
|
|
154
|
+
errors.push({
|
|
155
|
+
code: "INVALID_EDGE_TARGET",
|
|
156
|
+
message: `Edge "${edge.id}" references non-existent target node "${edge.targetNodeId}"`,
|
|
157
|
+
edgeId: edge.id
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const cycleErrors = detectCycles(workingIR.nodes, workingIR.edges);
|
|
162
|
+
errors.push(...cycleErrors);
|
|
163
|
+
const connectedNodes = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const edge of workingIR.edges) {
|
|
165
|
+
connectedNodes.add(edge.sourceNodeId);
|
|
166
|
+
connectedNodes.add(edge.targetNodeId);
|
|
167
|
+
}
|
|
168
|
+
for (const node of workingIR.nodes) {
|
|
169
|
+
if (node.category !== "trigger" /* TRIGGER */ && !connectedNodes.has(node.id)) {
|
|
170
|
+
errors.push({
|
|
171
|
+
code: "ORPHAN_NODE",
|
|
172
|
+
message: `Node "${node.id}" (${node.label}) is not connected to any other node`,
|
|
173
|
+
nodeId: node.id
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
valid: errors.length === 0,
|
|
179
|
+
errors,
|
|
180
|
+
migrated,
|
|
181
|
+
migratedIR: migrated ? workingIR : void 0,
|
|
182
|
+
migrationLog
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function detectCycles(nodes, edges) {
|
|
186
|
+
const errors = [];
|
|
187
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const node of nodes) {
|
|
189
|
+
adjacency.set(node.id, []);
|
|
190
|
+
}
|
|
191
|
+
for (const edge of edges) {
|
|
192
|
+
adjacency.get(edge.sourceNodeId)?.push(edge.targetNodeId);
|
|
193
|
+
}
|
|
194
|
+
const WHITE = 0;
|
|
195
|
+
const GRAY = 1;
|
|
196
|
+
const BLACK = 2;
|
|
197
|
+
const color = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const node of nodes) {
|
|
199
|
+
color.set(node.id, WHITE);
|
|
200
|
+
}
|
|
201
|
+
function dfs(nodeId) {
|
|
202
|
+
color.set(nodeId, GRAY);
|
|
203
|
+
for (const neighbor of adjacency.get(nodeId) ?? []) {
|
|
204
|
+
if (color.get(neighbor) === GRAY) {
|
|
205
|
+
errors.push({
|
|
206
|
+
code: "CYCLE_DETECTED",
|
|
207
|
+
message: `Cycle detected: node "${nodeId}" \u2192 "${neighbor}"`,
|
|
208
|
+
nodeId
|
|
209
|
+
});
|
|
210
|
+
} else if (color.get(neighbor) === WHITE) {
|
|
211
|
+
dfs(neighbor);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
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
|
+
}
|
|
221
|
+
return errors;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/lib/ir/topological-sort.ts
|
|
225
|
+
function buildGraph(nodes, edges) {
|
|
226
|
+
const indegree = /* @__PURE__ */ new Map();
|
|
227
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
228
|
+
const reverseAdjacency = /* @__PURE__ */ new Map();
|
|
229
|
+
for (const node of nodes) {
|
|
230
|
+
indegree.set(node.id, 0);
|
|
231
|
+
adjacency.set(node.id, /* @__PURE__ */ new Set());
|
|
232
|
+
reverseAdjacency.set(node.id, /* @__PURE__ */ new Set());
|
|
233
|
+
}
|
|
234
|
+
for (const edge of edges) {
|
|
235
|
+
adjacency.get(edge.sourceNodeId).add(edge.targetNodeId);
|
|
236
|
+
reverseAdjacency.get(edge.targetNodeId).add(edge.sourceNodeId);
|
|
237
|
+
indegree.set(
|
|
238
|
+
edge.targetNodeId,
|
|
239
|
+
(indegree.get(edge.targetNodeId) ?? 0) + 1
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return { indegree, adjacency, reverseAdjacency };
|
|
243
|
+
}
|
|
244
|
+
function topologicalSort(ir) {
|
|
245
|
+
const { nodes, edges } = ir;
|
|
246
|
+
const { indegree, adjacency, reverseAdjacency } = buildGraph(nodes, edges);
|
|
247
|
+
const indegreeCopy = new Map(indegree);
|
|
248
|
+
const sortedNodeIds = [];
|
|
249
|
+
const steps = [];
|
|
250
|
+
let currentLevel = [];
|
|
251
|
+
for (const [nodeId, degree] of indegreeCopy) {
|
|
252
|
+
if (degree === 0) {
|
|
253
|
+
currentLevel.push(nodeId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
let stepIndex = 0;
|
|
257
|
+
while (currentLevel.length > 0) {
|
|
258
|
+
steps.push({
|
|
259
|
+
index: stepIndex,
|
|
260
|
+
nodeIds: [...currentLevel],
|
|
261
|
+
concurrent: currentLevel.length > 1
|
|
262
|
+
});
|
|
263
|
+
sortedNodeIds.push(...currentLevel);
|
|
264
|
+
const nextLevel = [];
|
|
265
|
+
for (const nodeId of currentLevel) {
|
|
266
|
+
for (const neighbor of adjacency.get(nodeId) ?? []) {
|
|
267
|
+
const newDegree = (indegreeCopy.get(neighbor) ?? 1) - 1;
|
|
268
|
+
indegreeCopy.set(neighbor, newDegree);
|
|
269
|
+
if (newDegree === 0) {
|
|
270
|
+
nextLevel.push(neighbor);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
currentLevel = nextLevel;
|
|
275
|
+
stepIndex++;
|
|
276
|
+
}
|
|
277
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
278
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const node of nodes) {
|
|
280
|
+
dependencies.set(node.id, reverseAdjacency.get(node.id) ?? /* @__PURE__ */ new Set());
|
|
281
|
+
dependents.set(node.id, adjacency.get(node.id) ?? /* @__PURE__ */ new Set());
|
|
282
|
+
}
|
|
283
|
+
if (sortedNodeIds.length !== nodes.length) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Topological sort failed: cycle detected in graph. Sorted ${sortedNodeIds.length}/${nodes.length} nodes.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
steps,
|
|
290
|
+
sortedNodeIds,
|
|
291
|
+
dependencies,
|
|
292
|
+
dependents
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/lib/compiler/expression-parser.ts
|
|
297
|
+
function parseExpression(expr, context) {
|
|
298
|
+
const tokens = tokenize(expr);
|
|
299
|
+
return tokens.map((token) => {
|
|
300
|
+
if (token.type === "literal") return token.value;
|
|
301
|
+
return resolveReference(parseReference(token.value), context);
|
|
302
|
+
}).join("");
|
|
303
|
+
}
|
|
304
|
+
function tokenize(input) {
|
|
305
|
+
const tokens = [];
|
|
306
|
+
let i = 0;
|
|
307
|
+
let literalBuf = "";
|
|
308
|
+
while (i < input.length) {
|
|
309
|
+
if (input[i] === "\\" && input[i + 1] === "{" && input[i + 2] === "{") {
|
|
310
|
+
literalBuf += "{{";
|
|
311
|
+
i += 3;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (input[i] === "{" && input[i + 1] === "{") {
|
|
315
|
+
if (literalBuf) {
|
|
316
|
+
tokens.push({ type: "literal", value: literalBuf });
|
|
317
|
+
literalBuf = "";
|
|
318
|
+
}
|
|
319
|
+
i += 2;
|
|
320
|
+
const refStart = i;
|
|
321
|
+
let bracketDepth = 0;
|
|
322
|
+
while (i < input.length) {
|
|
323
|
+
if (input[i] === "[") {
|
|
324
|
+
bracketDepth++;
|
|
325
|
+
i++;
|
|
326
|
+
} else if (input[i] === "]") {
|
|
327
|
+
bracketDepth--;
|
|
328
|
+
i++;
|
|
329
|
+
} else if (input[i] === "}" && input[i + 1] === "}" && bracketDepth === 0) {
|
|
330
|
+
break;
|
|
331
|
+
} else {
|
|
332
|
+
i++;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (i >= input.length) {
|
|
336
|
+
throw new ExpressionParseError(
|
|
337
|
+
`Unclosed template expression: missing matching '}}' (at position ${refStart - 2})`,
|
|
338
|
+
input,
|
|
339
|
+
refStart - 2
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const refContent = input.slice(refStart, i).trim();
|
|
343
|
+
if (!refContent) {
|
|
344
|
+
throw new ExpressionParseError(
|
|
345
|
+
"Empty template expression {{}}",
|
|
346
|
+
input,
|
|
347
|
+
refStart - 2
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
tokens.push({ type: "reference", value: refContent });
|
|
351
|
+
i += 2;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
literalBuf += input[i];
|
|
355
|
+
i++;
|
|
356
|
+
}
|
|
357
|
+
if (literalBuf) {
|
|
358
|
+
tokens.push({ type: "literal", value: literalBuf });
|
|
359
|
+
}
|
|
360
|
+
return tokens;
|
|
361
|
+
}
|
|
362
|
+
function parseReference(ref) {
|
|
363
|
+
const match = ref.match(/^(\$?\w+)((?:\.[\w]+|\[.+?\])*)$/);
|
|
364
|
+
if (!match) {
|
|
365
|
+
const dotIndex = ref.indexOf(".");
|
|
366
|
+
const bracketIndex = ref.indexOf("[");
|
|
367
|
+
let splitAt = -1;
|
|
368
|
+
if (dotIndex !== -1 && bracketIndex !== -1) {
|
|
369
|
+
splitAt = Math.min(dotIndex, bracketIndex);
|
|
370
|
+
} else if (dotIndex !== -1) {
|
|
371
|
+
splitAt = dotIndex;
|
|
372
|
+
} else if (bracketIndex !== -1) {
|
|
373
|
+
splitAt = bracketIndex;
|
|
374
|
+
}
|
|
375
|
+
if (splitAt > 0) {
|
|
376
|
+
return {
|
|
377
|
+
base: ref.slice(0, splitAt),
|
|
378
|
+
path: ref.slice(splitAt)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return { base: ref, path: "" };
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
base: match[1],
|
|
385
|
+
path: match[2] || ""
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function resolveReference(ref, context) {
|
|
389
|
+
const { base, path } = ref;
|
|
390
|
+
if (base === "$input") {
|
|
391
|
+
return resolveInputRef(path, context);
|
|
392
|
+
}
|
|
393
|
+
if (base === "$trigger") {
|
|
394
|
+
return resolveTriggerRef(path, context);
|
|
395
|
+
}
|
|
396
|
+
if (context.scopeStack) {
|
|
397
|
+
for (let i = context.scopeStack.length - 1; i >= 0; i--) {
|
|
398
|
+
const scope = context.scopeStack[i];
|
|
399
|
+
if (scope.nodeId === base) {
|
|
400
|
+
return `${scope.scopeVar}['${base}']${path}`;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (context.blockScopedNodeIds?.has(base)) {
|
|
405
|
+
return `flowState['${base}']${path}`;
|
|
406
|
+
}
|
|
407
|
+
if (context.symbolTable?.hasVar(base)) {
|
|
408
|
+
return `${context.symbolTable.getVarName(base)}${path}`;
|
|
409
|
+
}
|
|
410
|
+
return `flowState['${base}']${path}`;
|
|
411
|
+
}
|
|
412
|
+
function resolveInputRef(path, context) {
|
|
413
|
+
if (!context.currentNodeId) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Expression parser error: No current node context for $input reference`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const incoming = context.ir.edges.filter(
|
|
419
|
+
(e) => e.targetNodeId === context.currentNodeId
|
|
420
|
+
);
|
|
421
|
+
const dataSource = incoming.find((e) => {
|
|
422
|
+
const src = context.nodeMap.get(e.sourceNodeId);
|
|
423
|
+
return src && src.category !== "trigger" /* TRIGGER */;
|
|
424
|
+
}) || incoming[0];
|
|
425
|
+
if (dataSource) {
|
|
426
|
+
const srcId = dataSource.sourceNodeId;
|
|
427
|
+
if (context.blockScopedNodeIds?.has(srcId)) {
|
|
428
|
+
return `flowState['${srcId}']${path}`;
|
|
429
|
+
}
|
|
430
|
+
if (context.symbolTable?.hasVar(srcId)) {
|
|
431
|
+
return `${context.symbolTable.getVarName(srcId)}${path}`;
|
|
432
|
+
}
|
|
433
|
+
return `flowState['${srcId}']${path}`;
|
|
434
|
+
}
|
|
435
|
+
throw new Error(
|
|
436
|
+
`Expression parser error: Node "${context.currentNodeId}" has no input connected`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
function resolveTriggerRef(path, context) {
|
|
440
|
+
const trigger = context.ir.nodes.find(
|
|
441
|
+
(n) => n.category === "trigger" /* TRIGGER */
|
|
442
|
+
);
|
|
443
|
+
if (trigger) {
|
|
444
|
+
if (context.symbolTable?.hasVar(trigger.id)) {
|
|
445
|
+
return `${context.symbolTable.getVarName(trigger.id)}${path}`;
|
|
446
|
+
}
|
|
447
|
+
return `flowState['${trigger.id}']${path}`;
|
|
448
|
+
}
|
|
449
|
+
return "undefined";
|
|
450
|
+
}
|
|
451
|
+
var ExpressionParseError = class extends Error {
|
|
452
|
+
constructor(message, expression, position) {
|
|
453
|
+
super(message);
|
|
454
|
+
this.expression = expression;
|
|
455
|
+
this.position = position;
|
|
456
|
+
this.name = "ExpressionParseError";
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// src/lib/compiler/platforms/types.ts
|
|
461
|
+
var platformRegistry = /* @__PURE__ */ new Map();
|
|
462
|
+
function registerPlatform(name, factory) {
|
|
463
|
+
platformRegistry.set(name, factory);
|
|
464
|
+
}
|
|
465
|
+
function getPlatform(name) {
|
|
466
|
+
const factory = platformRegistry.get(name);
|
|
467
|
+
if (!factory) {
|
|
468
|
+
throw new Error(
|
|
469
|
+
`Unknown platform "${name}". Available platforms: ${[...platformRegistry.keys()].join(", ")}`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return factory();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/lib/compiler/platforms/nextjs.ts
|
|
476
|
+
var NextjsPlatform = class {
|
|
477
|
+
name = "nextjs";
|
|
478
|
+
generateImports(sourceFile, trigger, _context) {
|
|
479
|
+
if (trigger.nodeType !== "http_webhook" /* HTTP_WEBHOOK */) return;
|
|
480
|
+
const params = trigger.params;
|
|
481
|
+
const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
|
|
482
|
+
sourceFile.addImportDeclaration({
|
|
483
|
+
namedImports: isGetOrDelete ? ["NextRequest", "NextResponse"] : ["NextResponse"],
|
|
484
|
+
moduleSpecifier: "next/server"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
generateFunction(sourceFile, trigger, _context, bodyGenerator) {
|
|
488
|
+
switch (trigger.nodeType) {
|
|
489
|
+
case "http_webhook" /* HTTP_WEBHOOK */:
|
|
490
|
+
this.generateHttpFunction(sourceFile, trigger, bodyGenerator);
|
|
491
|
+
break;
|
|
492
|
+
case "cron_job" /* CRON_JOB */:
|
|
493
|
+
this.generateCronFunction(sourceFile, trigger, bodyGenerator);
|
|
494
|
+
break;
|
|
495
|
+
case "manual" /* MANUAL */:
|
|
496
|
+
this.generateManualFunction(sourceFile, trigger, bodyGenerator);
|
|
497
|
+
break;
|
|
498
|
+
default:
|
|
499
|
+
throw new Error(`Unsupported trigger type: ${trigger.nodeType}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
generateResponse(writer, bodyExpr, statusCode, headers) {
|
|
503
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
504
|
+
writer.writeLine(
|
|
505
|
+
`throw new EarlyResponse(NextResponse.json(${bodyExpr}, { status: ${statusCode}, headers: ${JSON.stringify(headers)} }));`
|
|
506
|
+
);
|
|
507
|
+
} else {
|
|
508
|
+
writer.writeLine(
|
|
509
|
+
`throw new EarlyResponse(NextResponse.json(${bodyExpr}, { status: ${statusCode} }));`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
generateErrorResponse(writer) {
|
|
514
|
+
writer.write("if (error instanceof EarlyResponse) ").block(() => {
|
|
515
|
+
writer.writeLine("return error.response;");
|
|
516
|
+
});
|
|
517
|
+
writer.writeLine('console.error("Workflow failed:", error);');
|
|
518
|
+
writer.writeLine(
|
|
519
|
+
'return NextResponse.json({ error: error instanceof Error ? error.message : "Internal Server Error" }, { status: 500 });'
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
getOutputFilePath(trigger) {
|
|
523
|
+
if (trigger.nodeType === "http_webhook" /* HTTP_WEBHOOK */) {
|
|
524
|
+
const params = trigger.params;
|
|
525
|
+
const routePath = params.routePath.replace(/^\//, "");
|
|
526
|
+
return `src/app/${routePath}/route.ts`;
|
|
527
|
+
}
|
|
528
|
+
if (trigger.nodeType === "cron_job" /* CRON_JOB */) {
|
|
529
|
+
const params = trigger.params;
|
|
530
|
+
return `src/lib/cron/${params.functionName}.ts`;
|
|
531
|
+
}
|
|
532
|
+
if (trigger.nodeType === "manual" /* MANUAL */) {
|
|
533
|
+
const params = trigger.params;
|
|
534
|
+
return `src/lib/functions/${params.functionName}.ts`;
|
|
535
|
+
}
|
|
536
|
+
return "src/generated/flow.ts";
|
|
537
|
+
}
|
|
538
|
+
getImplicitDependencies() {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
generateTriggerInit(writer, trigger, context) {
|
|
542
|
+
const varName = context.symbolTable.getVarName(trigger.id);
|
|
543
|
+
switch (trigger.nodeType) {
|
|
544
|
+
case "http_webhook" /* HTTP_WEBHOOK */: {
|
|
545
|
+
const params = trigger.params;
|
|
546
|
+
const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
|
|
547
|
+
if (isGetOrDelete) {
|
|
548
|
+
writer.writeLine("const searchParams = req.nextUrl.searchParams;");
|
|
549
|
+
writer.writeLine(
|
|
550
|
+
"const query = Object.fromEntries(searchParams.entries());"
|
|
551
|
+
);
|
|
552
|
+
writer.writeLine(`const ${varName} = { query, url: req.url };`);
|
|
553
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
554
|
+
} else if (params.parseBody && ["POST", "PUT", "PATCH"].includes(params.method)) {
|
|
555
|
+
writer.writeLine("let body: unknown;");
|
|
556
|
+
writer.write("try ").block(() => {
|
|
557
|
+
writer.writeLine("body = await req.json();");
|
|
558
|
+
});
|
|
559
|
+
writer.write(" catch ").block(() => {
|
|
560
|
+
writer.writeLine(
|
|
561
|
+
'return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });'
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
writer.writeLine(`const ${varName} = { body, url: req.url };`);
|
|
565
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
566
|
+
} else {
|
|
567
|
+
writer.writeLine(`const ${varName} = { url: req.url };`);
|
|
568
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case "cron_job" /* CRON_JOB */: {
|
|
573
|
+
writer.writeLine(
|
|
574
|
+
`const ${varName} = { triggeredAt: new Date().toISOString() };`
|
|
575
|
+
);
|
|
576
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
case "manual" /* MANUAL */: {
|
|
580
|
+
const params = trigger.params;
|
|
581
|
+
if (params.args.length > 0) {
|
|
582
|
+
const argsObj = params.args.map((a) => a.name).join(", ");
|
|
583
|
+
writer.writeLine(`const ${varName} = { ${argsObj} };`);
|
|
584
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
585
|
+
}
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// ── Private ──
|
|
591
|
+
generateHttpFunction(sourceFile, trigger, bodyGenerator) {
|
|
592
|
+
const params = trigger.params;
|
|
593
|
+
const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
|
|
594
|
+
const funcDecl = sourceFile.addFunction({
|
|
595
|
+
name: params.method,
|
|
596
|
+
isAsync: true,
|
|
597
|
+
isExported: true,
|
|
598
|
+
parameters: [
|
|
599
|
+
{ name: "req", type: isGetOrDelete ? "NextRequest" : "Request" }
|
|
600
|
+
]
|
|
601
|
+
});
|
|
602
|
+
sourceFile.insertStatements(funcDecl.getChildIndex(), (writer) => {
|
|
603
|
+
writer.writeLine("class EarlyResponse extends Error {");
|
|
604
|
+
writer.writeLine(" constructor(public response: Response) { super(); }");
|
|
605
|
+
writer.writeLine("}");
|
|
606
|
+
writer.blankLine();
|
|
607
|
+
});
|
|
608
|
+
funcDecl.addStatements((writer) => {
|
|
609
|
+
writer.write("try ").block(() => {
|
|
610
|
+
bodyGenerator(writer);
|
|
611
|
+
});
|
|
612
|
+
writer.write(" catch (error) ").block(() => {
|
|
613
|
+
this.generateErrorResponse(writer);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
generateCronFunction(sourceFile, trigger, bodyGenerator) {
|
|
618
|
+
const params = trigger.params;
|
|
619
|
+
sourceFile.addStatements(`// @schedule ${params.schedule}`);
|
|
620
|
+
const funcDecl = sourceFile.addFunction({
|
|
621
|
+
name: params.functionName,
|
|
622
|
+
isAsync: true,
|
|
623
|
+
isExported: true
|
|
624
|
+
});
|
|
625
|
+
funcDecl.addStatements((writer) => {
|
|
626
|
+
bodyGenerator(writer);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
generateManualFunction(sourceFile, trigger, bodyGenerator) {
|
|
630
|
+
const params = trigger.params;
|
|
631
|
+
const funcDecl = sourceFile.addFunction({
|
|
632
|
+
name: params.functionName,
|
|
633
|
+
isAsync: true,
|
|
634
|
+
isExported: true,
|
|
635
|
+
parameters: params.args.map((arg) => ({
|
|
636
|
+
name: arg.name,
|
|
637
|
+
type: arg.type
|
|
638
|
+
}))
|
|
639
|
+
});
|
|
640
|
+
funcDecl.addStatements((writer) => {
|
|
641
|
+
bodyGenerator(writer);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
// src/lib/compiler/platforms/express.ts
|
|
647
|
+
var ExpressPlatform = class {
|
|
648
|
+
name = "express";
|
|
649
|
+
generateImports(sourceFile, trigger, _context) {
|
|
650
|
+
if (trigger.nodeType === "http_webhook" /* HTTP_WEBHOOK */) {
|
|
651
|
+
sourceFile.addImportDeclaration({
|
|
652
|
+
namedImports: ["Request", "Response"],
|
|
653
|
+
moduleSpecifier: "express"
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
generateFunction(sourceFile, trigger, _context, bodyGenerator) {
|
|
658
|
+
switch (trigger.nodeType) {
|
|
659
|
+
case "http_webhook" /* HTTP_WEBHOOK */:
|
|
660
|
+
this.generateHttpHandler(sourceFile, trigger, bodyGenerator);
|
|
661
|
+
break;
|
|
662
|
+
case "cron_job" /* CRON_JOB */:
|
|
663
|
+
this.generateCronFunction(sourceFile, trigger, bodyGenerator);
|
|
664
|
+
break;
|
|
665
|
+
case "manual" /* MANUAL */:
|
|
666
|
+
this.generateManualFunction(sourceFile, trigger, bodyGenerator);
|
|
667
|
+
break;
|
|
668
|
+
default:
|
|
669
|
+
throw new Error(`Unsupported trigger type: ${trigger.nodeType}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
generateResponse(writer, bodyExpr, statusCode, headers) {
|
|
673
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
674
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
675
|
+
writer.writeLine(`res.setHeader(${JSON.stringify(key)}, ${JSON.stringify(value)});`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
writer.writeLine(`throw new EarlyResponse(() => res.status(${statusCode}).json(${bodyExpr}));`);
|
|
679
|
+
}
|
|
680
|
+
generateErrorResponse(writer) {
|
|
681
|
+
writer.write("if (error instanceof EarlyResponse) ").block(() => {
|
|
682
|
+
writer.writeLine("return error.send();");
|
|
683
|
+
});
|
|
684
|
+
writer.writeLine('console.error("Workflow failed:", error);');
|
|
685
|
+
writer.writeLine(
|
|
686
|
+
'return res.status(500).json({ error: error instanceof Error ? error.message : "Internal Server Error" });'
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
getOutputFilePath(trigger) {
|
|
690
|
+
if (trigger.nodeType === "http_webhook" /* HTTP_WEBHOOK */) {
|
|
691
|
+
const params = trigger.params;
|
|
692
|
+
const routePath = params.routePath.replace(/^\//, "").replace(/\//g, "-");
|
|
693
|
+
return `src/routes/${routePath}.ts`;
|
|
694
|
+
}
|
|
695
|
+
if (trigger.nodeType === "cron_job" /* CRON_JOB */) {
|
|
696
|
+
const params = trigger.params;
|
|
697
|
+
return `src/cron/${params.functionName}.ts`;
|
|
698
|
+
}
|
|
699
|
+
if (trigger.nodeType === "manual" /* MANUAL */) {
|
|
700
|
+
const params = trigger.params;
|
|
701
|
+
return `src/functions/${params.functionName}.ts`;
|
|
702
|
+
}
|
|
703
|
+
return "src/generated/flow.ts";
|
|
704
|
+
}
|
|
705
|
+
getImplicitDependencies() {
|
|
706
|
+
return ["express", "@types/express"];
|
|
707
|
+
}
|
|
708
|
+
generateTriggerInit(writer, trigger, context) {
|
|
709
|
+
const varName = context.symbolTable.getVarName(trigger.id);
|
|
710
|
+
switch (trigger.nodeType) {
|
|
711
|
+
case "http_webhook" /* HTTP_WEBHOOK */: {
|
|
712
|
+
const params = trigger.params;
|
|
713
|
+
const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
|
|
714
|
+
if (isGetOrDelete) {
|
|
715
|
+
writer.writeLine(
|
|
716
|
+
"const query = req.query as Record<string, string>;"
|
|
717
|
+
);
|
|
718
|
+
writer.writeLine(
|
|
719
|
+
`const ${varName} = { query, url: req.originalUrl };`
|
|
720
|
+
);
|
|
721
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
722
|
+
} else if (params.parseBody && ["POST", "PUT", "PATCH"].includes(params.method)) {
|
|
723
|
+
writer.writeLine("const body = req.body;");
|
|
724
|
+
writer.writeLine(
|
|
725
|
+
`const ${varName} = { body, url: req.originalUrl };`
|
|
726
|
+
);
|
|
727
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
728
|
+
} else {
|
|
729
|
+
writer.writeLine(
|
|
730
|
+
`const ${varName} = { url: req.originalUrl };`
|
|
731
|
+
);
|
|
732
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case "cron_job" /* CRON_JOB */: {
|
|
737
|
+
writer.writeLine(
|
|
738
|
+
`const ${varName} = { triggeredAt: new Date().toISOString() };`
|
|
739
|
+
);
|
|
740
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
case "manual" /* MANUAL */: {
|
|
744
|
+
const params = trigger.params;
|
|
745
|
+
if (params.args.length > 0) {
|
|
746
|
+
const argsObj = params.args.map((a) => a.name).join(", ");
|
|
747
|
+
writer.writeLine(`const ${varName} = { ${argsObj} };`);
|
|
748
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
749
|
+
}
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// ── Private ──
|
|
755
|
+
generateHttpHandler(sourceFile, trigger, bodyGenerator) {
|
|
756
|
+
const params = trigger.params;
|
|
757
|
+
const funcDecl = sourceFile.addFunction({
|
|
758
|
+
name: `handle${params.method.charAt(0)}${params.method.slice(1).toLowerCase()}`,
|
|
759
|
+
isAsync: true,
|
|
760
|
+
isExported: true,
|
|
761
|
+
parameters: [
|
|
762
|
+
{ name: "req", type: "Request" },
|
|
763
|
+
{ name: "res", type: "Response" }
|
|
764
|
+
]
|
|
765
|
+
});
|
|
766
|
+
sourceFile.insertStatements(funcDecl.getChildIndex(), (writer) => {
|
|
767
|
+
writer.writeLine("class EarlyResponse extends Error {");
|
|
768
|
+
writer.writeLine(" constructor(public send: () => void) { super(); }");
|
|
769
|
+
writer.writeLine("}");
|
|
770
|
+
writer.blankLine();
|
|
771
|
+
});
|
|
772
|
+
funcDecl.addStatements((writer) => {
|
|
773
|
+
writer.write("try ").block(() => {
|
|
774
|
+
bodyGenerator(writer);
|
|
775
|
+
});
|
|
776
|
+
writer.write(" catch (error) ").block(() => {
|
|
777
|
+
this.generateErrorResponse(writer);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
generateCronFunction(sourceFile, trigger, bodyGenerator) {
|
|
782
|
+
const params = trigger.params;
|
|
783
|
+
sourceFile.addStatements(`// @schedule ${params.schedule}`);
|
|
784
|
+
const funcDecl = sourceFile.addFunction({
|
|
785
|
+
name: params.functionName,
|
|
786
|
+
isAsync: true,
|
|
787
|
+
isExported: true
|
|
788
|
+
});
|
|
789
|
+
funcDecl.addStatements((writer) => {
|
|
790
|
+
bodyGenerator(writer);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
generateManualFunction(sourceFile, trigger, bodyGenerator) {
|
|
794
|
+
const params = trigger.params;
|
|
795
|
+
const funcDecl = sourceFile.addFunction({
|
|
796
|
+
name: params.functionName,
|
|
797
|
+
isAsync: true,
|
|
798
|
+
isExported: true,
|
|
799
|
+
parameters: params.args.map((arg) => ({
|
|
800
|
+
name: arg.name,
|
|
801
|
+
type: arg.type
|
|
802
|
+
}))
|
|
803
|
+
});
|
|
804
|
+
funcDecl.addStatements((writer) => {
|
|
805
|
+
bodyGenerator(writer);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/lib/compiler/platforms/cloudflare.ts
|
|
811
|
+
var CloudflarePlatform = class {
|
|
812
|
+
name = "cloudflare";
|
|
813
|
+
generateImports(_sourceFile, _trigger, _context) {
|
|
814
|
+
}
|
|
815
|
+
generateFunction(sourceFile, trigger, _context, bodyGenerator) {
|
|
816
|
+
switch (trigger.nodeType) {
|
|
817
|
+
case "http_webhook" /* HTTP_WEBHOOK */:
|
|
818
|
+
this.generateFetchHandler(sourceFile, trigger, bodyGenerator);
|
|
819
|
+
break;
|
|
820
|
+
case "cron_job" /* CRON_JOB */:
|
|
821
|
+
this.generateScheduledHandler(sourceFile, trigger, bodyGenerator);
|
|
822
|
+
break;
|
|
823
|
+
case "manual" /* MANUAL */:
|
|
824
|
+
this.generateManualFunction(sourceFile, trigger, bodyGenerator);
|
|
825
|
+
break;
|
|
826
|
+
default:
|
|
827
|
+
throw new Error(`Unsupported trigger type: ${trigger.nodeType}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
generateResponse(writer, bodyExpr, statusCode, headers) {
|
|
831
|
+
const headersObj = headers && Object.keys(headers).length > 0 ? `, { status: ${statusCode}, headers: ${JSON.stringify({ "Content-Type": "application/json", ...headers })} }` : `, { status: ${statusCode}, headers: { "Content-Type": "application/json" } }`;
|
|
832
|
+
writer.writeLine(
|
|
833
|
+
`throw new EarlyResponse(new Response(JSON.stringify(${bodyExpr})${headersObj}));`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
generateErrorResponse(writer) {
|
|
837
|
+
writer.write("if (error instanceof EarlyResponse) ").block(() => {
|
|
838
|
+
writer.writeLine("return error.response;");
|
|
839
|
+
});
|
|
840
|
+
writer.writeLine('console.error("Workflow failed:", error);');
|
|
841
|
+
writer.writeLine(
|
|
842
|
+
`return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal Server Error" }), { status: 500, headers: { "Content-Type": "application/json" } });`
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
getOutputFilePath(trigger) {
|
|
846
|
+
if (trigger.nodeType === "http_webhook" /* HTTP_WEBHOOK */) {
|
|
847
|
+
return "src/worker.ts";
|
|
848
|
+
}
|
|
849
|
+
if (trigger.nodeType === "cron_job" /* CRON_JOB */) {
|
|
850
|
+
const params = trigger.params;
|
|
851
|
+
return `src/scheduled/${params.functionName}.ts`;
|
|
852
|
+
}
|
|
853
|
+
if (trigger.nodeType === "manual" /* MANUAL */) {
|
|
854
|
+
const params = trigger.params;
|
|
855
|
+
return `src/functions/${params.functionName}.ts`;
|
|
856
|
+
}
|
|
857
|
+
return "src/generated/flow.ts";
|
|
858
|
+
}
|
|
859
|
+
getImplicitDependencies() {
|
|
860
|
+
return ["@cloudflare/workers-types"];
|
|
861
|
+
}
|
|
862
|
+
generateTriggerInit(writer, trigger, context) {
|
|
863
|
+
const varName = context.symbolTable.getVarName(trigger.id);
|
|
864
|
+
switch (trigger.nodeType) {
|
|
865
|
+
case "http_webhook" /* HTTP_WEBHOOK */: {
|
|
866
|
+
const params = trigger.params;
|
|
867
|
+
const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
|
|
868
|
+
if (isGetOrDelete) {
|
|
869
|
+
writer.writeLine(
|
|
870
|
+
"const url = new URL(request.url);"
|
|
871
|
+
);
|
|
872
|
+
writer.writeLine(
|
|
873
|
+
"const query = Object.fromEntries(url.searchParams.entries());"
|
|
874
|
+
);
|
|
875
|
+
writer.writeLine(
|
|
876
|
+
`const ${varName} = { query, url: request.url };`
|
|
877
|
+
);
|
|
878
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
879
|
+
} else if (params.parseBody && ["POST", "PUT", "PATCH"].includes(params.method)) {
|
|
880
|
+
writer.writeLine("let body: unknown;");
|
|
881
|
+
writer.write("try ").block(() => {
|
|
882
|
+
writer.writeLine("body = await request.json();");
|
|
883
|
+
});
|
|
884
|
+
writer.write(" catch ").block(() => {
|
|
885
|
+
writer.writeLine(
|
|
886
|
+
'return new Response(JSON.stringify({ error: "Invalid JSON body" }), { status: 400, headers: { "Content-Type": "application/json" } });'
|
|
887
|
+
);
|
|
888
|
+
});
|
|
889
|
+
writer.writeLine(
|
|
890
|
+
`const ${varName} = { body, url: request.url };`
|
|
891
|
+
);
|
|
892
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
893
|
+
} else {
|
|
894
|
+
writer.writeLine(
|
|
895
|
+
`const ${varName} = { url: request.url };`
|
|
896
|
+
);
|
|
897
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
898
|
+
}
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
case "cron_job" /* CRON_JOB */: {
|
|
902
|
+
writer.writeLine(
|
|
903
|
+
`const ${varName} = { triggeredAt: new Date().toISOString() };`
|
|
904
|
+
);
|
|
905
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
case "manual" /* MANUAL */: {
|
|
909
|
+
const params = trigger.params;
|
|
910
|
+
if (params.args.length > 0) {
|
|
911
|
+
const argsObj = params.args.map((a) => a.name).join(", ");
|
|
912
|
+
writer.writeLine(`const ${varName} = { ${argsObj} };`);
|
|
913
|
+
writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
|
|
914
|
+
}
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
// ── Private ──
|
|
920
|
+
generateFetchHandler(sourceFile, trigger, bodyGenerator) {
|
|
921
|
+
const params = trigger.params;
|
|
922
|
+
sourceFile.addStatements(`// Cloudflare Workers handler`);
|
|
923
|
+
const funcDecl = sourceFile.addFunction({
|
|
924
|
+
name: "handleRequest",
|
|
925
|
+
isAsync: true,
|
|
926
|
+
isExported: true,
|
|
927
|
+
parameters: [
|
|
928
|
+
{ name: "request", type: "Request" },
|
|
929
|
+
{ name: "env", type: "Env" },
|
|
930
|
+
{ name: "ctx", type: "ExecutionContext" }
|
|
931
|
+
]
|
|
932
|
+
});
|
|
933
|
+
sourceFile.insertStatements(funcDecl.getChildIndex(), (writer) => {
|
|
934
|
+
writer.writeLine("class EarlyResponse extends Error {");
|
|
935
|
+
writer.writeLine(" constructor(public response: Response) { super(); }");
|
|
936
|
+
writer.writeLine("}");
|
|
937
|
+
writer.blankLine();
|
|
938
|
+
});
|
|
939
|
+
funcDecl.addStatements((writer) => {
|
|
940
|
+
writer.write("try ").block(() => {
|
|
941
|
+
bodyGenerator(writer);
|
|
942
|
+
});
|
|
943
|
+
writer.write(" catch (error) ").block(() => {
|
|
944
|
+
this.generateErrorResponse(writer);
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
sourceFile.addStatements(`
|
|
948
|
+
export default {
|
|
949
|
+
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
950
|
+
return handleRequest(request, env, ctx);
|
|
951
|
+
},
|
|
952
|
+
};`);
|
|
953
|
+
}
|
|
954
|
+
generateScheduledHandler(sourceFile, trigger, bodyGenerator) {
|
|
955
|
+
const params = trigger.params;
|
|
956
|
+
sourceFile.addStatements(`// @schedule ${params.schedule}`);
|
|
957
|
+
const funcDecl = sourceFile.addFunction({
|
|
958
|
+
name: params.functionName,
|
|
959
|
+
isAsync: true,
|
|
960
|
+
isExported: true
|
|
961
|
+
});
|
|
962
|
+
funcDecl.addStatements((writer) => {
|
|
963
|
+
bodyGenerator(writer);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
generateManualFunction(sourceFile, trigger, bodyGenerator) {
|
|
967
|
+
const params = trigger.params;
|
|
968
|
+
const funcDecl = sourceFile.addFunction({
|
|
969
|
+
name: params.functionName,
|
|
970
|
+
isAsync: true,
|
|
971
|
+
isExported: true,
|
|
972
|
+
parameters: params.args.map((arg) => ({
|
|
973
|
+
name: arg.name,
|
|
974
|
+
type: arg.type
|
|
975
|
+
}))
|
|
976
|
+
});
|
|
977
|
+
funcDecl.addStatements((writer) => {
|
|
978
|
+
bodyGenerator(writer);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// src/lib/compiler/platforms/index.ts
|
|
984
|
+
registerPlatform("nextjs", () => new NextjsPlatform());
|
|
985
|
+
registerPlatform("express", () => new ExpressPlatform());
|
|
986
|
+
registerPlatform("cloudflare", () => new CloudflarePlatform());
|
|
987
|
+
|
|
988
|
+
// src/lib/compiler/plugins/types.ts
|
|
989
|
+
function createPluginRegistry() {
|
|
990
|
+
const map = /* @__PURE__ */ new Map();
|
|
991
|
+
return {
|
|
992
|
+
register(plugin) {
|
|
993
|
+
map.set(plugin.nodeType, plugin);
|
|
994
|
+
},
|
|
995
|
+
registerAll(plugins) {
|
|
996
|
+
for (const p of plugins) map.set(p.nodeType, p);
|
|
997
|
+
},
|
|
998
|
+
get(nodeType) {
|
|
999
|
+
return map.get(nodeType);
|
|
1000
|
+
},
|
|
1001
|
+
has(nodeType) {
|
|
1002
|
+
return map.has(nodeType);
|
|
1003
|
+
},
|
|
1004
|
+
getAll() {
|
|
1005
|
+
return new Map(map);
|
|
1006
|
+
},
|
|
1007
|
+
clear() {
|
|
1008
|
+
map.clear();
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
var globalRegistry = createPluginRegistry();
|
|
1013
|
+
function getPlugin(nodeType) {
|
|
1014
|
+
return globalRegistry.get(nodeType);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/lib/compiler/plugins/builtin.ts
|
|
1018
|
+
function inferTypeFromExpression(expr) {
|
|
1019
|
+
const trimmed = expr.trim();
|
|
1020
|
+
if (/\.map\s*\(/.test(trimmed)) return "unknown[]";
|
|
1021
|
+
if (/\.filter\s*\(/.test(trimmed)) return "unknown[]";
|
|
1022
|
+
if (/\.flatMap\s*\(/.test(trimmed)) return "unknown[]";
|
|
1023
|
+
if (/\.slice\s*\(/.test(trimmed)) return "unknown[]";
|
|
1024
|
+
if (/\.concat\s*\(/.test(trimmed)) return "unknown[]";
|
|
1025
|
+
if (/\.sort\s*\(/.test(trimmed)) return "unknown[]";
|
|
1026
|
+
if (/\.reverse\s*\(/.test(trimmed)) return "unknown[]";
|
|
1027
|
+
if (/Array\.from\s*\(/.test(trimmed)) return "unknown[]";
|
|
1028
|
+
if (/\.flat\s*\(/.test(trimmed)) return "unknown[]";
|
|
1029
|
+
if (/\.entries\s*\(/.test(trimmed)) return "[string, unknown][]";
|
|
1030
|
+
if (/\.reduce\s*\(/.test(trimmed)) return "unknown";
|
|
1031
|
+
if (/\.length\b/.test(trimmed)) return "number";
|
|
1032
|
+
if (/\.indexOf\s*\(/.test(trimmed)) return "number";
|
|
1033
|
+
if (/\.findIndex\s*\(/.test(trimmed)) return "number";
|
|
1034
|
+
if (/parseInt\s*\(/.test(trimmed)) return "number";
|
|
1035
|
+
if (/parseFloat\s*\(/.test(trimmed)) return "number";
|
|
1036
|
+
if (/Number\s*\(/.test(trimmed)) return "number";
|
|
1037
|
+
if (/Math\./.test(trimmed)) return "number";
|
|
1038
|
+
if (/\.includes\s*\(/.test(trimmed)) return "boolean";
|
|
1039
|
+
if (/\.some\s*\(/.test(trimmed)) return "boolean";
|
|
1040
|
+
if (/\.every\s*\(/.test(trimmed)) return "boolean";
|
|
1041
|
+
if (/\.has\s*\(/.test(trimmed)) return "boolean";
|
|
1042
|
+
if (/^!/.test(trimmed)) return "boolean";
|
|
1043
|
+
if (/===|!==|==|!=|>=|<=|>|<|&&|\|\|/.test(trimmed)) return "boolean";
|
|
1044
|
+
if (/\.join\s*\(/.test(trimmed)) return "string";
|
|
1045
|
+
if (/\.toString\s*\(/.test(trimmed)) return "string";
|
|
1046
|
+
if (/\.trim\s*\(/.test(trimmed)) return "string";
|
|
1047
|
+
if (/\.replace\s*\(/.test(trimmed)) return "string";
|
|
1048
|
+
if (/\.toLowerCase\s*\(/.test(trimmed)) return "string";
|
|
1049
|
+
if (/\.toUpperCase\s*\(/.test(trimmed)) return "string";
|
|
1050
|
+
if (/String\s*\(/.test(trimmed)) return "string";
|
|
1051
|
+
if (/JSON\.stringify\s*\(/.test(trimmed)) return "string";
|
|
1052
|
+
if (/`[^`]*`/.test(trimmed)) return "string";
|
|
1053
|
+
if (/JSON\.parse\s*\(/.test(trimmed)) return "unknown";
|
|
1054
|
+
if (/Object\.keys\s*\(/.test(trimmed)) return "string[]";
|
|
1055
|
+
if (/Object\.values\s*\(/.test(trimmed)) return "unknown[]";
|
|
1056
|
+
if (/Object\.entries\s*\(/.test(trimmed)) return "[string, unknown][]";
|
|
1057
|
+
if (/Object\.assign\s*\(/.test(trimmed)) return "Record<string, unknown>";
|
|
1058
|
+
if (/Object\.fromEntries\s*\(/.test(trimmed)) return "Record<string, unknown>";
|
|
1059
|
+
if (/^\{/.test(trimmed)) return "Record<string, unknown>";
|
|
1060
|
+
if (/^\[/.test(trimmed)) return "unknown[]";
|
|
1061
|
+
if (/\.\.\.\s*\{\{/.test(trimmed)) return "Record<string, unknown>";
|
|
1062
|
+
if (/\.find\s*\(/.test(trimmed)) return "unknown | undefined";
|
|
1063
|
+
return "unknown";
|
|
1064
|
+
}
|
|
1065
|
+
function inferTypeFromCode(code, returnVar) {
|
|
1066
|
+
const declMatch = code.match(
|
|
1067
|
+
new RegExp(`(?:const|let|var)\\s+${escapeRegex(returnVar)}\\s*(?::\\s*([^=]+?))?\\s*=\\s*(.+?)(?:;|$)`, "m")
|
|
1068
|
+
);
|
|
1069
|
+
if (declMatch) {
|
|
1070
|
+
const typeAnnotation = declMatch[1]?.trim();
|
|
1071
|
+
if (typeAnnotation) return typeAnnotation;
|
|
1072
|
+
const initializer = declMatch[2]?.trim();
|
|
1073
|
+
if (initializer) {
|
|
1074
|
+
if (/^\[/.test(initializer)) return "unknown[]";
|
|
1075
|
+
if (/^\{/.test(initializer)) return "Record<string, unknown>";
|
|
1076
|
+
if (/^["'`]/.test(initializer)) return "string";
|
|
1077
|
+
if (/^\d/.test(initializer)) return "number";
|
|
1078
|
+
if (/^(true|false)$/.test(initializer)) return "boolean";
|
|
1079
|
+
if (/^new Map/.test(initializer)) return "Map<unknown, unknown>";
|
|
1080
|
+
if (/^new Set/.test(initializer)) return "Set<unknown>";
|
|
1081
|
+
if (/\.map\s*\(/.test(initializer)) return "unknown[]";
|
|
1082
|
+
if (/\.filter\s*\(/.test(initializer)) return "unknown[]";
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return "unknown";
|
|
1086
|
+
}
|
|
1087
|
+
function escapeRegex(s) {
|
|
1088
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1089
|
+
}
|
|
1090
|
+
var httpWebhookPlugin = {
|
|
1091
|
+
nodeType: "http_webhook" /* HTTP_WEBHOOK */,
|
|
1092
|
+
generate: () => {
|
|
1093
|
+
},
|
|
1094
|
+
getOutputType(node) {
|
|
1095
|
+
const params = node.params;
|
|
1096
|
+
if (["GET", "DELETE"].includes(params.method)) {
|
|
1097
|
+
return "{ query: Record<string, string>; url: string }";
|
|
1098
|
+
}
|
|
1099
|
+
if (params.parseBody) {
|
|
1100
|
+
return "{ body: unknown; url: string }";
|
|
1101
|
+
}
|
|
1102
|
+
return "{ url: string }";
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
var cronJobPlugin = {
|
|
1106
|
+
nodeType: "cron_job" /* CRON_JOB */,
|
|
1107
|
+
generate: () => {
|
|
1108
|
+
},
|
|
1109
|
+
getOutputType: () => "{ triggeredAt: string }"
|
|
1110
|
+
};
|
|
1111
|
+
var manualPlugin = {
|
|
1112
|
+
nodeType: "manual" /* MANUAL */,
|
|
1113
|
+
generate: () => {
|
|
1114
|
+
},
|
|
1115
|
+
getOutputType: () => "Record<string, unknown>"
|
|
1116
|
+
};
|
|
1117
|
+
var fetchApiPlugin = {
|
|
1118
|
+
nodeType: "fetch_api" /* FETCH_API */,
|
|
1119
|
+
generate(node, writer, context) {
|
|
1120
|
+
const params = node.params;
|
|
1121
|
+
const url = context.resolveEnvVars(params.url);
|
|
1122
|
+
writer.write("try ").block(() => {
|
|
1123
|
+
const hasBody = params.body && ["POST", "PUT", "PATCH"].includes(params.method);
|
|
1124
|
+
writer.writeLine(`const response = await fetch(${url}, {`);
|
|
1125
|
+
writer.writeLine(` method: "${params.method}",`);
|
|
1126
|
+
if (params.headers && Object.keys(params.headers).length > 0) {
|
|
1127
|
+
writer.writeLine(` headers: ${JSON.stringify(params.headers)},`);
|
|
1128
|
+
} else if (hasBody) {
|
|
1129
|
+
writer.writeLine(
|
|
1130
|
+
` headers: { "Content-Type": "application/json" },`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
if (hasBody) {
|
|
1134
|
+
const bodyExpr = context.resolveExpression(params.body, node.id);
|
|
1135
|
+
if (bodyExpr.trimStart().startsWith("JSON.stringify")) {
|
|
1136
|
+
writer.writeLine(` body: ${bodyExpr},`);
|
|
1137
|
+
} else {
|
|
1138
|
+
writer.writeLine(` body: JSON.stringify(${bodyExpr}),`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
writer.writeLine("});");
|
|
1142
|
+
writer.blankLine();
|
|
1143
|
+
writer.write("if (!response.ok) ").block(() => {
|
|
1144
|
+
writer.writeLine(
|
|
1145
|
+
`throw new Error(\`${node.label} failed: HTTP \${response.status} \${response.statusText}\`);`
|
|
1146
|
+
);
|
|
1147
|
+
});
|
|
1148
|
+
writer.blankLine();
|
|
1149
|
+
if (params.parseJson) {
|
|
1150
|
+
writer.writeLine(`const data = await response.json();`);
|
|
1151
|
+
writer.writeLine(`flowState['${node.id}'] = {`);
|
|
1152
|
+
writer.writeLine(` data,`);
|
|
1153
|
+
writer.writeLine(` status: response.status,`);
|
|
1154
|
+
writer.writeLine(` headers: Object.fromEntries(response.headers.entries()),`);
|
|
1155
|
+
writer.writeLine(`};`);
|
|
1156
|
+
} else {
|
|
1157
|
+
writer.writeLine(`flowState['${node.id}'] = response;`);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
writer.write(" catch (fetchError) ").block(() => {
|
|
1161
|
+
writer.writeLine(
|
|
1162
|
+
`console.error("Fetch failed for ${node.label}:", fetchError);`
|
|
1163
|
+
);
|
|
1164
|
+
writer.writeLine(`throw fetchError;`);
|
|
1165
|
+
});
|
|
1166
|
+
},
|
|
1167
|
+
getRequiredPackages: () => [],
|
|
1168
|
+
getOutputType(node) {
|
|
1169
|
+
const params = node.params;
|
|
1170
|
+
return params.parseJson ? "{ data: unknown; status: number; headers: Record<string, string> }" : "Response";
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
var sqlQueryPlugin = {
|
|
1174
|
+
nodeType: "sql_query" /* SQL_QUERY */,
|
|
1175
|
+
generate(node, writer) {
|
|
1176
|
+
const params = node.params;
|
|
1177
|
+
switch (params.orm) {
|
|
1178
|
+
case "drizzle":
|
|
1179
|
+
writer.writeLine(`// Drizzle ORM Query`);
|
|
1180
|
+
writer.writeLine(
|
|
1181
|
+
`const result = await db.execute(sql\`${params.query}\`);`
|
|
1182
|
+
);
|
|
1183
|
+
writer.writeLine(`flowState['${node.id}'] = result;`);
|
|
1184
|
+
break;
|
|
1185
|
+
case "prisma":
|
|
1186
|
+
writer.writeLine(`// Prisma Query`);
|
|
1187
|
+
writer.writeLine(
|
|
1188
|
+
`const result = await prisma.$queryRaw\`${params.query}\`;`
|
|
1189
|
+
);
|
|
1190
|
+
writer.writeLine(`flowState['${node.id}'] = result;`);
|
|
1191
|
+
break;
|
|
1192
|
+
case "raw":
|
|
1193
|
+
default:
|
|
1194
|
+
writer.writeLine(`// Raw SQL Query`);
|
|
1195
|
+
writer.writeLine(
|
|
1196
|
+
`const result = await db.query(\`${params.query}\`);`
|
|
1197
|
+
);
|
|
1198
|
+
writer.writeLine(`flowState['${node.id}'] = result;`);
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
getRequiredPackages(node) {
|
|
1203
|
+
const params = node.params;
|
|
1204
|
+
const map = {
|
|
1205
|
+
drizzle: ["drizzle-orm"],
|
|
1206
|
+
prisma: ["@prisma/client"],
|
|
1207
|
+
raw: []
|
|
1208
|
+
};
|
|
1209
|
+
return map[params.orm] ?? [];
|
|
1210
|
+
},
|
|
1211
|
+
getOutputType: () => "unknown[]"
|
|
1212
|
+
};
|
|
1213
|
+
var redisCachePlugin = {
|
|
1214
|
+
nodeType: "redis_cache" /* REDIS_CACHE */,
|
|
1215
|
+
generate(node, writer) {
|
|
1216
|
+
const params = node.params;
|
|
1217
|
+
const needsInterpolation = params.key.includes("${") || params.key.includes("{{");
|
|
1218
|
+
const keyExpr = needsInterpolation ? `\`${params.key}\`` : `"${params.key}"`;
|
|
1219
|
+
switch (params.operation) {
|
|
1220
|
+
case "get":
|
|
1221
|
+
writer.writeLine(
|
|
1222
|
+
`flowState['${node.id}'] = await redis.get(${keyExpr});`
|
|
1223
|
+
);
|
|
1224
|
+
break;
|
|
1225
|
+
case "set":
|
|
1226
|
+
if (params.ttl) {
|
|
1227
|
+
writer.writeLine(
|
|
1228
|
+
`await redis.set(${keyExpr}, ${params.value ?? "null"}, "EX", ${params.ttl});`
|
|
1229
|
+
);
|
|
1230
|
+
} else {
|
|
1231
|
+
writer.writeLine(
|
|
1232
|
+
`await redis.set(${keyExpr}, ${params.value ?? "null"});`
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
writer.writeLine(`flowState['${node.id}'] = true;`);
|
|
1236
|
+
break;
|
|
1237
|
+
case "del":
|
|
1238
|
+
writer.writeLine(`await redis.del(${keyExpr});`);
|
|
1239
|
+
writer.writeLine(`flowState['${node.id}'] = true;`);
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
getRequiredPackages: () => ["ioredis"],
|
|
1244
|
+
getOutputType(node) {
|
|
1245
|
+
const params = node.params;
|
|
1246
|
+
return params.operation === "get" ? "string | null" : "boolean";
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
var DANGEROUS_CODE_PATTERNS = [
|
|
1250
|
+
{ pattern: /\bprocess\.exit\b/, desc: "process.exit() \u2014 terminates the Node.js process" },
|
|
1251
|
+
{ pattern: /\bchild_process\b/, desc: "child_process \u2014 can execute arbitrary system commands" },
|
|
1252
|
+
{ pattern: /\beval\s*\(/, desc: "eval() \u2014 dynamically executes arbitrary code" },
|
|
1253
|
+
{ pattern: /\bnew\s+Function\s*\(/, desc: "new Function() \u2014 dynamically constructs functions" },
|
|
1254
|
+
{ pattern: /\brequire\s*\(\s*['"]fs['"]/, desc: "require('fs') \u2014 file system access" },
|
|
1255
|
+
{ pattern: /\bimport\s*\(\s*['"]fs['"]/, desc: "import('fs') \u2014 file system access" },
|
|
1256
|
+
{ pattern: /\bfs\.\w*(unlink|rmdir|rm|writeFile)\b/, desc: "fs delete/write operations" }
|
|
1257
|
+
];
|
|
1258
|
+
var customCodePlugin = {
|
|
1259
|
+
nodeType: "custom_code" /* CUSTOM_CODE */,
|
|
1260
|
+
generate(node, writer, context) {
|
|
1261
|
+
const params = node.params;
|
|
1262
|
+
const warnings = [];
|
|
1263
|
+
for (const { pattern, desc } of DANGEROUS_CODE_PATTERNS) {
|
|
1264
|
+
if (pattern.test(params.code)) {
|
|
1265
|
+
warnings.push(desc);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (warnings.length > 0) {
|
|
1269
|
+
writer.writeLine(`// \u26A0\uFE0F SECURITY WARNING: This custom code uses the following dangerous APIs:`);
|
|
1270
|
+
for (const w of warnings) {
|
|
1271
|
+
writer.writeLine(`// - ${w}`);
|
|
1272
|
+
}
|
|
1273
|
+
writer.writeLine(`// Please carefully review this code before deployment.`);
|
|
1274
|
+
if (context && "addWarning" in context) {
|
|
1275
|
+
const addWarning = context.addWarning;
|
|
1276
|
+
addWarning?.(`[${node.id}] Custom code uses dangerous API: ${warnings.join(", ")}`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const resultVar = `custom_result_${node.id.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
1280
|
+
if (params.returnVariable) {
|
|
1281
|
+
writer.writeLine(`const ${resultVar} = await (async () => {`);
|
|
1282
|
+
} else {
|
|
1283
|
+
writer.writeLine(`await (async () => {`);
|
|
1284
|
+
}
|
|
1285
|
+
writer.writeLine(`// Custom Code: ${node.label}`);
|
|
1286
|
+
const lines = params.code.split("\n");
|
|
1287
|
+
for (const line of lines) {
|
|
1288
|
+
writer.writeLine(` ${line}`);
|
|
1289
|
+
}
|
|
1290
|
+
if (params.returnVariable) {
|
|
1291
|
+
writer.writeLine(` if (typeof ${params.returnVariable} !== 'undefined') return ${params.returnVariable};`);
|
|
1292
|
+
}
|
|
1293
|
+
writer.writeLine(`})();`);
|
|
1294
|
+
if (params.returnVariable) {
|
|
1295
|
+
writer.writeLine(`flowState['${node.id}'] = ${resultVar};`);
|
|
1296
|
+
}
|
|
1297
|
+
},
|
|
1298
|
+
getOutputType(node) {
|
|
1299
|
+
const params = node.params;
|
|
1300
|
+
if (params.returnType) return params.returnType;
|
|
1301
|
+
if (!params.returnVariable) return "void";
|
|
1302
|
+
return inferTypeFromCode(params.code, params.returnVariable);
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
var callSubflowPlugin = {
|
|
1306
|
+
nodeType: "call_subflow" /* CALL_SUBFLOW */,
|
|
1307
|
+
generate(node, writer, context) {
|
|
1308
|
+
const params = node.params;
|
|
1309
|
+
const existing = context.imports.get(params.flowPath);
|
|
1310
|
+
if (existing) {
|
|
1311
|
+
existing.add(params.functionName);
|
|
1312
|
+
} else {
|
|
1313
|
+
context.imports.set(params.flowPath, /* @__PURE__ */ new Set([params.functionName]));
|
|
1314
|
+
}
|
|
1315
|
+
const args = Object.entries(params.inputMapping).map(([key, expr]) => {
|
|
1316
|
+
const resolved = context.resolveExpression(expr, node.id);
|
|
1317
|
+
return `${key}: ${resolved}`;
|
|
1318
|
+
}).join(", ");
|
|
1319
|
+
writer.writeLine(
|
|
1320
|
+
`flowState['${node.id}'] = await ${params.functionName}({ ${args} });`
|
|
1321
|
+
);
|
|
1322
|
+
},
|
|
1323
|
+
getOutputType(node) {
|
|
1324
|
+
const params = node.params;
|
|
1325
|
+
if (params.returnType) return params.returnType;
|
|
1326
|
+
return `Awaited<ReturnType<typeof ${params.functionName}>>`;
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
var ifElsePlugin = {
|
|
1330
|
+
nodeType: "if_else" /* IF_ELSE */,
|
|
1331
|
+
generate(node, writer, context) {
|
|
1332
|
+
const params = node.params;
|
|
1333
|
+
const trueEdges = context.ir.edges.filter(
|
|
1334
|
+
(e) => e.sourceNodeId === node.id && e.sourcePortId === "true"
|
|
1335
|
+
);
|
|
1336
|
+
const falseEdges = context.ir.edges.filter(
|
|
1337
|
+
(e) => e.sourceNodeId === node.id && e.sourcePortId === "false"
|
|
1338
|
+
);
|
|
1339
|
+
const conditionExpr = context.resolveExpression(
|
|
1340
|
+
params.condition,
|
|
1341
|
+
node.id
|
|
1342
|
+
);
|
|
1343
|
+
writer.write(`if (${conditionExpr}) `).block(() => {
|
|
1344
|
+
writer.writeLine(`flowState['${node.id}'] = true;`);
|
|
1345
|
+
for (const edge of trueEdges) {
|
|
1346
|
+
const childNode = context.nodeMap.get(edge.targetNodeId);
|
|
1347
|
+
if (childNode) {
|
|
1348
|
+
context.generateChildNode(writer, childNode);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
if (falseEdges.length > 0) {
|
|
1353
|
+
writer.write(" else ").block(() => {
|
|
1354
|
+
writer.writeLine(`flowState['${node.id}'] = false;`);
|
|
1355
|
+
for (const edge of falseEdges) {
|
|
1356
|
+
const childNode = context.nodeMap.get(edge.targetNodeId);
|
|
1357
|
+
if (childNode) {
|
|
1358
|
+
context.generateChildNode(writer, childNode);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
},
|
|
1364
|
+
getOutputType: () => "boolean"
|
|
1365
|
+
};
|
|
1366
|
+
var forLoopPlugin = {
|
|
1367
|
+
nodeType: "for_loop" /* FOR_LOOP */,
|
|
1368
|
+
generate(node, writer, context) {
|
|
1369
|
+
const params = node.params;
|
|
1370
|
+
const iterableExpr = context.resolveExpression(
|
|
1371
|
+
params.iterableExpression,
|
|
1372
|
+
node.id
|
|
1373
|
+
);
|
|
1374
|
+
const sanitizedId = node.id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1375
|
+
const scopeVar = `_scope_${sanitizedId}`;
|
|
1376
|
+
writer.writeLine(`const ${sanitizedId}_results: unknown[] = [];`);
|
|
1377
|
+
if (params.indexVariable) {
|
|
1378
|
+
writer.write(
|
|
1379
|
+
`for (const [${params.indexVariable}, ${params.itemVariable}] of (${iterableExpr}).entries()) `
|
|
1380
|
+
);
|
|
1381
|
+
} else {
|
|
1382
|
+
writer.write(
|
|
1383
|
+
`for (const ${params.itemVariable} of ${iterableExpr}) `
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
writer.block(() => {
|
|
1387
|
+
writer.writeLine(
|
|
1388
|
+
`const ${scopeVar}: Record<string, unknown> = {};`
|
|
1389
|
+
);
|
|
1390
|
+
writer.writeLine(
|
|
1391
|
+
`${scopeVar}['${node.id}'] = ${params.itemVariable};`
|
|
1392
|
+
);
|
|
1393
|
+
context.pushScope(node.id, scopeVar);
|
|
1394
|
+
const childEdges = context.ir.edges.filter(
|
|
1395
|
+
(e) => e.sourceNodeId === node.id && e.sourcePortId === "body"
|
|
1396
|
+
);
|
|
1397
|
+
for (const edge of childEdges) {
|
|
1398
|
+
const childNode = context.nodeMap.get(edge.targetNodeId);
|
|
1399
|
+
if (childNode) {
|
|
1400
|
+
context.generateChildNode(writer, childNode);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
context.popScope();
|
|
1404
|
+
writer.writeLine(`${sanitizedId}_results.push(${params.itemVariable});`);
|
|
1405
|
+
});
|
|
1406
|
+
writer.writeLine(`flowState['${node.id}'] = ${sanitizedId}_results;`);
|
|
1407
|
+
},
|
|
1408
|
+
getOutputType: () => "unknown[]"
|
|
1409
|
+
};
|
|
1410
|
+
var tryCatchPlugin = {
|
|
1411
|
+
nodeType: "try_catch" /* TRY_CATCH */,
|
|
1412
|
+
generate(node, writer, context) {
|
|
1413
|
+
const params = node.params;
|
|
1414
|
+
const successEdges = context.ir.edges.filter(
|
|
1415
|
+
(e) => e.sourceNodeId === node.id && e.sourcePortId === "success"
|
|
1416
|
+
);
|
|
1417
|
+
const errorEdges = context.ir.edges.filter(
|
|
1418
|
+
(e) => e.sourceNodeId === node.id && e.sourcePortId === "error"
|
|
1419
|
+
);
|
|
1420
|
+
const tryScopeVar = `_scope_${node.id.replace(/[^a-zA-Z0-9_]/g, "_")}_try`;
|
|
1421
|
+
const catchScopeVar = `_scope_${node.id.replace(/[^a-zA-Z0-9_]/g, "_")}_catch`;
|
|
1422
|
+
writer.write("try ").block(() => {
|
|
1423
|
+
writer.writeLine(
|
|
1424
|
+
`const ${tryScopeVar}: Record<string, unknown> = {};`
|
|
1425
|
+
);
|
|
1426
|
+
context.pushScope(node.id, tryScopeVar);
|
|
1427
|
+
for (const edge of successEdges) {
|
|
1428
|
+
const childNode = context.nodeMap.get(edge.targetNodeId);
|
|
1429
|
+
if (childNode) {
|
|
1430
|
+
context.generateChildNode(writer, childNode);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
context.popScope();
|
|
1434
|
+
writer.writeLine(
|
|
1435
|
+
`flowState['${node.id}'] = { success: true };`
|
|
1436
|
+
);
|
|
1437
|
+
});
|
|
1438
|
+
writer.write(` catch (${params.errorVariable}) `).block(() => {
|
|
1439
|
+
writer.writeLine(
|
|
1440
|
+
`console.error("Error in ${node.label}:", ${params.errorVariable});`
|
|
1441
|
+
);
|
|
1442
|
+
writer.writeLine(
|
|
1443
|
+
`const ${catchScopeVar}: Record<string, unknown> = {};`
|
|
1444
|
+
);
|
|
1445
|
+
writer.writeLine(
|
|
1446
|
+
`flowState['${node.id}'] = { success: false, error: ${params.errorVariable} };`
|
|
1447
|
+
);
|
|
1448
|
+
context.pushScope(node.id, catchScopeVar);
|
|
1449
|
+
for (const edge of errorEdges) {
|
|
1450
|
+
const childNode = context.nodeMap.get(edge.targetNodeId);
|
|
1451
|
+
if (childNode) {
|
|
1452
|
+
context.generateChildNode(writer, childNode);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
context.popScope();
|
|
1456
|
+
});
|
|
1457
|
+
},
|
|
1458
|
+
getOutputType: () => "{ success: boolean; error?: unknown }"
|
|
1459
|
+
};
|
|
1460
|
+
var promiseAllPlugin = {
|
|
1461
|
+
nodeType: "promise_all" /* PROMISE_ALL */,
|
|
1462
|
+
generate(node, writer) {
|
|
1463
|
+
writer.writeLine(`// Promise.all handled by concurrent execution`);
|
|
1464
|
+
writer.writeLine(
|
|
1465
|
+
`flowState['${node.id}'] = undefined; // populated by concurrent handler`
|
|
1466
|
+
);
|
|
1467
|
+
},
|
|
1468
|
+
getOutputType: () => "unknown[]"
|
|
1469
|
+
};
|
|
1470
|
+
var declarePlugin = {
|
|
1471
|
+
nodeType: "declare" /* DECLARE */,
|
|
1472
|
+
generate(node, writer) {
|
|
1473
|
+
const params = node.params;
|
|
1474
|
+
const keyword = params.isConst ? "const" : "let";
|
|
1475
|
+
const initialValue = params.initialValue ?? "undefined";
|
|
1476
|
+
writer.writeLine(`${keyword} ${params.name} = ${initialValue};`);
|
|
1477
|
+
writer.writeLine(`flowState['${node.id}'] = ${params.name};`);
|
|
1478
|
+
},
|
|
1479
|
+
getOutputType(node) {
|
|
1480
|
+
const params = node.params;
|
|
1481
|
+
return params.dataType;
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
var transformPlugin = {
|
|
1485
|
+
nodeType: "transform" /* TRANSFORM */,
|
|
1486
|
+
generate(node, writer, context) {
|
|
1487
|
+
const params = node.params;
|
|
1488
|
+
const expr = context.resolveExpression(params.expression, node.id);
|
|
1489
|
+
writer.writeLine(`flowState['${node.id}'] = ${expr};`);
|
|
1490
|
+
},
|
|
1491
|
+
getOutputType(node) {
|
|
1492
|
+
const params = node.params;
|
|
1493
|
+
return inferTypeFromExpression(params.expression);
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
var returnResponsePlugin = {
|
|
1497
|
+
nodeType: "return_response" /* RETURN_RESPONSE */,
|
|
1498
|
+
generate(node, writer, context) {
|
|
1499
|
+
const params = node.params;
|
|
1500
|
+
const bodyExpr = context.resolveExpression(
|
|
1501
|
+
params.bodyExpression,
|
|
1502
|
+
node.id
|
|
1503
|
+
);
|
|
1504
|
+
const ctx = context;
|
|
1505
|
+
if (ctx.__platformResponse) {
|
|
1506
|
+
ctx.__platformResponse(writer, bodyExpr, params.statusCode, params.headers);
|
|
1507
|
+
} else {
|
|
1508
|
+
if (params.headers && Object.keys(params.headers).length > 0) {
|
|
1509
|
+
writer.writeLine(
|
|
1510
|
+
`return NextResponse.json(${bodyExpr}, { status: ${params.statusCode}, headers: ${JSON.stringify(params.headers)} });`
|
|
1511
|
+
);
|
|
1512
|
+
} else {
|
|
1513
|
+
writer.writeLine(
|
|
1514
|
+
`return NextResponse.json(${bodyExpr}, { status: ${params.statusCode} });`
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
getOutputType: () => "never"
|
|
1520
|
+
};
|
|
1521
|
+
var builtinPlugins = [
|
|
1522
|
+
// Triggers
|
|
1523
|
+
httpWebhookPlugin,
|
|
1524
|
+
cronJobPlugin,
|
|
1525
|
+
manualPlugin,
|
|
1526
|
+
// Actions
|
|
1527
|
+
fetchApiPlugin,
|
|
1528
|
+
sqlQueryPlugin,
|
|
1529
|
+
redisCachePlugin,
|
|
1530
|
+
customCodePlugin,
|
|
1531
|
+
callSubflowPlugin,
|
|
1532
|
+
// Logic
|
|
1533
|
+
ifElsePlugin,
|
|
1534
|
+
forLoopPlugin,
|
|
1535
|
+
tryCatchPlugin,
|
|
1536
|
+
promiseAllPlugin,
|
|
1537
|
+
// Variables
|
|
1538
|
+
declarePlugin,
|
|
1539
|
+
transformPlugin,
|
|
1540
|
+
// Output
|
|
1541
|
+
returnResponsePlugin
|
|
1542
|
+
];
|
|
1543
|
+
|
|
1544
|
+
// src/lib/compiler/type-inference.ts
|
|
1545
|
+
function inferFlowStateTypes(ir, registry) {
|
|
1546
|
+
const nodeTypes = /* @__PURE__ */ new Map();
|
|
1547
|
+
for (const node of ir.nodes) {
|
|
1548
|
+
const type = inferNodeOutputType(node, registry);
|
|
1549
|
+
nodeTypes.set(node.id, type);
|
|
1550
|
+
}
|
|
1551
|
+
const fields = ir.nodes.map((node) => {
|
|
1552
|
+
const type = nodeTypes.get(node.id) || "unknown";
|
|
1553
|
+
const safeId = node.id;
|
|
1554
|
+
return ` '${safeId}'?: ${type};`;
|
|
1555
|
+
}).join("\n");
|
|
1556
|
+
const interfaceCode = `interface FlowState {
|
|
1557
|
+
${fields}
|
|
1558
|
+
}`;
|
|
1559
|
+
return { interfaceCode, nodeTypes };
|
|
1560
|
+
}
|
|
1561
|
+
function inferNodeOutputType(node, registry) {
|
|
1562
|
+
const plugin = registry?.get(node.nodeType) ?? getPlugin(node.nodeType);
|
|
1563
|
+
if (plugin?.getOutputType) {
|
|
1564
|
+
return plugin.getOutputType(node);
|
|
1565
|
+
}
|
|
1566
|
+
if (node.outputs && node.outputs.length > 0) {
|
|
1567
|
+
const primaryOutput = node.outputs[0];
|
|
1568
|
+
return mapFlowDataTypeToTS(primaryOutput.dataType);
|
|
1569
|
+
}
|
|
1570
|
+
return "unknown";
|
|
1571
|
+
}
|
|
1572
|
+
function mapFlowDataTypeToTS(dataType) {
|
|
1573
|
+
switch (dataType) {
|
|
1574
|
+
case "string":
|
|
1575
|
+
return "string";
|
|
1576
|
+
case "number":
|
|
1577
|
+
return "number";
|
|
1578
|
+
case "boolean":
|
|
1579
|
+
return "boolean";
|
|
1580
|
+
case "object":
|
|
1581
|
+
return "Record<string, unknown>";
|
|
1582
|
+
case "array":
|
|
1583
|
+
return "unknown[]";
|
|
1584
|
+
case "void":
|
|
1585
|
+
return "void";
|
|
1586
|
+
case "Response":
|
|
1587
|
+
return "Response";
|
|
1588
|
+
case "any":
|
|
1589
|
+
default:
|
|
1590
|
+
return "unknown";
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/lib/compiler/symbol-table.ts
|
|
1595
|
+
function buildSymbolTable(ir) {
|
|
1596
|
+
const mappings = /* @__PURE__ */ new Map();
|
|
1597
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
1598
|
+
for (const node of ir.nodes) {
|
|
1599
|
+
let name = labelToVarName(node.label);
|
|
1600
|
+
if (!name || RESERVED_WORDS.has(name)) {
|
|
1601
|
+
name = `${name || "node"}Result`;
|
|
1602
|
+
}
|
|
1603
|
+
if (/^[0-9]/.test(name)) {
|
|
1604
|
+
name = `_${name}`;
|
|
1605
|
+
}
|
|
1606
|
+
let uniqueName = name;
|
|
1607
|
+
let counter = 2;
|
|
1608
|
+
while (usedNames.has(uniqueName)) {
|
|
1609
|
+
uniqueName = `${name}${counter}`;
|
|
1610
|
+
counter++;
|
|
1611
|
+
}
|
|
1612
|
+
usedNames.add(uniqueName);
|
|
1613
|
+
mappings.set(node.id, uniqueName);
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
getVarName(nodeId) {
|
|
1617
|
+
return mappings.get(nodeId) ?? `node_${nodeId.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
1618
|
+
},
|
|
1619
|
+
hasVar(nodeId) {
|
|
1620
|
+
return mappings.has(nodeId);
|
|
1621
|
+
},
|
|
1622
|
+
getAllMappings() {
|
|
1623
|
+
return mappings;
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
function labelToVarName(label) {
|
|
1628
|
+
const cleaned = label.replace(/[/\-_.]/g, " ").replace(/[^a-zA-Z0-9\s]/g, "").trim();
|
|
1629
|
+
if (!cleaned) return "";
|
|
1630
|
+
const words = cleaned.replace(/([a-z])([A-Z])/g, "$1 $2").split(/\s+/).filter((w) => w.length > 0);
|
|
1631
|
+
if (words.length === 0) return "";
|
|
1632
|
+
return words.map((word, i) => {
|
|
1633
|
+
const lower = word.toLowerCase();
|
|
1634
|
+
if (i === 0) return lower;
|
|
1635
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
1636
|
+
}).join("");
|
|
1637
|
+
}
|
|
1638
|
+
var RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
1639
|
+
// JavaScript reserved words
|
|
1640
|
+
"break",
|
|
1641
|
+
"case",
|
|
1642
|
+
"catch",
|
|
1643
|
+
"continue",
|
|
1644
|
+
"debugger",
|
|
1645
|
+
"default",
|
|
1646
|
+
"delete",
|
|
1647
|
+
"do",
|
|
1648
|
+
"else",
|
|
1649
|
+
"finally",
|
|
1650
|
+
"for",
|
|
1651
|
+
"function",
|
|
1652
|
+
"if",
|
|
1653
|
+
"in",
|
|
1654
|
+
"instanceof",
|
|
1655
|
+
"new",
|
|
1656
|
+
"return",
|
|
1657
|
+
"switch",
|
|
1658
|
+
"this",
|
|
1659
|
+
"throw",
|
|
1660
|
+
"try",
|
|
1661
|
+
"typeof",
|
|
1662
|
+
"var",
|
|
1663
|
+
"void",
|
|
1664
|
+
"while",
|
|
1665
|
+
"with",
|
|
1666
|
+
"class",
|
|
1667
|
+
"const",
|
|
1668
|
+
"enum",
|
|
1669
|
+
"export",
|
|
1670
|
+
"extends",
|
|
1671
|
+
"import",
|
|
1672
|
+
"super",
|
|
1673
|
+
"implements",
|
|
1674
|
+
"interface",
|
|
1675
|
+
"let",
|
|
1676
|
+
"package",
|
|
1677
|
+
"private",
|
|
1678
|
+
"protected",
|
|
1679
|
+
"public",
|
|
1680
|
+
"static",
|
|
1681
|
+
"yield",
|
|
1682
|
+
"await",
|
|
1683
|
+
"async",
|
|
1684
|
+
// Built-in global values
|
|
1685
|
+
"undefined",
|
|
1686
|
+
"null",
|
|
1687
|
+
"true",
|
|
1688
|
+
"false",
|
|
1689
|
+
"NaN",
|
|
1690
|
+
"Infinity",
|
|
1691
|
+
// Common identifiers in generated code (avoid shadowing)
|
|
1692
|
+
"req",
|
|
1693
|
+
"res",
|
|
1694
|
+
"body",
|
|
1695
|
+
"query",
|
|
1696
|
+
"data",
|
|
1697
|
+
"result",
|
|
1698
|
+
"response",
|
|
1699
|
+
"error",
|
|
1700
|
+
"flowState",
|
|
1701
|
+
"searchParams",
|
|
1702
|
+
"NextResponse",
|
|
1703
|
+
"NextRequest"
|
|
1704
|
+
]);
|
|
1705
|
+
|
|
1706
|
+
// src/lib/compiler/compiler.ts
|
|
1707
|
+
function compile(ir, options) {
|
|
1708
|
+
const pluginRegistry = createPluginRegistry();
|
|
1709
|
+
pluginRegistry.registerAll(builtinPlugins);
|
|
1710
|
+
if (options?.plugins) {
|
|
1711
|
+
pluginRegistry.registerAll(options.plugins);
|
|
1712
|
+
}
|
|
1713
|
+
const validation = validateFlowIR(ir);
|
|
1714
|
+
if (!validation.valid) {
|
|
1715
|
+
return {
|
|
1716
|
+
success: false,
|
|
1717
|
+
errors: validation.errors.map((e) => `[${e.code}] ${e.message}`)
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
let plan;
|
|
1721
|
+
try {
|
|
1722
|
+
plan = topologicalSort(ir);
|
|
1723
|
+
} catch (err) {
|
|
1724
|
+
return {
|
|
1725
|
+
success: false,
|
|
1726
|
+
errors: [err instanceof Error ? err.message : String(err)]
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
1730
|
+
const platformName = options?.platform ?? "nextjs";
|
|
1731
|
+
const platform = getPlatform(platformName);
|
|
1732
|
+
const symbolTable = buildSymbolTable(ir);
|
|
1733
|
+
const context = {
|
|
1734
|
+
ir,
|
|
1735
|
+
plan,
|
|
1736
|
+
nodeMap,
|
|
1737
|
+
envVars: /* @__PURE__ */ new Set(),
|
|
1738
|
+
imports: /* @__PURE__ */ new Map(),
|
|
1739
|
+
requiredPackages: /* @__PURE__ */ new Set(),
|
|
1740
|
+
sourceMapEntries: /* @__PURE__ */ new Map(),
|
|
1741
|
+
currentLine: 1,
|
|
1742
|
+
platform,
|
|
1743
|
+
symbolTable,
|
|
1744
|
+
scopeStack: [],
|
|
1745
|
+
childBlockNodeIds: /* @__PURE__ */ new Set(),
|
|
1746
|
+
dagMode: false,
|
|
1747
|
+
symbolTableExclusions: /* @__PURE__ */ new Set(),
|
|
1748
|
+
generatedBlockNodeIds: /* @__PURE__ */ new Set(),
|
|
1749
|
+
pluginRegistry
|
|
1750
|
+
};
|
|
1751
|
+
const trigger = ir.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
|
|
1752
|
+
const preComputedBlockNodes = computeControlFlowDescendants(ir, trigger.id);
|
|
1753
|
+
for (const nodeId of preComputedBlockNodes) {
|
|
1754
|
+
context.childBlockNodeIds.add(nodeId);
|
|
1755
|
+
context.symbolTableExclusions.add(nodeId);
|
|
1756
|
+
}
|
|
1757
|
+
const hasConcurrency = plan.steps.some(
|
|
1758
|
+
(s) => s.concurrent && s.nodeIds.filter((id) => id !== trigger.id).length > 1
|
|
1759
|
+
);
|
|
1760
|
+
if (hasConcurrency) {
|
|
1761
|
+
context.dagMode = true;
|
|
1762
|
+
for (const node of ir.nodes) {
|
|
1763
|
+
if (node.category !== "trigger" /* TRIGGER */) {
|
|
1764
|
+
context.symbolTableExclusions.add(node.id);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
1769
|
+
const sourceFile = project.createSourceFile("generated.ts", "");
|
|
1770
|
+
try {
|
|
1771
|
+
generateCode(sourceFile, trigger, context);
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
throw err;
|
|
1774
|
+
}
|
|
1775
|
+
sourceFile.formatText({
|
|
1776
|
+
indentSize: 2,
|
|
1777
|
+
convertTabsToSpaces: true
|
|
1778
|
+
});
|
|
1779
|
+
const code = sourceFile.getFullText();
|
|
1780
|
+
const filePath = platform.getOutputFilePath(trigger);
|
|
1781
|
+
collectRequiredPackages(ir, context);
|
|
1782
|
+
const sourceMap = buildSourceMap(code, ir, filePath);
|
|
1783
|
+
const dependencies = {
|
|
1784
|
+
all: [...context.requiredPackages].sort(),
|
|
1785
|
+
missing: [...context.requiredPackages].sort(),
|
|
1786
|
+
installCommand: context.requiredPackages.size > 0 ? `npm install ${[...context.requiredPackages].sort().join(" ")}` : void 0
|
|
1787
|
+
};
|
|
1788
|
+
return {
|
|
1789
|
+
success: true,
|
|
1790
|
+
code,
|
|
1791
|
+
filePath,
|
|
1792
|
+
dependencies,
|
|
1793
|
+
sourceMap
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
function generateCode(sourceFile, trigger, context) {
|
|
1797
|
+
const { platform } = context;
|
|
1798
|
+
platform.generateImports(sourceFile, trigger, {
|
|
1799
|
+
ir: context.ir,
|
|
1800
|
+
nodeMap: context.nodeMap,
|
|
1801
|
+
envVars: context.envVars,
|
|
1802
|
+
imports: context.imports
|
|
1803
|
+
});
|
|
1804
|
+
platform.generateFunction(
|
|
1805
|
+
sourceFile,
|
|
1806
|
+
trigger,
|
|
1807
|
+
{
|
|
1808
|
+
ir: context.ir,
|
|
1809
|
+
nodeMap: context.nodeMap,
|
|
1810
|
+
envVars: context.envVars,
|
|
1811
|
+
imports: context.imports
|
|
1812
|
+
},
|
|
1813
|
+
(writer) => {
|
|
1814
|
+
generateFunctionBody(writer, trigger, context);
|
|
1815
|
+
}
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
function generateFunctionBody(writer, trigger, context) {
|
|
1819
|
+
const { ir } = context;
|
|
1820
|
+
const typeInfo = inferFlowStateTypes(ir, context.pluginRegistry);
|
|
1821
|
+
writer.writeLine(typeInfo.interfaceCode);
|
|
1822
|
+
writer.writeLine("const flowState: Partial<FlowState> = {};");
|
|
1823
|
+
writer.blankLine();
|
|
1824
|
+
context.platform.generateTriggerInit(writer, trigger, {
|
|
1825
|
+
symbolTable: context.symbolTable
|
|
1826
|
+
});
|
|
1827
|
+
writer.blankLine();
|
|
1828
|
+
generateNodeChain(writer, trigger.id, context);
|
|
1829
|
+
}
|
|
1830
|
+
var CONTROL_FLOW_PORT_MAP = {
|
|
1831
|
+
["if_else" /* IF_ELSE */]: /* @__PURE__ */ new Set(["true", "false"]),
|
|
1832
|
+
["for_loop" /* FOR_LOOP */]: /* @__PURE__ */ new Set(["body"]),
|
|
1833
|
+
["try_catch" /* TRY_CATCH */]: /* @__PURE__ */ new Set(["success", "error"])
|
|
1834
|
+
};
|
|
1835
|
+
function isControlFlowEdge(edge, nodeMap) {
|
|
1836
|
+
const sourceNode = nodeMap.get(edge.sourceNodeId);
|
|
1837
|
+
if (!sourceNode) return false;
|
|
1838
|
+
const controlPorts = CONTROL_FLOW_PORT_MAP[sourceNode.nodeType];
|
|
1839
|
+
return controlPorts !== void 0 && controlPorts.has(edge.sourcePortId);
|
|
1840
|
+
}
|
|
1841
|
+
function computeControlFlowDescendants(ir, triggerId) {
|
|
1842
|
+
const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
1843
|
+
const strippedSuccessors = /* @__PURE__ */ new Map();
|
|
1844
|
+
for (const node of ir.nodes) {
|
|
1845
|
+
strippedSuccessors.set(node.id, /* @__PURE__ */ new Set());
|
|
1846
|
+
}
|
|
1847
|
+
for (const edge of ir.edges) {
|
|
1848
|
+
if (isControlFlowEdge(edge, nodeMap)) continue;
|
|
1849
|
+
strippedSuccessors.get(edge.sourceNodeId)?.add(edge.targetNodeId);
|
|
1850
|
+
}
|
|
1851
|
+
const reachableWithoutControlFlow = /* @__PURE__ */ new Set();
|
|
1852
|
+
const queue = [triggerId];
|
|
1853
|
+
while (queue.length > 0) {
|
|
1854
|
+
const id = queue.shift();
|
|
1855
|
+
if (reachableWithoutControlFlow.has(id)) continue;
|
|
1856
|
+
reachableWithoutControlFlow.add(id);
|
|
1857
|
+
for (const succ of strippedSuccessors.get(id) ?? []) {
|
|
1858
|
+
if (!reachableWithoutControlFlow.has(succ)) {
|
|
1859
|
+
queue.push(succ);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
const childBlockNodeIds = /* @__PURE__ */ new Set();
|
|
1864
|
+
for (const node of ir.nodes) {
|
|
1865
|
+
if (node.id === triggerId) continue;
|
|
1866
|
+
if (!reachableWithoutControlFlow.has(node.id)) {
|
|
1867
|
+
childBlockNodeIds.add(node.id);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return childBlockNodeIds;
|
|
1871
|
+
}
|
|
1872
|
+
function generateBlockContinuation(writer, fromNodeId, context) {
|
|
1873
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1874
|
+
const bfsQueue = [fromNodeId];
|
|
1875
|
+
const edgeSuccessors = /* @__PURE__ */ new Map();
|
|
1876
|
+
for (const edge of context.ir.edges) {
|
|
1877
|
+
if (!edgeSuccessors.has(edge.sourceNodeId)) {
|
|
1878
|
+
edgeSuccessors.set(edge.sourceNodeId, []);
|
|
1879
|
+
}
|
|
1880
|
+
edgeSuccessors.get(edge.sourceNodeId).push(edge.targetNodeId);
|
|
1881
|
+
}
|
|
1882
|
+
while (bfsQueue.length > 0) {
|
|
1883
|
+
const id = bfsQueue.shift();
|
|
1884
|
+
if (reachable.has(id)) continue;
|
|
1885
|
+
reachable.add(id);
|
|
1886
|
+
for (const succ of edgeSuccessors.get(id) ?? []) {
|
|
1887
|
+
if (!reachable.has(succ)) {
|
|
1888
|
+
bfsQueue.push(succ);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
for (const nodeId of context.plan.sortedNodeIds) {
|
|
1893
|
+
if (nodeId === fromNodeId) continue;
|
|
1894
|
+
if (!reachable.has(nodeId)) continue;
|
|
1895
|
+
if (!context.childBlockNodeIds.has(nodeId)) continue;
|
|
1896
|
+
if (context.generatedBlockNodeIds.has(nodeId)) continue;
|
|
1897
|
+
const deps = context.plan.dependencies.get(nodeId) ?? /* @__PURE__ */ new Set();
|
|
1898
|
+
const allBlockDepsReady = [...deps].every((depId) => {
|
|
1899
|
+
if (!context.childBlockNodeIds.has(depId)) return true;
|
|
1900
|
+
return context.generatedBlockNodeIds.has(depId);
|
|
1901
|
+
});
|
|
1902
|
+
if (!allBlockDepsReady) continue;
|
|
1903
|
+
const node = context.nodeMap.get(nodeId);
|
|
1904
|
+
if (!node) continue;
|
|
1905
|
+
context.generatedBlockNodeIds.add(nodeId);
|
|
1906
|
+
context.symbolTableExclusions.add(nodeId);
|
|
1907
|
+
writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
|
|
1908
|
+
generateNodeBody(writer, node, context);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
function generateNodeChain(writer, triggerId, context) {
|
|
1912
|
+
if (context.dagMode) {
|
|
1913
|
+
generateNodeChainDAG(writer, triggerId, context);
|
|
1914
|
+
} else {
|
|
1915
|
+
generateNodeChainSequential(writer, triggerId, context);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
function generateNodeChainSequential(writer, triggerId, context) {
|
|
1919
|
+
const { plan, nodeMap } = context;
|
|
1920
|
+
for (const step of plan.steps) {
|
|
1921
|
+
const activeNodes = step.nodeIds.filter(
|
|
1922
|
+
(id) => id !== triggerId && !context.childBlockNodeIds.has(id)
|
|
1923
|
+
);
|
|
1924
|
+
if (activeNodes.length === 0) continue;
|
|
1925
|
+
if (step.concurrent && activeNodes.length > 1) {
|
|
1926
|
+
generateConcurrentNodes(writer, activeNodes, context);
|
|
1927
|
+
} else {
|
|
1928
|
+
for (const nodeId of activeNodes) {
|
|
1929
|
+
const node = nodeMap.get(nodeId);
|
|
1930
|
+
if (!node) continue;
|
|
1931
|
+
generateSingleNode(writer, node, context);
|
|
1932
|
+
writer.blankLine();
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
function resolveToDAGNodes(depId, triggerId, context, visited = /* @__PURE__ */ new Set()) {
|
|
1938
|
+
if (visited.has(depId) || depId === triggerId) return [];
|
|
1939
|
+
visited.add(depId);
|
|
1940
|
+
if (!context.childBlockNodeIds.has(depId)) return [depId];
|
|
1941
|
+
const parentDeps = context.plan.dependencies.get(depId) ?? /* @__PURE__ */ new Set();
|
|
1942
|
+
const result = [];
|
|
1943
|
+
for (const pd of parentDeps) {
|
|
1944
|
+
result.push(...resolveToDAGNodes(pd, triggerId, context, visited));
|
|
1945
|
+
}
|
|
1946
|
+
return result;
|
|
1947
|
+
}
|
|
1948
|
+
function generateNodeChainDAG(writer, triggerId, context) {
|
|
1949
|
+
const { plan, nodeMap } = context;
|
|
1950
|
+
const allNodeIds = plan.sortedNodeIds.filter((id) => id !== triggerId);
|
|
1951
|
+
const outputNodeIds = [];
|
|
1952
|
+
const dagNodeIds = [];
|
|
1953
|
+
for (const id of allNodeIds) {
|
|
1954
|
+
if (context.childBlockNodeIds.has(id)) continue;
|
|
1955
|
+
const node = nodeMap.get(id);
|
|
1956
|
+
if (!node) continue;
|
|
1957
|
+
if (node.category === "output" /* OUTPUT */) {
|
|
1958
|
+
outputNodeIds.push(id);
|
|
1959
|
+
} else {
|
|
1960
|
+
dagNodeIds.push(id);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (dagNodeIds.length > 0) {
|
|
1964
|
+
writer.writeLine("// --- DAG Concurrent Execution ---");
|
|
1965
|
+
writer.blankLine();
|
|
1966
|
+
}
|
|
1967
|
+
for (const nodeId of dagNodeIds) {
|
|
1968
|
+
const node = nodeMap.get(nodeId);
|
|
1969
|
+
const rawDeps = [...plan.dependencies.get(nodeId) ?? []];
|
|
1970
|
+
const resolvedDeps = /* @__PURE__ */ new Set();
|
|
1971
|
+
for (const depId of rawDeps) {
|
|
1972
|
+
for (const dagDep of resolveToDAGNodes(depId, triggerId, context)) {
|
|
1973
|
+
resolvedDeps.add(dagDep);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
resolvedDeps.delete(nodeId);
|
|
1977
|
+
const promiseVar = `p_${sanitizeId(nodeId)}`;
|
|
1978
|
+
writer.write(`const ${promiseVar} = (async () => `).block(() => {
|
|
1979
|
+
for (const depId of resolvedDeps) {
|
|
1980
|
+
writer.writeLine(`await p_${sanitizeId(depId)};`);
|
|
1981
|
+
}
|
|
1982
|
+
writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
|
|
1983
|
+
generateNodeBody(writer, node, context);
|
|
1984
|
+
});
|
|
1985
|
+
writer.writeLine(`)();`);
|
|
1986
|
+
writer.writeLine(`${promiseVar}.catch(() => {});`);
|
|
1987
|
+
writer.blankLine();
|
|
1988
|
+
}
|
|
1989
|
+
if (dagNodeIds.length > 0) {
|
|
1990
|
+
writer.writeLine("// --- Sync Barrier: await all DAG promises before output ---");
|
|
1991
|
+
const allPromiseVars = dagNodeIds.map((id) => `p_${sanitizeId(id)}`);
|
|
1992
|
+
writer.writeLine(`await Promise.allSettled([${allPromiseVars.join(", ")}]);`);
|
|
1993
|
+
writer.blankLine();
|
|
1994
|
+
}
|
|
1995
|
+
for (const nodeId of outputNodeIds) {
|
|
1996
|
+
const node = nodeMap.get(nodeId);
|
|
1997
|
+
const rawDeps = [...plan.dependencies.get(nodeId) ?? []];
|
|
1998
|
+
const resolvedDeps = /* @__PURE__ */ new Set();
|
|
1999
|
+
for (const depId of rawDeps) {
|
|
2000
|
+
for (const dagDep of resolveToDAGNodes(depId, triggerId, context)) {
|
|
2001
|
+
resolvedDeps.add(dagDep);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
for (const depId of resolvedDeps) {
|
|
2005
|
+
writer.writeLine(`await p_${sanitizeId(depId)};`);
|
|
2006
|
+
}
|
|
2007
|
+
writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
|
|
2008
|
+
generateNodeBody(writer, node, context);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
function generateConcurrentNodes(writer, nodeIds, context) {
|
|
2012
|
+
const { nodeMap } = context;
|
|
2013
|
+
const activeNodeIds = nodeIds.filter((id) => !context.childBlockNodeIds.has(id));
|
|
2014
|
+
if (activeNodeIds.length === 0) return;
|
|
2015
|
+
if (activeNodeIds.length === 1) {
|
|
2016
|
+
const node = nodeMap.get(activeNodeIds[0]);
|
|
2017
|
+
if (node) {
|
|
2018
|
+
generateSingleNode(writer, node, context);
|
|
2019
|
+
writer.blankLine();
|
|
2020
|
+
}
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
writer.writeLine("// --- Concurrent Execution ---");
|
|
2024
|
+
const taskNames = [];
|
|
2025
|
+
for (const nodeId of activeNodeIds) {
|
|
2026
|
+
const node = nodeMap.get(nodeId);
|
|
2027
|
+
if (!node) continue;
|
|
2028
|
+
const taskName = `task_${sanitizeId(nodeId)}`;
|
|
2029
|
+
taskNames.push(taskName);
|
|
2030
|
+
writer.write(`const ${taskName} = async () => `).block(() => {
|
|
2031
|
+
generateNodeBody(writer, node, context);
|
|
2032
|
+
});
|
|
2033
|
+
writer.writeLine(";");
|
|
2034
|
+
}
|
|
2035
|
+
writer.writeLine(
|
|
2036
|
+
`const [${taskNames.map((_, i) => `r${i}`).join(", ")}] = await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
|
|
2037
|
+
);
|
|
2038
|
+
activeNodeIds.forEach((nodeId, i) => {
|
|
2039
|
+
writer.writeLine(`flowState['${nodeId}'] = r${i};`);
|
|
2040
|
+
});
|
|
2041
|
+
activeNodeIds.forEach((nodeId) => {
|
|
2042
|
+
const varName = context.symbolTable.getVarName(nodeId);
|
|
2043
|
+
writer.writeLine(`const ${varName} = flowState['${nodeId}'];`);
|
|
2044
|
+
});
|
|
2045
|
+
writer.blankLine();
|
|
2046
|
+
}
|
|
2047
|
+
function generateSingleNode(writer, node, context) {
|
|
2048
|
+
writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
|
|
2049
|
+
generateNodeBody(writer, node, context);
|
|
2050
|
+
if (node.category !== "output" /* OUTPUT */) {
|
|
2051
|
+
const varName = context.symbolTable.getVarName(node.id);
|
|
2052
|
+
writer.writeLine(`const ${varName} = flowState['${node.id}'];`);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function generateNodeBody(writer, node, context) {
|
|
2056
|
+
const plugin = context.pluginRegistry.get(node.nodeType);
|
|
2057
|
+
if (plugin) {
|
|
2058
|
+
const pluginCtx = createPluginContext(context);
|
|
2059
|
+
plugin.generate(node, writer, pluginCtx);
|
|
2060
|
+
} else {
|
|
2061
|
+
throw new Error(
|
|
2062
|
+
`[flow2code] Unsupported node type: "${node.nodeType}". Register a plugin via pluginRegistry.register() or use a built-in node type.`
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
function createPluginContext(context) {
|
|
2067
|
+
return {
|
|
2068
|
+
ir: context.ir,
|
|
2069
|
+
nodeMap: context.nodeMap,
|
|
2070
|
+
envVars: context.envVars,
|
|
2071
|
+
imports: context.imports,
|
|
2072
|
+
requiredPackages: context.requiredPackages,
|
|
2073
|
+
getVarName(nodeId) {
|
|
2074
|
+
return context.symbolTable.getVarName(nodeId);
|
|
2075
|
+
},
|
|
2076
|
+
resolveExpression(expr, currentNodeId) {
|
|
2077
|
+
const exprContext = {
|
|
2078
|
+
ir: context.ir,
|
|
2079
|
+
nodeMap: context.nodeMap,
|
|
2080
|
+
symbolTable: context.symbolTable,
|
|
2081
|
+
scopeStack: context.scopeStack.length > 0 ? [...context.scopeStack] : void 0,
|
|
2082
|
+
// Merge child block + DAG exclusion list
|
|
2083
|
+
blockScopedNodeIds: context.symbolTableExclusions.size > 0 ? context.symbolTableExclusions : void 0,
|
|
2084
|
+
currentNodeId
|
|
2085
|
+
};
|
|
2086
|
+
return parseExpression(expr, exprContext);
|
|
2087
|
+
},
|
|
2088
|
+
resolveEnvVars(url) {
|
|
2089
|
+
return resolveEnvVars(url, context);
|
|
2090
|
+
},
|
|
2091
|
+
generateChildNode(writer, node) {
|
|
2092
|
+
context.childBlockNodeIds.add(node.id);
|
|
2093
|
+
context.symbolTableExclusions.add(node.id);
|
|
2094
|
+
context.generatedBlockNodeIds.add(node.id);
|
|
2095
|
+
writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
|
|
2096
|
+
generateNodeBody(writer, node, context);
|
|
2097
|
+
generateBlockContinuation(writer, node.id, context);
|
|
2098
|
+
},
|
|
2099
|
+
pushScope(nodeId, scopeVar) {
|
|
2100
|
+
context.scopeStack.push({ nodeId, scopeVar });
|
|
2101
|
+
},
|
|
2102
|
+
popScope() {
|
|
2103
|
+
context.scopeStack.pop();
|
|
2104
|
+
},
|
|
2105
|
+
__platformResponse: context.platform.generateResponse.bind(context.platform)
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
function sanitizeId(id) {
|
|
2109
|
+
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2110
|
+
}
|
|
2111
|
+
function resolveEnvVars(url, context) {
|
|
2112
|
+
const hasEnvVar = /\$\{(\w+)\}/.test(url);
|
|
2113
|
+
if (hasEnvVar) {
|
|
2114
|
+
return "`" + url.replace(/\$\{(\w+)\}/g, (_match, varName) => {
|
|
2115
|
+
context.envVars.add(varName);
|
|
2116
|
+
return "${process.env." + varName + "}";
|
|
2117
|
+
}) + "`";
|
|
2118
|
+
}
|
|
2119
|
+
if (url.includes("${")) {
|
|
2120
|
+
return "`" + url + "`";
|
|
2121
|
+
}
|
|
2122
|
+
return `"${url}"`;
|
|
2123
|
+
}
|
|
2124
|
+
function collectRequiredPackages(ir, context) {
|
|
2125
|
+
for (const node of ir.nodes) {
|
|
2126
|
+
const plugin = context.pluginRegistry.get(node.nodeType);
|
|
2127
|
+
if (plugin?.getRequiredPackages) {
|
|
2128
|
+
const packages = plugin.getRequiredPackages(node);
|
|
2129
|
+
packages.forEach((pkg) => context.requiredPackages.add(pkg));
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
const platformDeps = context.platform.getImplicitDependencies();
|
|
2133
|
+
platformDeps.forEach((pkg) => context.requiredPackages.add(pkg));
|
|
2134
|
+
}
|
|
2135
|
+
function buildSourceMap(code, ir, filePath) {
|
|
2136
|
+
const lines = code.split("\n");
|
|
2137
|
+
const mappings = {};
|
|
2138
|
+
const nodeMarkerRegex = /^[\s]*\/\/ --- .+? \(.+?\) \[(.+?)\] ---$/;
|
|
2139
|
+
let currentNodeId = null;
|
|
2140
|
+
let currentStartLine = 0;
|
|
2141
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2142
|
+
const lineNum = i + 1;
|
|
2143
|
+
const match = lines[i].match(nodeMarkerRegex);
|
|
2144
|
+
if (match) {
|
|
2145
|
+
if (currentNodeId) {
|
|
2146
|
+
mappings[currentNodeId] = {
|
|
2147
|
+
startLine: currentStartLine,
|
|
2148
|
+
endLine: lineNum - 1
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
const [, nodeId] = match;
|
|
2152
|
+
if (ir.nodes.some((n) => n.id === nodeId)) {
|
|
2153
|
+
currentNodeId = nodeId;
|
|
2154
|
+
currentStartLine = lineNum;
|
|
2155
|
+
} else {
|
|
2156
|
+
currentNodeId = null;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (currentNodeId) {
|
|
2161
|
+
mappings[currentNodeId] = {
|
|
2162
|
+
startLine: currentStartLine,
|
|
2163
|
+
endLine: lines.length
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
const trigger = ir.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
|
|
2167
|
+
if (trigger && !mappings[trigger.id]) {
|
|
2168
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2169
|
+
if (lines[i].includes("export async function") || lines[i].includes("@schedule")) {
|
|
2170
|
+
mappings[trigger.id] = { startLine: i + 1, endLine: lines.length };
|
|
2171
|
+
break;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return {
|
|
2176
|
+
version: 1,
|
|
2177
|
+
generatedFile: filePath,
|
|
2178
|
+
mappings
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/lib/ir/security.ts
|
|
2183
|
+
var SECURITY_PATTERNS = [
|
|
2184
|
+
// ── Critical: Remote Code Execution / System Access ──
|
|
2185
|
+
{ pattern: /\beval\s*\(/, desc: "eval() \u2014 dynamically executes arbitrary code", severity: "critical" },
|
|
2186
|
+
{ pattern: /\bnew\s+Function\s*\(/, desc: "new Function() \u2014 dynamically constructs a function", severity: "critical" },
|
|
2187
|
+
{ pattern: /\bchild_process\b/, desc: "child_process \u2014 can execute arbitrary system commands", severity: "critical" },
|
|
2188
|
+
{ pattern: /\bexec\s*\(/, desc: "exec() \u2014 executes shell commands", severity: "critical" },
|
|
2189
|
+
{ pattern: /\bexecSync\s*\(/, desc: "execSync() \u2014 synchronously executes shell commands", severity: "critical" },
|
|
2190
|
+
{ pattern: /\bspawn\s*\(/, desc: "spawn() \u2014 spawns a child process", severity: "critical" },
|
|
2191
|
+
{ pattern: /\bprocess\.exit\b/, desc: "process.exit() \u2014 terminates the Node.js process", severity: "critical" },
|
|
2192
|
+
{ pattern: /\bprocess\.env\b/, desc: "process.env \u2014 accesses environment variables (may leak secrets)", severity: "critical" },
|
|
2193
|
+
{ pattern: /\bprocess\.kill\b/, desc: "process.kill() \u2014 kills a process", severity: "critical" },
|
|
2194
|
+
{ pattern: /\brequire\s*\(\s*['"`]child_process/, desc: "require('child_process')", severity: "critical" },
|
|
2195
|
+
{ pattern: /\brequire\s*\(\s*['"`]vm['"`]/, desc: "require('vm') \u2014 V8 virtual machine", severity: "critical" },
|
|
2196
|
+
// ── Critical: File System Destructive ──
|
|
2197
|
+
{ pattern: /\bfs\.\w*(unlink|rmdir|rm|rmSync|unlinkSync)\b/, desc: "fs delete operation", severity: "critical" },
|
|
2198
|
+
{ pattern: /\bfs\.\w*(writeFile|writeFileSync|appendFile)\b/, desc: "fs write operation", severity: "critical" },
|
|
2199
|
+
{ pattern: /\brequire\s*\(\s*['"`]fs['"`]\)/, desc: "require('fs') \u2014 file system access", severity: "critical" },
|
|
2200
|
+
// ── Warning: Network / Dynamic Import ──
|
|
2201
|
+
{ pattern: /\bimport\s*\(/, desc: "dynamic import() \u2014 can load arbitrary modules", severity: "warning" },
|
|
2202
|
+
{ pattern: /\brequire\s*\(\s*['"`]https?['"`]/, desc: "require('http/https') \u2014 network module", severity: "warning" },
|
|
2203
|
+
{ pattern: /\brequire\s*\(\s*['"`]net['"`]/, desc: "require('net') \u2014 low-level network access", severity: "warning" },
|
|
2204
|
+
{ pattern: /\bglobalThis\b/, desc: "globalThis \u2014 accesses the global scope", severity: "warning" },
|
|
2205
|
+
{ pattern: /\b__proto__\b/, desc: "__proto__ \u2014 prototype pollution risk", severity: "warning" },
|
|
2206
|
+
{ pattern: /\bconstructor\s*\[\s*['"`]/, desc: "constructor[] \u2014 prototype pollution risk", severity: "warning" },
|
|
2207
|
+
// ── Warning: FS Read (non-destructive but sensitive) ──
|
|
2208
|
+
{ pattern: /\brequire\s*\(\s*['"`]fs['"`]\s*\)\.read/, desc: "fs read operation", severity: "warning" },
|
|
2209
|
+
{ pattern: /\bfs\.\w*(readFile|readFileSync|readdir)\b/, desc: "fs read operation", severity: "warning" },
|
|
2210
|
+
// ── Info: Uncommon patterns ──
|
|
2211
|
+
{ pattern: /\bsetTimeout\s*\(\s*[^,]+,\s*\d{5,}/, desc: "long setTimeout (>10s) \u2014 possibly a malicious delay", severity: "info" },
|
|
2212
|
+
{ pattern: /\bwhile\s*\(\s*true\s*\)/, desc: "while(true) \u2014 possible infinite loop", severity: "info" },
|
|
2213
|
+
{ pattern: /\bfor\s*\(\s*;\s*;\s*\)/, desc: "for(;;) \u2014 possible infinite loop", severity: "info" }
|
|
2214
|
+
];
|
|
2215
|
+
function truncate(str, maxLen) {
|
|
2216
|
+
return str.length > maxLen ? str.slice(0, maxLen) + "\u2026" : str;
|
|
2217
|
+
}
|
|
2218
|
+
function scanCode(nodeId, nodeLabel, code) {
|
|
2219
|
+
const findings = [];
|
|
2220
|
+
for (const { pattern, desc, severity } of SECURITY_PATTERNS) {
|
|
2221
|
+
const globalPattern = new RegExp(pattern.source, "g");
|
|
2222
|
+
let match;
|
|
2223
|
+
while ((match = globalPattern.exec(code)) !== null) {
|
|
2224
|
+
findings.push({
|
|
2225
|
+
severity,
|
|
2226
|
+
nodeId,
|
|
2227
|
+
nodeLabel,
|
|
2228
|
+
pattern: desc,
|
|
2229
|
+
match: truncate(match[0], 80)
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
return findings;
|
|
2234
|
+
}
|
|
2235
|
+
function extractCodeFields(node) {
|
|
2236
|
+
const codes = [];
|
|
2237
|
+
const params = node.params;
|
|
2238
|
+
if (!params) return codes;
|
|
2239
|
+
if (node.nodeType === "custom_code" /* CUSTOM_CODE */ && typeof params.code === "string") {
|
|
2240
|
+
codes.push(params.code);
|
|
2241
|
+
}
|
|
2242
|
+
if (typeof params.expression === "string") {
|
|
2243
|
+
codes.push(params.expression);
|
|
2244
|
+
}
|
|
2245
|
+
if (typeof params.inputMapping === "string") {
|
|
2246
|
+
codes.push(params.inputMapping);
|
|
2247
|
+
} else if (typeof params.inputMapping === "object" && params.inputMapping !== null) {
|
|
2248
|
+
codes.push(JSON.stringify(params.inputMapping));
|
|
2249
|
+
}
|
|
2250
|
+
if (typeof params.body === "string") {
|
|
2251
|
+
codes.push(params.body);
|
|
2252
|
+
}
|
|
2253
|
+
if (typeof params.query === "string") {
|
|
2254
|
+
codes.push(params.query);
|
|
2255
|
+
}
|
|
2256
|
+
if (typeof params.bodyExpression === "string") {
|
|
2257
|
+
codes.push(params.bodyExpression);
|
|
2258
|
+
}
|
|
2259
|
+
if (typeof params.condition === "string") {
|
|
2260
|
+
codes.push(params.condition);
|
|
2261
|
+
}
|
|
2262
|
+
return codes;
|
|
2263
|
+
}
|
|
2264
|
+
function validateIRSecurity(ir) {
|
|
2265
|
+
const findings = [];
|
|
2266
|
+
let nodesScanned = 0;
|
|
2267
|
+
for (const node of ir.nodes) {
|
|
2268
|
+
const codeFields = extractCodeFields(node);
|
|
2269
|
+
if (codeFields.length === 0) continue;
|
|
2270
|
+
nodesScanned++;
|
|
2271
|
+
for (const code of codeFields) {
|
|
2272
|
+
const nodeFindings = scanCode(node.id, node.label ?? node.id, code);
|
|
2273
|
+
findings.push(...nodeFindings);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
const hasCritical = findings.some((f) => f.severity === "critical");
|
|
2277
|
+
return {
|
|
2278
|
+
safe: !hasCritical,
|
|
2279
|
+
findings,
|
|
2280
|
+
nodesScanned
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
function formatSecurityReport(result) {
|
|
2284
|
+
if (result.findings.length === 0) {
|
|
2285
|
+
return `\u2705 Security check passed (scanned ${result.nodesScanned} nodes, no dangerous patterns detected)`;
|
|
2286
|
+
}
|
|
2287
|
+
const lines = [];
|
|
2288
|
+
const critical = result.findings.filter((f) => f.severity === "critical");
|
|
2289
|
+
const warnings = result.findings.filter((f) => f.severity === "warning");
|
|
2290
|
+
const infos = result.findings.filter((f) => f.severity === "info");
|
|
2291
|
+
lines.push(`\u26A0\uFE0F Security check results (scanned ${result.nodesScanned} nodes)`);
|
|
2292
|
+
lines.push("");
|
|
2293
|
+
if (critical.length > 0) {
|
|
2294
|
+
lines.push(`\u{1F534} Critical (${critical.length}):`);
|
|
2295
|
+
for (const f of critical) {
|
|
2296
|
+
lines.push(` [${f.nodeId}] ${f.pattern} \u2014 match: "${f.match}"`);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (warnings.length > 0) {
|
|
2300
|
+
lines.push(`\u{1F7E1} Warning (${warnings.length}):`);
|
|
2301
|
+
for (const f of warnings) {
|
|
2302
|
+
lines.push(` [${f.nodeId}] ${f.pattern} \u2014 match: "${f.match}"`);
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
if (infos.length > 0) {
|
|
2306
|
+
lines.push(`\u{1F535} Info (${infos.length}):`);
|
|
2307
|
+
for (const f of infos) {
|
|
2308
|
+
lines.push(` [${f.nodeId}] ${f.pattern} \u2014 match: "${f.match}"`);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return lines.join("\n");
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// src/lib/openapi/converter.ts
|
|
2315
|
+
function convertOpenAPIToFlowIR(jsonInput) {
|
|
2316
|
+
idCounter = 0;
|
|
2317
|
+
const errors = [];
|
|
2318
|
+
let spec;
|
|
2319
|
+
try {
|
|
2320
|
+
spec = typeof jsonInput === "string" ? JSON.parse(jsonInput) : jsonInput;
|
|
2321
|
+
} catch (e) {
|
|
2322
|
+
return {
|
|
2323
|
+
success: false,
|
|
2324
|
+
flows: [],
|
|
2325
|
+
errors: [`JSON parse failed: ${e instanceof Error ? e.message : String(e)}`],
|
|
2326
|
+
summary: { totalPaths: 0, totalOperations: 0, tags: [] }
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
if (!spec.openapi || !spec.paths) {
|
|
2330
|
+
return {
|
|
2331
|
+
success: false,
|
|
2332
|
+
flows: [],
|
|
2333
|
+
errors: ["Not a valid OpenAPI spec: missing openapi or paths field"],
|
|
2334
|
+
summary: { totalPaths: 0, totalOperations: 0, tags: [] }
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
const flows = [];
|
|
2338
|
+
const allTags = /* @__PURE__ */ new Set();
|
|
2339
|
+
let totalOps = 0;
|
|
2340
|
+
const methods = ["get", "post", "put", "patch", "delete"];
|
|
2341
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
2342
|
+
for (const method of methods) {
|
|
2343
|
+
const operation = pathItem[method];
|
|
2344
|
+
if (!operation) continue;
|
|
2345
|
+
totalOps++;
|
|
2346
|
+
operation.tags?.forEach((t) => allTags.add(t));
|
|
2347
|
+
try {
|
|
2348
|
+
const flow = convertSingleOperation(
|
|
2349
|
+
path,
|
|
2350
|
+
method.toUpperCase(),
|
|
2351
|
+
operation,
|
|
2352
|
+
spec
|
|
2353
|
+
);
|
|
2354
|
+
flows.push(flow);
|
|
2355
|
+
} catch (e) {
|
|
2356
|
+
errors.push(
|
|
2357
|
+
`${method.toUpperCase()} ${path}: ${e instanceof Error ? e.message : String(e)}`
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return {
|
|
2363
|
+
success: errors.length === 0,
|
|
2364
|
+
flows,
|
|
2365
|
+
errors,
|
|
2366
|
+
summary: {
|
|
2367
|
+
totalPaths: Object.keys(spec.paths).length,
|
|
2368
|
+
totalOperations: totalOps,
|
|
2369
|
+
tags: [...allTags].sort()
|
|
2370
|
+
}
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
var idCounter = 0;
|
|
2374
|
+
function nextId(prefix) {
|
|
2375
|
+
return `${prefix}_${++idCounter}`;
|
|
2376
|
+
}
|
|
2377
|
+
function convertSingleOperation(path, method, operation, _spec) {
|
|
2378
|
+
const nodes = [];
|
|
2379
|
+
const edges = [];
|
|
2380
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2381
|
+
const isBodyMethod = ["POST", "PUT", "PATCH"].includes(method);
|
|
2382
|
+
const hasRequestBody = isBodyMethod && !!operation.requestBody;
|
|
2383
|
+
const hasQueryParams = operation.parameters?.some((p) => p.in === "query");
|
|
2384
|
+
const hasPathParams = operation.parameters?.some((p) => p.in === "path");
|
|
2385
|
+
const triggerId = nextId("trigger");
|
|
2386
|
+
const queryParamsDef = operation.parameters?.filter((p) => p.in === "query").map((p) => ({
|
|
2387
|
+
name: p.name,
|
|
2388
|
+
type: mapOpenAPIType(p.schema?.type),
|
|
2389
|
+
required: p.required ?? false
|
|
2390
|
+
}));
|
|
2391
|
+
const triggerNode = {
|
|
2392
|
+
id: triggerId,
|
|
2393
|
+
nodeType: "http_webhook" /* HTTP_WEBHOOK */,
|
|
2394
|
+
category: "trigger" /* TRIGGER */,
|
|
2395
|
+
label: `${method} ${path}`,
|
|
2396
|
+
params: {
|
|
2397
|
+
method,
|
|
2398
|
+
routePath: convertPathToNextJS(path),
|
|
2399
|
+
parseBody: hasRequestBody,
|
|
2400
|
+
...queryParamsDef && queryParamsDef.length > 0 ? { queryParams: queryParamsDef } : {}
|
|
2401
|
+
},
|
|
2402
|
+
inputs: [],
|
|
2403
|
+
outputs: buildTriggerOutputs(method, hasRequestBody, hasQueryParams)
|
|
2404
|
+
};
|
|
2405
|
+
nodes.push(triggerNode);
|
|
2406
|
+
let prevNodeId = triggerId;
|
|
2407
|
+
if (hasPathParams) {
|
|
2408
|
+
const transformId = nextId("transform");
|
|
2409
|
+
const pathParams = operation.parameters.filter((p) => p.in === "path");
|
|
2410
|
+
const transformNode = {
|
|
2411
|
+
id: transformId,
|
|
2412
|
+
nodeType: "transform",
|
|
2413
|
+
category: "variable" /* VARIABLE */,
|
|
2414
|
+
label: "Extract Path Params",
|
|
2415
|
+
params: {
|
|
2416
|
+
expression: `{ ${pathParams.map((p) => `${p.name}: {{$trigger}}.query.${p.name}`).join(", ")} }`
|
|
2417
|
+
},
|
|
2418
|
+
inputs: [
|
|
2419
|
+
{ id: "input", label: "Input", dataType: "any", required: true }
|
|
2420
|
+
],
|
|
2421
|
+
outputs: [{ id: "result", label: "Result", dataType: "object" }]
|
|
2422
|
+
};
|
|
2423
|
+
nodes.push(transformNode);
|
|
2424
|
+
edges.push({
|
|
2425
|
+
id: nextId("edge"),
|
|
2426
|
+
sourceNodeId: prevNodeId,
|
|
2427
|
+
sourcePortId: "request",
|
|
2428
|
+
targetNodeId: transformId,
|
|
2429
|
+
targetPortId: "input"
|
|
2430
|
+
});
|
|
2431
|
+
prevNodeId = transformId;
|
|
2432
|
+
}
|
|
2433
|
+
const responseId = nextId("response");
|
|
2434
|
+
const successStatus = getSuccessStatus(operation);
|
|
2435
|
+
const responseNode = {
|
|
2436
|
+
id: responseId,
|
|
2437
|
+
nodeType: "return_response" /* RETURN_RESPONSE */,
|
|
2438
|
+
category: "output" /* OUTPUT */,
|
|
2439
|
+
label: operation.summary || `Return ${successStatus}`,
|
|
2440
|
+
params: {
|
|
2441
|
+
statusCode: successStatus,
|
|
2442
|
+
bodyExpression: "{{$input}}",
|
|
2443
|
+
headers: { "Content-Type": "application/json" }
|
|
2444
|
+
},
|
|
2445
|
+
inputs: [
|
|
2446
|
+
{ id: "data", label: "Data", dataType: "any", required: true }
|
|
2447
|
+
],
|
|
2448
|
+
outputs: []
|
|
2449
|
+
};
|
|
2450
|
+
nodes.push(responseNode);
|
|
2451
|
+
edges.push({
|
|
2452
|
+
id: nextId("edge"),
|
|
2453
|
+
sourceNodeId: prevNodeId,
|
|
2454
|
+
sourcePortId: prevNodeId === triggerId ? "request" : "result",
|
|
2455
|
+
targetNodeId: responseId,
|
|
2456
|
+
targetPortId: "data"
|
|
2457
|
+
});
|
|
2458
|
+
return {
|
|
2459
|
+
version: "1.0.0",
|
|
2460
|
+
meta: {
|
|
2461
|
+
name: operation.operationId || `${method} ${path}`,
|
|
2462
|
+
description: operation.description || operation.summary || "",
|
|
2463
|
+
createdAt: now,
|
|
2464
|
+
updatedAt: now
|
|
2465
|
+
},
|
|
2466
|
+
nodes,
|
|
2467
|
+
edges
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
function buildTriggerOutputs(method, hasBody, hasQuery) {
|
|
2471
|
+
const outputs = [
|
|
2472
|
+
{ id: "request", label: "Request", dataType: "object" }
|
|
2473
|
+
];
|
|
2474
|
+
if (hasBody) {
|
|
2475
|
+
outputs.push({ id: "body", label: "Body", dataType: "object" });
|
|
2476
|
+
}
|
|
2477
|
+
if (hasQuery) {
|
|
2478
|
+
outputs.push({ id: "query", label: "Query", dataType: "object" });
|
|
2479
|
+
}
|
|
2480
|
+
return outputs;
|
|
2481
|
+
}
|
|
2482
|
+
function getSuccessStatus(operation) {
|
|
2483
|
+
if (!operation.responses) return 200;
|
|
2484
|
+
const statusCodes = Object.keys(operation.responses);
|
|
2485
|
+
const success = statusCodes.find((s) => s.startsWith("2"));
|
|
2486
|
+
return success ? parseInt(success, 10) : 200;
|
|
2487
|
+
}
|
|
2488
|
+
function convertPathToNextJS(path) {
|
|
2489
|
+
const apiPath = path.startsWith("/api") ? path : `/api${path}`;
|
|
2490
|
+
return apiPath.replace(/\{(\w+)\}/g, "[$1]");
|
|
2491
|
+
}
|
|
2492
|
+
function mapOpenAPIType(type) {
|
|
2493
|
+
switch (type) {
|
|
2494
|
+
case "integer":
|
|
2495
|
+
case "number":
|
|
2496
|
+
return "number";
|
|
2497
|
+
case "boolean":
|
|
2498
|
+
return "boolean";
|
|
2499
|
+
case "array":
|
|
2500
|
+
return "array";
|
|
2501
|
+
case "object":
|
|
2502
|
+
return "object";
|
|
2503
|
+
case "string":
|
|
2504
|
+
default:
|
|
2505
|
+
return "string";
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// src/lib/ai/prompt.ts
|
|
2510
|
+
var FLOW_IR_SYSTEM_PROMPT = `You are Flow2Code AI, a specialist in generating FlowIR JSON for a visual API builder.
|
|
2511
|
+
|
|
2512
|
+
## Your Task
|
|
2513
|
+
Given a user's natural language description of an API endpoint or workflow, generate a valid FlowIR JSON object.
|
|
2514
|
+
|
|
2515
|
+
## FlowIR Schema
|
|
2516
|
+
|
|
2517
|
+
### Top-level structure:
|
|
2518
|
+
\`\`\`json
|
|
2519
|
+
{
|
|
2520
|
+
"version": "1.0.0",
|
|
2521
|
+
"meta": {
|
|
2522
|
+
"name": "string",
|
|
2523
|
+
"description": "string (optional)",
|
|
2524
|
+
"createdAt": "ISO 8601 date string",
|
|
2525
|
+
"updatedAt": "ISO 8601 date string"
|
|
2526
|
+
},
|
|
2527
|
+
"nodes": [ ... ],
|
|
2528
|
+
"edges": [ ... ]
|
|
2529
|
+
}
|
|
2530
|
+
\`\`\`
|
|
2531
|
+
|
|
2532
|
+
### Node structure:
|
|
2533
|
+
\`\`\`json
|
|
2534
|
+
{
|
|
2535
|
+
"id": "unique_string (e.g. trigger_1, fetch_1, response_1)",
|
|
2536
|
+
"nodeType": "one of the NodeType enums below",
|
|
2537
|
+
"category": "trigger | action | logic | variable | output",
|
|
2538
|
+
"label": "human readable label",
|
|
2539
|
+
"params": { ... type-specific params ... },
|
|
2540
|
+
"inputs": [{ "id": "string", "label": "string", "dataType": "string|number|boolean|object|array|any|void|Response", "required": boolean }],
|
|
2541
|
+
"outputs": [{ "id": "string", "label": "string", "dataType": "string|number|boolean|object|array|any|void|Response" }]
|
|
2542
|
+
}
|
|
2543
|
+
\`\`\`
|
|
2544
|
+
|
|
2545
|
+
### Edge structure:
|
|
2546
|
+
\`\`\`json
|
|
2547
|
+
{
|
|
2548
|
+
"id": "unique_string (e.g. e1, e2)",
|
|
2549
|
+
"sourceNodeId": "node id",
|
|
2550
|
+
"sourcePortId": "output port id",
|
|
2551
|
+
"targetNodeId": "node id",
|
|
2552
|
+
"targetPortId": "input port id"
|
|
2553
|
+
}
|
|
2554
|
+
\`\`\`
|
|
2555
|
+
|
|
2556
|
+
### Available Node Types:
|
|
2557
|
+
|
|
2558
|
+
#### Triggers (category: "trigger") \u2014 exactly ONE required per flow:
|
|
2559
|
+
1. **http_webhook** \u2014 params: { method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", routePath: "/api/...", parseBody: boolean }
|
|
2560
|
+
- outputs: [{ id: "request", label: "Request", dataType: "object" }, { id: "body", label: "Body", dataType: "object" }, { id: "query", label: "Query", dataType: "object" }]
|
|
2561
|
+
- inputs: none
|
|
2562
|
+
|
|
2563
|
+
2. **cron_job** \u2014 params: { schedule: "cron expression", functionName: "string" }
|
|
2564
|
+
- outputs: [{ id: "output", label: "Output", dataType: "any" }]
|
|
2565
|
+
|
|
2566
|
+
3. **manual** \u2014 params: { functionName: "string", args: [{ name: "string", type: "FlowDataType" }] }
|
|
2567
|
+
- outputs: [{ id: "output", label: "Output", dataType: "any" }]
|
|
2568
|
+
|
|
2569
|
+
#### Actions (category: "action"):
|
|
2570
|
+
4. **fetch_api** \u2014 params: { url: "string (supports \${ENV_VAR})", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", headers?: {}, body?: "string", parseJson: boolean }
|
|
2571
|
+
- inputs: [{ id: "input", label: "Input", dataType: "any", required: false }]
|
|
2572
|
+
- outputs: [{ id: "response", label: "Response", dataType: "object" }, { id: "data", label: "Data", dataType: "any" }]
|
|
2573
|
+
|
|
2574
|
+
5. **sql_query** \u2014 params: { orm: "drizzle"|"prisma"|"raw", query: "SQL string", params?: [] }
|
|
2575
|
+
- inputs: [{ id: "input", label: "Input", dataType: "any", required: false }]
|
|
2576
|
+
- outputs: [{ id: "result", label: "Result", dataType: "array" }]
|
|
2577
|
+
|
|
2578
|
+
6. **redis_cache** \u2014 params: { operation: "get"|"set"|"del", key: "string", value?: "string", ttl?: number }
|
|
2579
|
+
- inputs: [{ id: "input", label: "Input", dataType: "any", required: false }]
|
|
2580
|
+
- outputs: [{ id: "value", label: "Value", dataType: "any" }]
|
|
2581
|
+
|
|
2582
|
+
7. **custom_code** \u2014 params: { code: "TypeScript code", returnVariable?: "string" }
|
|
2583
|
+
- inputs: [{ id: "input", label: "Input", dataType: "any", required: false }]
|
|
2584
|
+
- outputs: [{ id: "result", label: "Result", dataType: "any" }]
|
|
2585
|
+
|
|
2586
|
+
#### Logic (category: "logic"):
|
|
2587
|
+
8. **if_else** \u2014 params: { condition: "TypeScript expression" }
|
|
2588
|
+
- inputs: [{ id: "input", label: "Input", dataType: "any", required: true }]
|
|
2589
|
+
- outputs: [{ id: "true", label: "True", dataType: "any" }, { id: "false", label: "False", dataType: "any" }]
|
|
2590
|
+
|
|
2591
|
+
9. **for_loop** \u2014 params: { iterableExpression: "string", itemVariable: "string", indexVariable?: "string" }
|
|
2592
|
+
- inputs: [{ id: "iterable", label: "Iterable", dataType: "array", required: true }]
|
|
2593
|
+
- outputs: [{ id: "item", label: "Item", dataType: "any" }, { id: "result", label: "Result", dataType: "array" }]
|
|
2594
|
+
|
|
2595
|
+
10. **try_catch** \u2014 params: { errorVariable: "string" }
|
|
2596
|
+
- inputs: [{ id: "input", label: "Input", dataType: "any", required: true }]
|
|
2597
|
+
- outputs: [{ id: "success", label: "Success", dataType: "any" }, { id: "error", label: "Error", dataType: "object" }]
|
|
2598
|
+
|
|
2599
|
+
11. **promise_all** \u2014 params: {}
|
|
2600
|
+
- inputs: [{ id: "task1", label: "Task 1", dataType: "any", required: true }, { id: "task2", label: "Task 2", dataType: "any", required: true }]
|
|
2601
|
+
- outputs: [{ id: "results", label: "Results", dataType: "array" }]
|
|
2602
|
+
|
|
2603
|
+
#### Variables (category: "variable"):
|
|
2604
|
+
12. **declare** \u2014 params: { name: "string", dataType: "FlowDataType", initialValue?: "expression", isConst: boolean }
|
|
2605
|
+
- outputs: [{ id: "value", label: "Value", dataType: "any" }]
|
|
2606
|
+
|
|
2607
|
+
13. **transform** \u2014 params: { expression: "TypeScript expression" }
|
|
2608
|
+
- inputs: [{ id: "input", label: "Input", dataType: "any", required: true }]
|
|
2609
|
+
- outputs: [{ id: "output", label: "Output", dataType: "any" }]
|
|
2610
|
+
|
|
2611
|
+
#### Output (category: "output"):
|
|
2612
|
+
14. **return_response** \u2014 params: { statusCode: number, bodyExpression: "JS expression string", headers?: {} }
|
|
2613
|
+
- inputs: [{ id: "data", label: "Data", dataType: "any", required: true }]
|
|
2614
|
+
|
|
2615
|
+
## Variable Reference System
|
|
2616
|
+
- Nodes access previous node's output via: flowState['nodeId']
|
|
2617
|
+
- In params like condition or bodyExpression, use: flowState['nodeId'] directly
|
|
2618
|
+
- For environment variables in URLs, use: \${ENV_VAR_NAME}
|
|
2619
|
+
|
|
2620
|
+
## Rules
|
|
2621
|
+
1. There must be EXACTLY ONE trigger node
|
|
2622
|
+
2. All node IDs must be unique
|
|
2623
|
+
3. Edges must reference valid node IDs and port IDs
|
|
2624
|
+
4. No cycles allowed in the graph
|
|
2625
|
+
5. STRICT RULE: Every non-trigger node MUST be connected via an edge. For parallel execution branches, you MUST create separate edges connecting the trigger's output to EACH parallel node's input. Do NOT leave any node orphaned!
|
|
2626
|
+
6. Use descriptive labels for nodes
|
|
2627
|
+
7. Generate sensible default values
|
|
2628
|
+
8. For HTTP APIs that receive data, use parseBody: true with POST/PUT/PATCH methods
|
|
2629
|
+
9. Always end HTTP flows with a return_response node
|
|
2630
|
+
10. Use meaningful nodeId naming like "trigger_1", "fetch_users", "check_auth", "response_ok"
|
|
2631
|
+
|
|
2632
|
+
## Output
|
|
2633
|
+
Return ONLY valid JSON (no markdown, no explanation). The JSON must conform to the FlowIR schema above.
|
|
2634
|
+
`;
|
|
2635
|
+
|
|
2636
|
+
// src/server/handlers.ts
|
|
2637
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
2638
|
+
import { join, dirname, resolve } from "path";
|
|
2639
|
+
function handleCompile(body, projectRoot) {
|
|
2640
|
+
try {
|
|
2641
|
+
const ir = body.ir;
|
|
2642
|
+
const shouldWrite = body.write !== false;
|
|
2643
|
+
if (!ir) {
|
|
2644
|
+
return { status: 400, body: { success: false, error: "Missing 'ir' in request body" } };
|
|
2645
|
+
}
|
|
2646
|
+
const result = compile(ir);
|
|
2647
|
+
if (!result.success) {
|
|
2648
|
+
return {
|
|
2649
|
+
status: 400,
|
|
2650
|
+
body: { success: false, error: result.errors?.join("\n") }
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
let writtenPath = null;
|
|
2654
|
+
if (shouldWrite && result.filePath && result.code) {
|
|
2655
|
+
const fullPath = resolve(join(projectRoot, result.filePath));
|
|
2656
|
+
if (!fullPath.startsWith(resolve(projectRoot))) {
|
|
2657
|
+
return {
|
|
2658
|
+
status: 400,
|
|
2659
|
+
body: { success: false, error: "Output path escapes project root" }
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
const dir = dirname(fullPath);
|
|
2663
|
+
if (!existsSync(dir)) {
|
|
2664
|
+
mkdirSync(dir, { recursive: true });
|
|
2665
|
+
}
|
|
2666
|
+
writeFileSync(fullPath, result.code, "utf-8");
|
|
2667
|
+
writtenPath = fullPath;
|
|
2668
|
+
if (result.sourceMap) {
|
|
2669
|
+
const mapPath = fullPath.replace(/\.ts$/, ".flow.map.json");
|
|
2670
|
+
writeFileSync(mapPath, JSON.stringify(result.sourceMap, null, 2), "utf-8");
|
|
2671
|
+
}
|
|
2672
|
+
if (result.dependencies && result.dependencies.all.length > 0) {
|
|
2673
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
2674
|
+
if (existsSync(pkgPath)) {
|
|
2675
|
+
try {
|
|
2676
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2677
|
+
const installed = /* @__PURE__ */ new Set([
|
|
2678
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
2679
|
+
...Object.keys(pkg.devDependencies ?? {})
|
|
2680
|
+
]);
|
|
2681
|
+
result.dependencies.missing = result.dependencies.all.filter(
|
|
2682
|
+
(d) => !installed.has(d)
|
|
2683
|
+
);
|
|
2684
|
+
} catch {
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return {
|
|
2690
|
+
status: 200,
|
|
2691
|
+
body: {
|
|
2692
|
+
success: true,
|
|
2693
|
+
code: result.code,
|
|
2694
|
+
filePath: result.filePath,
|
|
2695
|
+
writtenTo: writtenPath,
|
|
2696
|
+
dependencies: result.dependencies,
|
|
2697
|
+
sourceMap: result.sourceMap
|
|
2698
|
+
}
|
|
2699
|
+
};
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
return {
|
|
2702
|
+
status: 500,
|
|
2703
|
+
body: {
|
|
2704
|
+
success: false,
|
|
2705
|
+
error: `Server error: ${err instanceof Error ? err.message : String(err)}`
|
|
2706
|
+
}
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
async function handleGenerate(body) {
|
|
2711
|
+
try {
|
|
2712
|
+
const { prompt } = body;
|
|
2713
|
+
if (!prompt || typeof prompt !== "string") {
|
|
2714
|
+
return { status: 400, body: { success: false, error: "Missing required 'prompt' string" } };
|
|
2715
|
+
}
|
|
2716
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
2717
|
+
if (!apiKey) {
|
|
2718
|
+
return { status: 500, body: { success: false, error: "OPENAI_API_KEY environment variable is not set" } };
|
|
2719
|
+
}
|
|
2720
|
+
const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
2721
|
+
const model = process.env.OPENAI_MODEL ?? "gpt-4o-mini";
|
|
2722
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
2723
|
+
method: "POST",
|
|
2724
|
+
headers: {
|
|
2725
|
+
"Content-Type": "application/json",
|
|
2726
|
+
Authorization: `Bearer ${apiKey}`
|
|
2727
|
+
},
|
|
2728
|
+
body: JSON.stringify({
|
|
2729
|
+
model,
|
|
2730
|
+
messages: [
|
|
2731
|
+
{ role: "system", content: FLOW_IR_SYSTEM_PROMPT },
|
|
2732
|
+
{ role: "user", content: prompt }
|
|
2733
|
+
],
|
|
2734
|
+
temperature: 0.2,
|
|
2735
|
+
response_format: { type: "json_object" }
|
|
2736
|
+
})
|
|
2737
|
+
});
|
|
2738
|
+
if (!response.ok) {
|
|
2739
|
+
const errText = await response.text();
|
|
2740
|
+
return { status: 502, body: { success: false, error: `LLM API error (${response.status}): ${errText}` } };
|
|
2741
|
+
}
|
|
2742
|
+
const data = await response.json();
|
|
2743
|
+
const content = data.choices?.[0]?.message?.content;
|
|
2744
|
+
if (!content) {
|
|
2745
|
+
return { status: 502, body: { success: false, error: "LLM returned empty content" } };
|
|
2746
|
+
}
|
|
2747
|
+
let jsonStr = content;
|
|
2748
|
+
const codeBlockMatch = content.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
2749
|
+
if (codeBlockMatch) jsonStr = codeBlockMatch[1];
|
|
2750
|
+
let ir;
|
|
2751
|
+
try {
|
|
2752
|
+
ir = JSON.parse(jsonStr);
|
|
2753
|
+
} catch {
|
|
2754
|
+
return { status: 422, body: { success: false, error: "Failed to parse LLM JSON response", raw: content } };
|
|
2755
|
+
}
|
|
2756
|
+
const triggerNode = ir.nodes.find((n) => n.category === "trigger");
|
|
2757
|
+
if (triggerNode) {
|
|
2758
|
+
const triggerOutputPortId = triggerNode.outputs?.[0]?.id || "output";
|
|
2759
|
+
const connectedTargetNodeIds = new Set(ir.edges.map((e) => e.targetNodeId));
|
|
2760
|
+
const existingEdgeIds = new Set(ir.edges.map((e) => e.id));
|
|
2761
|
+
let healedCount = 0;
|
|
2762
|
+
ir.nodes.forEach((node) => {
|
|
2763
|
+
if (node.id !== triggerNode.id && !connectedTargetNodeIds.has(node.id) && node.inputs && node.inputs.length > 0) {
|
|
2764
|
+
let edgeId;
|
|
2765
|
+
do {
|
|
2766
|
+
edgeId = `healed_e_${crypto.randomUUID().slice(0, 8)}`;
|
|
2767
|
+
} while (existingEdgeIds.has(edgeId));
|
|
2768
|
+
existingEdgeIds.add(edgeId);
|
|
2769
|
+
ir.edges.push({
|
|
2770
|
+
id: edgeId,
|
|
2771
|
+
sourceNodeId: triggerNode.id,
|
|
2772
|
+
sourcePortId: triggerNode.nodeType === "http_webhook" ? "request" : triggerOutputPortId,
|
|
2773
|
+
targetNodeId: node.id,
|
|
2774
|
+
targetPortId: node.inputs[0].id
|
|
2775
|
+
});
|
|
2776
|
+
healedCount++;
|
|
2777
|
+
}
|
|
2778
|
+
});
|
|
2779
|
+
if (healedCount > 0) {
|
|
2780
|
+
console.warn(`[AutoHeal] Connected ${healedCount} orphaned nodes to trigger.`);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
const validation = validateFlowIR(ir);
|
|
2784
|
+
if (!validation.valid) {
|
|
2785
|
+
return {
|
|
2786
|
+
status: 422,
|
|
2787
|
+
body: { success: false, error: "LLM-generated IR failed validation", validationErrors: validation.errors, raw: ir }
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
const security = validateIRSecurity(ir);
|
|
2791
|
+
const securityReport = security.findings.length > 0 ? formatSecurityReport(security) : void 0;
|
|
2792
|
+
return { status: 200, body: { success: true, ir, security: { safe: security.safe, findings: security.findings, report: securityReport } } };
|
|
2793
|
+
} catch (err) {
|
|
2794
|
+
return {
|
|
2795
|
+
status: 500,
|
|
2796
|
+
body: { success: false, error: `Server error: ${err instanceof Error ? err.message : String(err)}` }
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
function handleImportOpenAPI(body) {
|
|
2801
|
+
try {
|
|
2802
|
+
if (!body.spec) {
|
|
2803
|
+
return { status: 400, body: { error: "Missing 'spec' field in request body" } };
|
|
2804
|
+
}
|
|
2805
|
+
const result = convertOpenAPIToFlowIR(body.spec);
|
|
2806
|
+
let filteredFlows = result.flows;
|
|
2807
|
+
if (body.filter?.paths && Array.isArray(body.filter.paths)) {
|
|
2808
|
+
const paths = body.filter.paths;
|
|
2809
|
+
filteredFlows = filteredFlows.filter(
|
|
2810
|
+
(flow) => paths.some((p) => flow.meta.name.includes(p))
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2813
|
+
return {
|
|
2814
|
+
status: 200,
|
|
2815
|
+
body: {
|
|
2816
|
+
success: result.success,
|
|
2817
|
+
flows: filteredFlows,
|
|
2818
|
+
summary: result.summary,
|
|
2819
|
+
errors: result.errors
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
} catch (error) {
|
|
2823
|
+
return {
|
|
2824
|
+
status: 500,
|
|
2825
|
+
body: { error: error instanceof Error ? error.message : "Internal Server Error" }
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// src/server/index.ts
|
|
2831
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
2832
|
+
var __dirname = dirname2(__filename);
|
|
2833
|
+
function resolveStaticDir() {
|
|
2834
|
+
const candidates = [
|
|
2835
|
+
join2(__dirname, "..", "out"),
|
|
2836
|
+
// dist/server.js → ../out (npm package structure)
|
|
2837
|
+
join2(__dirname, "out"),
|
|
2838
|
+
// dist/out/
|
|
2839
|
+
join2(__dirname, "..", "..", "out"),
|
|
2840
|
+
// src/server/index.ts → ../../out (dev)
|
|
2841
|
+
join2(process.cwd(), "out")
|
|
2842
|
+
// fallback: cwd/out
|
|
2843
|
+
];
|
|
2844
|
+
for (const dir of candidates) {
|
|
2845
|
+
if (existsSync2(join2(dir, "index.html"))) {
|
|
2846
|
+
return dir;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
return candidates[0];
|
|
2850
|
+
}
|
|
2851
|
+
var MIME_TYPES = {
|
|
2852
|
+
".html": "text/html; charset=utf-8",
|
|
2853
|
+
".js": "application/javascript; charset=utf-8",
|
|
2854
|
+
".css": "text/css; charset=utf-8",
|
|
2855
|
+
".json": "application/json; charset=utf-8",
|
|
2856
|
+
".png": "image/png",
|
|
2857
|
+
".jpg": "image/jpeg",
|
|
2858
|
+
".jpeg": "image/jpeg",
|
|
2859
|
+
".gif": "image/gif",
|
|
2860
|
+
".svg": "image/svg+xml",
|
|
2861
|
+
".ico": "image/x-icon",
|
|
2862
|
+
".woff": "font/woff",
|
|
2863
|
+
".woff2": "font/woff2",
|
|
2864
|
+
".ttf": "font/ttf",
|
|
2865
|
+
".map": "application/json",
|
|
2866
|
+
".txt": "text/plain; charset=utf-8",
|
|
2867
|
+
".webp": "image/webp"
|
|
2868
|
+
};
|
|
2869
|
+
var isDev = process.env.NODE_ENV !== "production";
|
|
2870
|
+
function setCors(res) {
|
|
2871
|
+
const origin = isDev ? "*" : process.env.CORS_ORIGIN || "";
|
|
2872
|
+
if (!origin) return;
|
|
2873
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
2874
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2875
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
2876
|
+
}
|
|
2877
|
+
function setSecurityHeaders(res) {
|
|
2878
|
+
const csp = isDev ? [
|
|
2879
|
+
"default-src 'self'",
|
|
2880
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
2881
|
+
"style-src 'self' 'unsafe-inline'",
|
|
2882
|
+
"img-src 'self' data: blob:",
|
|
2883
|
+
"font-src 'self' data:",
|
|
2884
|
+
"connect-src 'self' *",
|
|
2885
|
+
"frame-ancestors 'self'",
|
|
2886
|
+
"form-action 'self'",
|
|
2887
|
+
"base-uri 'self'"
|
|
2888
|
+
].join("; ") : [
|
|
2889
|
+
"default-src 'self'",
|
|
2890
|
+
"script-src 'self'",
|
|
2891
|
+
"style-src 'self' 'unsafe-inline'",
|
|
2892
|
+
"img-src 'self' data: blob:",
|
|
2893
|
+
"font-src 'self' data:",
|
|
2894
|
+
"connect-src 'self'",
|
|
2895
|
+
"frame-ancestors 'self'",
|
|
2896
|
+
"form-action 'self'",
|
|
2897
|
+
"base-uri 'self'"
|
|
2898
|
+
].join("; ");
|
|
2899
|
+
res.setHeader("Content-Security-Policy", csp);
|
|
2900
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
2901
|
+
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
|
2902
|
+
res.setHeader("X-XSS-Protection", "1; mode=block");
|
|
2903
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
2904
|
+
}
|
|
2905
|
+
function sendJson(res, status, body) {
|
|
2906
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
2907
|
+
res.end(JSON.stringify(body));
|
|
2908
|
+
}
|
|
2909
|
+
var MAX_BODY_SIZE = 2 * 1024 * 1024;
|
|
2910
|
+
async function readBody(req) {
|
|
2911
|
+
return new Promise((resolve2, reject) => {
|
|
2912
|
+
const chunks = [];
|
|
2913
|
+
let totalSize = 0;
|
|
2914
|
+
req.on("data", (chunk) => {
|
|
2915
|
+
totalSize += chunk.length;
|
|
2916
|
+
if (totalSize > MAX_BODY_SIZE) {
|
|
2917
|
+
req.destroy();
|
|
2918
|
+
reject(new Error(`Body too large (max ${MAX_BODY_SIZE / 1024 / 1024} MB)`));
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
chunks.push(chunk);
|
|
2922
|
+
});
|
|
2923
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
|
|
2924
|
+
req.on("error", reject);
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
async function parseJsonBody(req) {
|
|
2928
|
+
const raw = await readBody(req);
|
|
2929
|
+
return JSON.parse(raw);
|
|
2930
|
+
}
|
|
2931
|
+
async function serveStatic(staticDir, pathname, res) {
|
|
2932
|
+
let filePath = join2(staticDir, pathname === "/" ? "index.html" : pathname);
|
|
2933
|
+
if (!extname(filePath)) {
|
|
2934
|
+
filePath += ".html";
|
|
2935
|
+
}
|
|
2936
|
+
try {
|
|
2937
|
+
const s = await stat(filePath);
|
|
2938
|
+
if (!s.isFile()) return false;
|
|
2939
|
+
const ext = extname(filePath).toLowerCase();
|
|
2940
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
2941
|
+
const content = await readFile(filePath);
|
|
2942
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
2943
|
+
res.end(content);
|
|
2944
|
+
return true;
|
|
2945
|
+
} catch {
|
|
2946
|
+
return false;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
async function handleRequest(req, res, staticDir, projectRoot) {
|
|
2950
|
+
setCors(res);
|
|
2951
|
+
setSecurityHeaders(res);
|
|
2952
|
+
const { method } = req;
|
|
2953
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
2954
|
+
const pathname = url.pathname;
|
|
2955
|
+
if (method === "OPTIONS") {
|
|
2956
|
+
res.writeHead(204);
|
|
2957
|
+
res.end();
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
if (pathname.startsWith("/api/")) {
|
|
2961
|
+
if (method !== "POST") {
|
|
2962
|
+
sendJson(res, 405, { error: "Method Not Allowed" });
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
let body;
|
|
2966
|
+
try {
|
|
2967
|
+
const contentType = req.headers["content-type"] || "";
|
|
2968
|
+
if (!contentType.includes("application/json")) {
|
|
2969
|
+
sendJson(res, 415, { error: "Content-Type must be application/json" });
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
body = await parseJsonBody(req);
|
|
2973
|
+
} catch (err) {
|
|
2974
|
+
const msg = err instanceof Error ? err.message : "Invalid JSON body";
|
|
2975
|
+
sendJson(res, 400, { error: msg });
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
if (pathname === "/api/compile") {
|
|
2979
|
+
const result = handleCompile(body, projectRoot);
|
|
2980
|
+
sendJson(res, result.status, result.body);
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
if (pathname === "/api/generate") {
|
|
2984
|
+
const result = await handleGenerate(body);
|
|
2985
|
+
sendJson(res, result.status, result.body);
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
if (pathname === "/api/import-openapi") {
|
|
2989
|
+
const result = handleImportOpenAPI(body);
|
|
2990
|
+
sendJson(res, result.status, result.body);
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
sendJson(res, 404, { error: `Unknown API route: ${pathname}` });
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
const served = await serveStatic(staticDir, pathname, res);
|
|
2997
|
+
if (served) return;
|
|
2998
|
+
const indexPath = join2(staticDir, "index.html");
|
|
2999
|
+
try {
|
|
3000
|
+
const content = await readFile(indexPath, "utf-8");
|
|
3001
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3002
|
+
res.end(content);
|
|
3003
|
+
} catch {
|
|
3004
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
3005
|
+
res.end("404 Not Found \u2014 UI has not been built yet, please run pnpm build:ui first");
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
function startServer(options = {}) {
|
|
3009
|
+
const port = options.port ?? (Number(process.env.PORT) || 3100);
|
|
3010
|
+
const host = options.host ?? "0.0.0.0";
|
|
3011
|
+
const staticDir = options.staticDir ?? resolveStaticDir();
|
|
3012
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
3013
|
+
const server = createServer((req, res) => {
|
|
3014
|
+
handleRequest(req, res, staticDir, projectRoot).catch((err) => {
|
|
3015
|
+
console.error("[flow2code] Internal error:", err);
|
|
3016
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
3017
|
+
res.end("Internal Server Error");
|
|
3018
|
+
});
|
|
3019
|
+
});
|
|
3020
|
+
server.listen(port, host, () => {
|
|
3021
|
+
const url = `http://localhost:${port}`;
|
|
3022
|
+
if (options.onReady) {
|
|
3023
|
+
options.onReady(url);
|
|
3024
|
+
} else {
|
|
3025
|
+
console.log(`
|
|
3026
|
+
\u{1F680} Flow2Code Dev Server`);
|
|
3027
|
+
console.log(` \u251C\u2500 Local: ${url}`);
|
|
3028
|
+
console.log(` \u251C\u2500 API: ${url}/api/compile`);
|
|
3029
|
+
console.log(` \u251C\u2500 Static: ${staticDir}`);
|
|
3030
|
+
console.log(` \u2514\u2500 Project: ${projectRoot}
|
|
3031
|
+
`);
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
return server;
|
|
3035
|
+
}
|
|
3036
|
+
export {
|
|
3037
|
+
handleCompile,
|
|
3038
|
+
handleGenerate,
|
|
3039
|
+
handleImportOpenAPI,
|
|
3040
|
+
startServer
|
|
3041
|
+
};
|
|
3042
|
+
//# sourceMappingURL=server.js.map
|