@synergenius/flow-weaver 0.10.2 → 0.10.4
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/dist/ast/types.d.ts +2 -0
- package/dist/chevrotain-parser/connect-parser.d.ts +5 -0
- package/dist/chevrotain-parser/connect-parser.js +24 -4
- package/dist/cli/flow-weaver.mjs +401 -35
- package/dist/diagram/html-viewer.d.ts +8 -0
- package/dist/diagram/html-viewer.js +170 -7
- package/dist/diagram/index.js +48 -12
- package/dist/friendly-errors.js +45 -10
- package/dist/generator/code-utils.d.ts +12 -1
- package/dist/generator/code-utils.js +88 -3
- package/dist/jsdoc-parser.js +2 -1
- package/dist/validator.js +56 -0
- package/package.json +1 -1
package/dist/cli/flow-weaver.mjs
CHANGED
|
@@ -12263,7 +12263,21 @@ var init_theme = __esm({
|
|
|
12263
12263
|
});
|
|
12264
12264
|
|
|
12265
12265
|
// src/validator.ts
|
|
12266
|
-
|
|
12266
|
+
function suggestCoerceType(targetType) {
|
|
12267
|
+
switch (targetType) {
|
|
12268
|
+
case "STRING":
|
|
12269
|
+
return "string";
|
|
12270
|
+
case "NUMBER":
|
|
12271
|
+
return "number";
|
|
12272
|
+
case "BOOLEAN":
|
|
12273
|
+
return "boolean";
|
|
12274
|
+
case "OBJECT":
|
|
12275
|
+
return "object";
|
|
12276
|
+
default:
|
|
12277
|
+
return "<type>";
|
|
12278
|
+
}
|
|
12279
|
+
}
|
|
12280
|
+
var DOCS_BASE, ERROR_DOC_URLS, COERCE_OUTPUT_TYPE, WorkflowValidator, validator;
|
|
12267
12281
|
var init_validator = __esm({
|
|
12268
12282
|
"src/validator.ts"() {
|
|
12269
12283
|
"use strict";
|
|
@@ -12284,7 +12298,17 @@ var init_validator = __esm({
|
|
|
12284
12298
|
MISSING_EXIT_CONNECTION: `${DOCS_BASE}/concepts#start-and-exit`,
|
|
12285
12299
|
INFERRED_NODE_TYPE: `${DOCS_BASE}/node-conversion`,
|
|
12286
12300
|
DUPLICATE_CONNECTION: `${DOCS_BASE}/concepts#connections`,
|
|
12287
|
-
STUB_NODE: `${DOCS_BASE}/model-driven#stub-nodes
|
|
12301
|
+
STUB_NODE: `${DOCS_BASE}/model-driven#stub-nodes`,
|
|
12302
|
+
COERCE_TYPE_MISMATCH: `${DOCS_BASE}/compilation#type-coercion`,
|
|
12303
|
+
REDUNDANT_COERCE: `${DOCS_BASE}/compilation#type-coercion`,
|
|
12304
|
+
COERCE_ON_FUNCTION_PORT: `${DOCS_BASE}/compilation#type-coercion`
|
|
12305
|
+
};
|
|
12306
|
+
COERCE_OUTPUT_TYPE = {
|
|
12307
|
+
string: "STRING",
|
|
12308
|
+
number: "NUMBER",
|
|
12309
|
+
boolean: "BOOLEAN",
|
|
12310
|
+
json: "STRING",
|
|
12311
|
+
object: "OBJECT"
|
|
12288
12312
|
};
|
|
12289
12313
|
WorkflowValidator = class {
|
|
12290
12314
|
errors = [];
|
|
@@ -12710,7 +12734,27 @@ Add '@param ${fromPort}' to the workflow JSDoc and include it in the params obje
|
|
|
12710
12734
|
if (sourceType === "STEP" && targetType === "STEP") {
|
|
12711
12735
|
return;
|
|
12712
12736
|
}
|
|
12737
|
+
if (conn.coerce && (sourceType === "FUNCTION" || targetType === "FUNCTION")) {
|
|
12738
|
+
this.errors.push({
|
|
12739
|
+
type: "error",
|
|
12740
|
+
code: "COERCE_ON_FUNCTION_PORT",
|
|
12741
|
+
message: `Coercion \`as ${conn.coerce}\` cannot be used on FUNCTION ports in connection "${fromNode}.${fromPort}" \u2192 "${toNode}.${toPort}". FUNCTION values cannot be meaningfully coerced.`,
|
|
12742
|
+
connection: conn,
|
|
12743
|
+
location: connLocation
|
|
12744
|
+
});
|
|
12745
|
+
return;
|
|
12746
|
+
}
|
|
12713
12747
|
if (sourceType === targetType) {
|
|
12748
|
+
if (conn.coerce) {
|
|
12749
|
+
this.warnings.push({
|
|
12750
|
+
type: "warning",
|
|
12751
|
+
code: "REDUNDANT_COERCE",
|
|
12752
|
+
message: `Coercion \`as ${conn.coerce}\` on connection "${fromNode}.${fromPort}" \u2192 "${toNode}.${toPort}" is redundant \u2014 source and target are both ${sourceType}.`,
|
|
12753
|
+
connection: conn,
|
|
12754
|
+
location: connLocation
|
|
12755
|
+
});
|
|
12756
|
+
return;
|
|
12757
|
+
}
|
|
12714
12758
|
if (sourceType === "OBJECT" && sourcePortDef.tsType && targetPortDef.tsType) {
|
|
12715
12759
|
const normalizedSource = this.normalizeTypeString(sourcePortDef.tsType);
|
|
12716
12760
|
const normalizedTarget = this.normalizeTypeString(targetPortDef.tsType);
|
|
@@ -12732,6 +12776,23 @@ Add '@param ${fromPort}' to the workflow JSDoc and include it in the params obje
|
|
|
12732
12776
|
if (sourceType === "ANY" || targetType === "ANY") {
|
|
12733
12777
|
return;
|
|
12734
12778
|
}
|
|
12779
|
+
if (conn.coerce) {
|
|
12780
|
+
const producedType = COERCE_OUTPUT_TYPE[conn.coerce];
|
|
12781
|
+
if (producedType === targetType) {
|
|
12782
|
+
return;
|
|
12783
|
+
}
|
|
12784
|
+
pushTypeIssue(
|
|
12785
|
+
{
|
|
12786
|
+
type: "warning",
|
|
12787
|
+
code: "COERCE_TYPE_MISMATCH",
|
|
12788
|
+
message: `Coercion \`as ${conn.coerce}\` produces ${producedType} but target port "${toPort}" on "${toNode}" expects ${targetType}. Use \`as ${suggestCoerceType(targetType)}\` instead.`,
|
|
12789
|
+
connection: conn,
|
|
12790
|
+
location: connLocation
|
|
12791
|
+
},
|
|
12792
|
+
true
|
|
12793
|
+
);
|
|
12794
|
+
return;
|
|
12795
|
+
}
|
|
12735
12796
|
const safeCoercions = [
|
|
12736
12797
|
["NUMBER", "STRING"],
|
|
12737
12798
|
["BOOLEAN", "STRING"]
|
|
@@ -28164,6 +28225,43 @@ function generateScopeFunctionClosure(scopeName, parentNodeId, parentNodeType, w
|
|
|
28164
28225
|
|
|
28165
28226
|
// src/generator/code-utils.ts
|
|
28166
28227
|
init_type_mappings();
|
|
28228
|
+
var COERCION_EXPRESSIONS = {
|
|
28229
|
+
string: "String",
|
|
28230
|
+
number: "Number",
|
|
28231
|
+
boolean: "Boolean",
|
|
28232
|
+
json: "JSON.stringify",
|
|
28233
|
+
object: "JSON.parse"
|
|
28234
|
+
};
|
|
28235
|
+
function resolveSourcePortDataType(workflow, sourceNodeId, sourcePort) {
|
|
28236
|
+
if (isStartNode(sourceNodeId)) {
|
|
28237
|
+
return workflow.startPorts?.[sourcePort]?.dataType;
|
|
28238
|
+
}
|
|
28239
|
+
if (isExitNode(sourceNodeId)) {
|
|
28240
|
+
return workflow.exitPorts?.[sourcePort]?.dataType;
|
|
28241
|
+
}
|
|
28242
|
+
const instance = workflow.instances.find((i) => i.id === sourceNodeId);
|
|
28243
|
+
if (!instance) return void 0;
|
|
28244
|
+
const nodeType = workflow.nodeTypes.find(
|
|
28245
|
+
(nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType
|
|
28246
|
+
);
|
|
28247
|
+
if (!nodeType) return void 0;
|
|
28248
|
+
return nodeType.outputs?.[sourcePort]?.dataType;
|
|
28249
|
+
}
|
|
28250
|
+
function getCoercionWrapper(connection, sourceDataType, targetDataType) {
|
|
28251
|
+
if (connection.coerce) {
|
|
28252
|
+
return COERCION_EXPRESSIONS[connection.coerce];
|
|
28253
|
+
}
|
|
28254
|
+
if (!sourceDataType || !targetDataType || sourceDataType === targetDataType) return null;
|
|
28255
|
+
if (sourceDataType === "STEP" || targetDataType === "STEP") return null;
|
|
28256
|
+
if (sourceDataType === "ANY" || targetDataType === "ANY") return null;
|
|
28257
|
+
if (targetDataType === "STRING" && sourceDataType !== "STRING") {
|
|
28258
|
+
return "String";
|
|
28259
|
+
}
|
|
28260
|
+
if (sourceDataType === "BOOLEAN" && targetDataType === "NUMBER") {
|
|
28261
|
+
return "Number";
|
|
28262
|
+
}
|
|
28263
|
+
return null;
|
|
28264
|
+
}
|
|
28167
28265
|
function toValidIdentifier(nodeId) {
|
|
28168
28266
|
let sanitized = nodeId.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
28169
28267
|
if (/^[0-9]/.test(sanitized)) {
|
|
@@ -28293,9 +28391,18 @@ function buildNodeArgumentsWithContext(opts) {
|
|
|
28293
28391
|
`${indent}const ${varName} = ${varName}_resolved.fn as ${portType};`
|
|
28294
28392
|
);
|
|
28295
28393
|
} else {
|
|
28296
|
-
|
|
28297
|
-
|
|
28298
|
-
)
|
|
28394
|
+
const sourceDataType = resolveSourcePortDataType(workflow, sourceNode, sourcePort);
|
|
28395
|
+
const coerceExpr = getCoercionWrapper(connection, sourceDataType, portConfig.dataType);
|
|
28396
|
+
const getExpr = `${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} })`;
|
|
28397
|
+
if (coerceExpr) {
|
|
28398
|
+
lines.push(
|
|
28399
|
+
`${indent}const ${varName} = ${coerceExpr}(${getExpr}) as ${portType};`
|
|
28400
|
+
);
|
|
28401
|
+
} else {
|
|
28402
|
+
lines.push(
|
|
28403
|
+
`${indent}const ${varName} = ${getExpr} as ${portType};`
|
|
28404
|
+
);
|
|
28405
|
+
}
|
|
28299
28406
|
}
|
|
28300
28407
|
} else {
|
|
28301
28408
|
const validConnections = connections.filter((conn) => {
|
|
@@ -28309,13 +28416,19 @@ function buildNodeArgumentsWithContext(opts) {
|
|
|
28309
28416
|
return;
|
|
28310
28417
|
}
|
|
28311
28418
|
const attempts = [];
|
|
28312
|
-
validConnections.forEach((conn
|
|
28419
|
+
validConnections.forEach((conn) => {
|
|
28313
28420
|
const sourceNode = conn.from.node;
|
|
28314
28421
|
const sourcePort = conn.from.port;
|
|
28315
28422
|
const sourceIdx = isStartNode(sourceNode) ? "startIdx" : `${toValidIdentifier(sourceNode)}Idx`;
|
|
28316
|
-
|
|
28317
|
-
|
|
28318
|
-
|
|
28423
|
+
const getExpr = `${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} })`;
|
|
28424
|
+
if (portConfig.dataType !== "FUNCTION") {
|
|
28425
|
+
const sourceDataType = resolveSourcePortDataType(workflow, sourceNode, sourcePort);
|
|
28426
|
+
const coerceExpr = getCoercionWrapper(conn, sourceDataType, portConfig.dataType);
|
|
28427
|
+
const wrapped = coerceExpr ? `${coerceExpr}(${getExpr})` : getExpr;
|
|
28428
|
+
attempts.push(`(${sourceIdx} !== undefined ? ${wrapped} : undefined)`);
|
|
28429
|
+
} else {
|
|
28430
|
+
attempts.push(`(${sourceIdx} !== undefined ? ${getExpr} : undefined)`);
|
|
28431
|
+
}
|
|
28319
28432
|
});
|
|
28320
28433
|
const ternary = attempts.join(" ?? ");
|
|
28321
28434
|
const portType = mapToTypeScript(portConfig.dataType, portConfig.tsType);
|
|
@@ -43545,12 +43658,16 @@ var ConnectParser = class extends CstParser {
|
|
|
43545
43658
|
super(allTokens);
|
|
43546
43659
|
this.performSelfAnalysis();
|
|
43547
43660
|
}
|
|
43548
|
-
// Entry rule for connect line
|
|
43661
|
+
// Entry rule for connect line: @connect A.port -> B.port [as type]
|
|
43549
43662
|
connectLine = this.RULE("connectLine", () => {
|
|
43550
43663
|
this.CONSUME(ConnectTag);
|
|
43551
43664
|
this.SUBRULE(this.portRef, { LABEL: "sourceRef" });
|
|
43552
43665
|
this.CONSUME(Arrow);
|
|
43553
43666
|
this.SUBRULE2(this.portRef, { LABEL: "targetRef" });
|
|
43667
|
+
this.OPTION(() => {
|
|
43668
|
+
this.CONSUME(AsKeyword);
|
|
43669
|
+
this.CONSUME(Identifier, { LABEL: "coerceType" });
|
|
43670
|
+
});
|
|
43554
43671
|
});
|
|
43555
43672
|
// node.port or node.port:scope
|
|
43556
43673
|
portRef = this.RULE("portRef", () => {
|
|
@@ -43573,7 +43690,17 @@ var ConnectVisitor = class extends BaseVisitor3 {
|
|
|
43573
43690
|
connectLine(ctx) {
|
|
43574
43691
|
const source = this.visit(ctx.sourceRef);
|
|
43575
43692
|
const target = this.visit(ctx.targetRef);
|
|
43576
|
-
|
|
43693
|
+
const result = { source, target };
|
|
43694
|
+
if (ctx.coerceType?.[0]) {
|
|
43695
|
+
const validTypes = /* @__PURE__ */ new Set(["string", "number", "boolean", "json", "object"]);
|
|
43696
|
+
const raw = ctx.coerceType[0].image;
|
|
43697
|
+
if (validTypes.has(raw)) {
|
|
43698
|
+
result.coerce = raw;
|
|
43699
|
+
} else {
|
|
43700
|
+
result.invalidCoerceType = raw;
|
|
43701
|
+
}
|
|
43702
|
+
}
|
|
43703
|
+
return result;
|
|
43577
43704
|
}
|
|
43578
43705
|
portRef(ctx) {
|
|
43579
43706
|
const nodeId = ctx.nodeId[0].image;
|
|
@@ -43607,7 +43734,14 @@ function parseConnectLine(input, warnings) {
|
|
|
43607
43734
|
);
|
|
43608
43735
|
return null;
|
|
43609
43736
|
}
|
|
43610
|
-
|
|
43737
|
+
const result = visitorInstance3.visit(cst);
|
|
43738
|
+
if (result.invalidCoerceType) {
|
|
43739
|
+
warnings.push(
|
|
43740
|
+
`Invalid coerce type "${result.invalidCoerceType}" in @connect. Valid types: string, number, boolean, json, object`
|
|
43741
|
+
);
|
|
43742
|
+
delete result.invalidCoerceType;
|
|
43743
|
+
}
|
|
43744
|
+
return result;
|
|
43611
43745
|
}
|
|
43612
43746
|
function getConnectGrammar() {
|
|
43613
43747
|
return parserInstance3.getSerializedGastProductions();
|
|
@@ -45307,7 +45441,7 @@ var JSDocParser = class {
|
|
|
45307
45441
|
warnings.push(`Invalid @connect tag format: @connect ${comment}`);
|
|
45308
45442
|
return;
|
|
45309
45443
|
}
|
|
45310
|
-
const { source, target } = result;
|
|
45444
|
+
const { source, target, coerce: coerce2 } = result;
|
|
45311
45445
|
const line = tag.getStartLineNumber();
|
|
45312
45446
|
config2.connections.push({
|
|
45313
45447
|
from: {
|
|
@@ -45320,7 +45454,8 @@ var JSDocParser = class {
|
|
|
45320
45454
|
port: target.portName,
|
|
45321
45455
|
...target.scope && { scope: target.scope }
|
|
45322
45456
|
},
|
|
45323
|
-
sourceLocation: { line, column: 0 }
|
|
45457
|
+
sourceLocation: { line, column: 0 },
|
|
45458
|
+
...coerce2 && { coerce: coerce2 }
|
|
45324
45459
|
});
|
|
45325
45460
|
}
|
|
45326
45461
|
/**
|
|
@@ -53839,7 +53974,7 @@ function buildCoerceSuggestion(quoted, targetType) {
|
|
|
53839
53974
|
if (portRefs.length < 2) return null;
|
|
53840
53975
|
const coerceType = COERCE_TARGET_TYPES[targetType.toUpperCase()] || targetType.toLowerCase();
|
|
53841
53976
|
if (!["string", "number", "boolean", "json", "object"].includes(coerceType)) return null;
|
|
53842
|
-
return `@
|
|
53977
|
+
return `@connect ${portRefs[0]} -> ${portRefs[1]} as ${coerceType}`;
|
|
53843
53978
|
}
|
|
53844
53979
|
function extractCyclePath(message) {
|
|
53845
53980
|
const match2 = message.match(/:\s*(.+ -> .+)/);
|
|
@@ -53948,7 +54083,7 @@ var errorMappers = {
|
|
|
53948
54083
|
return {
|
|
53949
54084
|
title: "Type Mismatch",
|
|
53950
54085
|
explanation: `Type mismatch: you're connecting a ${source} to a ${target}. The value will be automatically converted, but this might cause unexpected behavior.`,
|
|
53951
|
-
fix: coerceSuggestion ? `
|
|
54086
|
+
fix: coerceSuggestion ? `Use \`${coerceSuggestion}\` for explicit coercion, change one of the port types, or use @strictTypes to turn this into an error.` : `Add \`as <type>\` to the @connect annotation (e.g. \`as string\`), change one of the port types, or use @strictTypes to turn this into an error.`,
|
|
53952
54087
|
code: error2.code
|
|
53953
54088
|
};
|
|
53954
54089
|
},
|
|
@@ -54047,7 +54182,7 @@ var errorMappers = {
|
|
|
54047
54182
|
return {
|
|
54048
54183
|
title: "Type Incompatible",
|
|
54049
54184
|
explanation: `Type mismatch: ${source} to ${target}. With @strictTypes enabled, this is an error instead of a warning.`,
|
|
54050
|
-
fix: coerceSuggestion ? `
|
|
54185
|
+
fix: coerceSuggestion ? `Use \`${coerceSuggestion}\` for explicit coercion, change one of the port types, or remove @strictTypes to allow implicit coercions.` : `Add \`as <type>\` to the @connect annotation (e.g. \`as number\`), change one of the port types, or remove @strictTypes to allow implicit coercions.`,
|
|
54051
54186
|
code: error2.code
|
|
54052
54187
|
};
|
|
54053
54188
|
},
|
|
@@ -54060,7 +54195,7 @@ var errorMappers = {
|
|
|
54060
54195
|
return {
|
|
54061
54196
|
title: "Unusual Type Coercion",
|
|
54062
54197
|
explanation: `Converting ${source} to ${target} is technically valid but semantically unusual and may produce unexpected behavior.`,
|
|
54063
|
-
fix: coerceSuggestion ? `If intentional,
|
|
54198
|
+
fix: coerceSuggestion ? `If intentional, use \`${coerceSuggestion}\` for explicit coercion, or use @strictTypes to enforce type safety.` : `If intentional, add \`as <type>\` to the @connect annotation, or use @strictTypes to enforce type safety.`,
|
|
54064
54199
|
code: error2.code
|
|
54065
54200
|
};
|
|
54066
54201
|
},
|
|
@@ -54285,6 +54420,41 @@ var errorMappers = {
|
|
|
54285
54420
|
code: error2.code
|
|
54286
54421
|
};
|
|
54287
54422
|
},
|
|
54423
|
+
COERCE_TYPE_MISMATCH(error2) {
|
|
54424
|
+
const coerceMatch = error2.message.match(/`as (\w+)`/);
|
|
54425
|
+
const coerceType = coerceMatch?.[1] || "unknown";
|
|
54426
|
+
const expectsMatch = error2.message.match(/expects (\w+)/);
|
|
54427
|
+
const expectedType = expectsMatch?.[1] || "unknown";
|
|
54428
|
+
const suggestedType = COERCE_TARGET_TYPES[expectedType.toUpperCase()] || expectedType.toLowerCase();
|
|
54429
|
+
return {
|
|
54430
|
+
title: "Wrong Coercion Type",
|
|
54431
|
+
explanation: `The \`as ${coerceType}\` coercion produces the wrong type for the target port. The target expects ${expectedType}.`,
|
|
54432
|
+
fix: `Change \`as ${coerceType}\` to \`as ${suggestedType}\` in the @connect annotation.`,
|
|
54433
|
+
code: error2.code
|
|
54434
|
+
};
|
|
54435
|
+
},
|
|
54436
|
+
REDUNDANT_COERCE(error2) {
|
|
54437
|
+
const coerceMatch = error2.message.match(/`as (\w+)`/);
|
|
54438
|
+
const coerceType = coerceMatch?.[1] || "unknown";
|
|
54439
|
+
const bothMatch = error2.message.match(/both (\w+)/);
|
|
54440
|
+
const dataType = bothMatch?.[1] || "the same type";
|
|
54441
|
+
return {
|
|
54442
|
+
title: "Redundant Coercion",
|
|
54443
|
+
explanation: `The \`as ${coerceType}\` coercion is unnecessary because both the source and target ports are already ${dataType}.`,
|
|
54444
|
+
fix: `Remove \`as ${coerceType}\` from the @connect annotation \u2014 no coercion is needed.`,
|
|
54445
|
+
code: error2.code
|
|
54446
|
+
};
|
|
54447
|
+
},
|
|
54448
|
+
COERCE_ON_FUNCTION_PORT(error2) {
|
|
54449
|
+
const coerceMatch = error2.message.match(/`as (\w+)`/);
|
|
54450
|
+
const coerceType = coerceMatch?.[1] || "unknown";
|
|
54451
|
+
return {
|
|
54452
|
+
title: "Coercion on Function Port",
|
|
54453
|
+
explanation: `The \`as ${coerceType}\` coercion cannot be applied to FUNCTION ports. Function values are callable references and cannot be meaningfully converted to other types.`,
|
|
54454
|
+
fix: `Remove \`as ${coerceType}\` from the @connect annotation. If you need to convert the function's return value, add a transformation node.`,
|
|
54455
|
+
code: error2.code
|
|
54456
|
+
};
|
|
54457
|
+
},
|
|
54288
54458
|
LOSSY_TYPE_COERCION(error2) {
|
|
54289
54459
|
const types2 = extractTypes(error2.message);
|
|
54290
54460
|
const source = types2?.source || "unknown";
|
|
@@ -54294,7 +54464,7 @@ var errorMappers = {
|
|
|
54294
54464
|
return {
|
|
54295
54465
|
title: "Lossy Type Conversion",
|
|
54296
54466
|
explanation: `Converting ${source} to ${target} may lose data or produce unexpected results (e.g., NaN, truncation).`,
|
|
54297
|
-
fix: coerceSuggestion ? `
|
|
54467
|
+
fix: coerceSuggestion ? `Use \`${coerceSuggestion}\` for explicit coercion, or use @strictTypes to enforce type safety.` : `Add \`as <type>\` to the @connect annotation for explicit conversion, or use @strictTypes to enforce type safety.`,
|
|
54298
54468
|
code: error2.code
|
|
54299
54469
|
};
|
|
54300
54470
|
},
|
|
@@ -61418,24 +61588,46 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61418
61588
|
/* Info panel */
|
|
61419
61589
|
#info-panel {
|
|
61420
61590
|
position: fixed; bottom: 52px; left: 16px;
|
|
61421
|
-
max-width:
|
|
61591
|
+
max-width: 480px; min-width: 260px;
|
|
61422
61592
|
background: ${surfaceMain}; border: 1px solid ${borderSubtle};
|
|
61423
61593
|
border-radius: 8px; padding: 12px 16px;
|
|
61424
61594
|
font-size: 13px; line-height: 1.5;
|
|
61425
61595
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
61426
61596
|
z-index: 10; display: none;
|
|
61597
|
+
max-height: calc(100vh - 120px); overflow-y: auto;
|
|
61427
61598
|
}
|
|
61428
61599
|
#info-panel.visible { display: block; }
|
|
61429
61600
|
#info-panel h3 {
|
|
61430
61601
|
font-size: 14px; font-weight: 700; margin-bottom: 6px;
|
|
61431
61602
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
61432
61603
|
}
|
|
61604
|
+
#info-panel .node-desc { color: ${textMed}; font-size: 12px; margin-bottom: 8px; font-style: italic; }
|
|
61433
61605
|
#info-panel .info-section { margin-bottom: 6px; }
|
|
61434
61606
|
#info-panel .info-label { font-size: 11px; font-weight: 600; color: ${textLow}; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
61435
61607
|
#info-panel .info-value { color: ${textMed}; }
|
|
61436
61608
|
#info-panel .port-list { list-style: none; padding: 0; }
|
|
61437
61609
|
#info-panel .port-list li { padding: 1px 0; }
|
|
61438
61610
|
#info-panel .port-list li::before { content: '\\2022'; margin-right: 6px; color: ${textLow}; }
|
|
61611
|
+
#info-panel .port-type { color: ${textLow}; font-size: 11px; margin-left: 4px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; }
|
|
61612
|
+
#info-panel pre {
|
|
61613
|
+
background: ${isDark ? "#161625" : "#f0f1fa"}; border: 1px solid ${borderSubtle};
|
|
61614
|
+
border-radius: 6px; padding: 10px; overflow-x: auto;
|
|
61615
|
+
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
61616
|
+
font-size: 12px; line-height: 1.6; white-space: pre;
|
|
61617
|
+
max-height: 300px; overflow-y: auto; margin: 6px 0 0;
|
|
61618
|
+
color: ${isDark ? "#e6edf3" : "#1a2340"};
|
|
61619
|
+
}
|
|
61620
|
+
.hl-kw { color: ${isDark ? "#8e9eff" : "#4040bf"}; }
|
|
61621
|
+
.hl-str { color: ${isDark ? "#ff7b72" : "#c4432b"}; }
|
|
61622
|
+
.hl-num { color: ${isDark ? "#f0a050" : "#b35e14"}; }
|
|
61623
|
+
.hl-cm { color: #6a737d; font-style: italic; }
|
|
61624
|
+
.hl-fn { color: ${isDark ? "#d2a8ff" : "#7c3aed"}; }
|
|
61625
|
+
.hl-ty { color: ${isDark ? "#d2a8ff" : "#7c3aed"}; }
|
|
61626
|
+
.hl-pn { color: ${isDark ? "#b8bdd0" : "#4a5578"}; }
|
|
61627
|
+
.hl-ann { color: ${isDark ? "#8e9eff" : "#4040bf"}; font-weight: 600; font-style: normal; }
|
|
61628
|
+
.hl-arr { color: ${isDark ? "#79c0ff" : "#0969da"}; font-weight: 600; font-style: normal; }
|
|
61629
|
+
.hl-id { color: ${isDark ? "#e6edf3" : "#1a2340"}; font-style: normal; }
|
|
61630
|
+
.hl-sc { color: ${isDark ? "#d2a8ff" : "#7c3aed"}; font-style: italic; }
|
|
61439
61631
|
|
|
61440
61632
|
/* Branding badge */
|
|
61441
61633
|
#branding {
|
|
@@ -61490,8 +61682,8 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61490
61682
|
<circle cx="10" cy="10" r="1.5" fill="${dotColor}" opacity="0.6"/>
|
|
61491
61683
|
</pattern>
|
|
61492
61684
|
</defs>
|
|
61493
|
-
<rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}"/>
|
|
61494
|
-
<rect x="-100000" y="-100000" width="200000" height="200000" fill="url(#viewer-dots)"/>
|
|
61685
|
+
<rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}" pointer-events="none"/>
|
|
61686
|
+
<rect x="-100000" y="-100000" width="200000" height="200000" fill="url(#viewer-dots)" pointer-events="none"/>
|
|
61495
61687
|
<g id="diagram">${inner}</g>
|
|
61496
61688
|
</svg>
|
|
61497
61689
|
<div id="controls">
|
|
@@ -61521,6 +61713,7 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61521
61713
|
</a>
|
|
61522
61714
|
<div id="scroll-hint">Use <kbd id="mod-key">Ctrl</kbd> + scroll to zoom</div>
|
|
61523
61715
|
<div id="studio-hint">Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.</div>
|
|
61716
|
+
<script>var nodeSources = ${JSON.stringify(options.nodeSources ?? {})};</script>
|
|
61524
61717
|
<script>
|
|
61525
61718
|
(function() {
|
|
61526
61719
|
'use strict';
|
|
@@ -61605,6 +61798,7 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61605
61798
|
|
|
61606
61799
|
// ---- Pan (drag) + Node drag ----
|
|
61607
61800
|
var draggedNodeId = null, dragNodeStart = null, didDragNode = false;
|
|
61801
|
+
var clickTarget = null; // stash the real target before setPointerCapture steals it
|
|
61608
61802
|
var dragCount = 0, nudgeIndex = 0, nudgeTimer = null;
|
|
61609
61803
|
var nudgeMessages = [
|
|
61610
61804
|
'Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.',
|
|
@@ -61619,6 +61813,8 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61619
61813
|
|
|
61620
61814
|
canvas.addEventListener('pointerdown', function(e) {
|
|
61621
61815
|
if (e.button !== 0) return;
|
|
61816
|
+
clickTarget = e.target; // stash before setPointerCapture redirects events
|
|
61817
|
+
didDrag = false;
|
|
61622
61818
|
// Check if clicking on a node body (walk up to detect data-node-id)
|
|
61623
61819
|
var t = e.target;
|
|
61624
61820
|
while (t && t !== canvas) {
|
|
@@ -61635,7 +61831,6 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61635
61831
|
}
|
|
61636
61832
|
// Canvas pan
|
|
61637
61833
|
pointerDown = true;
|
|
61638
|
-
didDrag = false;
|
|
61639
61834
|
dragLast = { x: e.clientX, y: e.clientY };
|
|
61640
61835
|
canvas.setPointerCapture(e.pointerId);
|
|
61641
61836
|
});
|
|
@@ -62088,14 +62283,28 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
62088
62283
|
// Build info panel
|
|
62089
62284
|
infoTitle.textContent = labelText;
|
|
62090
62285
|
var html = '';
|
|
62286
|
+
var src = nodeSources[nodeId];
|
|
62287
|
+
if (src && src.description) {
|
|
62288
|
+
html += '<div class="node-desc">' + escapeH(src.description) + '</div>';
|
|
62289
|
+
}
|
|
62290
|
+
var portInfo = (src && src.ports) ? src.ports : {};
|
|
62291
|
+
function portLabel(name) {
|
|
62292
|
+
var p = portInfo[name];
|
|
62293
|
+
var label = escapeH(name);
|
|
62294
|
+
if (p) {
|
|
62295
|
+
var typeStr = p.tsType || p.type;
|
|
62296
|
+
if (typeStr) label += ' <span class="port-type">' + escapeH(typeStr) + '</span>';
|
|
62297
|
+
}
|
|
62298
|
+
return label;
|
|
62299
|
+
}
|
|
62091
62300
|
if (inputs.length) {
|
|
62092
62301
|
html += '<div class="info-section"><div class="info-label">Inputs</div><ul class="port-list">';
|
|
62093
|
-
inputs.forEach(function(n) { html += '<li>' +
|
|
62302
|
+
inputs.forEach(function(n) { html += '<li>' + portLabel(n) + '</li>'; });
|
|
62094
62303
|
html += '</ul></div>';
|
|
62095
62304
|
}
|
|
62096
62305
|
if (outputs.length) {
|
|
62097
62306
|
html += '<div class="info-section"><div class="info-label">Outputs</div><ul class="port-list">';
|
|
62098
|
-
outputs.forEach(function(n) { html += '<li>' +
|
|
62307
|
+
outputs.forEach(function(n) { html += '<li>' + portLabel(n) + '</li>'; });
|
|
62099
62308
|
html += '</ul></div>';
|
|
62100
62309
|
}
|
|
62101
62310
|
if (connectedNodes.size) {
|
|
@@ -62103,6 +62312,10 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
62103
62312
|
html += Array.from(connectedNodes).map(escapeH).join(', ');
|
|
62104
62313
|
html += '</div></div>';
|
|
62105
62314
|
}
|
|
62315
|
+
if (src && src.source) {
|
|
62316
|
+
html += '<div class="info-section"><div class="info-label">Source</div>';
|
|
62317
|
+
html += '<pre><code>' + highlightTS(src.source) + '</code></pre></div>';
|
|
62318
|
+
}
|
|
62106
62319
|
infoBody.innerHTML = html;
|
|
62107
62320
|
infoPanel.classList.add('visible');
|
|
62108
62321
|
}
|
|
@@ -62111,10 +62324,130 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
62111
62324
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
62112
62325
|
}
|
|
62113
62326
|
|
|
62327
|
+
var fwAnnotations = 'flowWeaver,input,output,step,node,connect,param,returns,fwImport,label,scope,position,color,icon,tag,map,path,name,description,expression,executeWhen,pullExecution,strictTypes,autoConnect,port,trigger,cancelOn,retries,timeout,throttle';
|
|
62328
|
+
|
|
62329
|
+
function highlightJSDoc(block) {
|
|
62330
|
+
var annSet = fwAnnotations.split(',');
|
|
62331
|
+
var out = '';
|
|
62332
|
+
var re = /(@[a-zA-Z]+)|(->)|(\\.[a-zA-Z_][a-zA-Z0-9_]*)|("[^"]*")|('\\''[^'\\'']*'\\'')|(-?[0-9]+(?:\\.[0-9]+)?)|([a-zA-Z_][a-zA-Z0-9_]*)|([^@a-zA-Z0-9"'\\-.]+)/g;
|
|
62333
|
+
var m;
|
|
62334
|
+
while ((m = re.exec(block)) !== null) {
|
|
62335
|
+
if (m[1]) {
|
|
62336
|
+
var tag = m[1].slice(1);
|
|
62337
|
+
if (annSet.indexOf(tag) >= 0) {
|
|
62338
|
+
out += '<span class="hl-ann">' + m[0] + '</span>';
|
|
62339
|
+
} else {
|
|
62340
|
+
out += '<span class="hl-ann">' + m[0] + '</span>';
|
|
62341
|
+
}
|
|
62342
|
+
} else if (m[2]) {
|
|
62343
|
+
out += '<span class="hl-arr">' + m[2] + '</span>';
|
|
62344
|
+
} else if (m[3]) {
|
|
62345
|
+
// .portName scope reference
|
|
62346
|
+
out += '<span class="hl-sc">' + m[3] + '</span>';
|
|
62347
|
+
} else if (m[4] || m[5]) {
|
|
62348
|
+
out += '<span class="hl-str">' + (m[4] || m[5]) + '</span>';
|
|
62349
|
+
} else if (m[6]) {
|
|
62350
|
+
out += '<span class="hl-num">' + m[6] + '</span>';
|
|
62351
|
+
} else if (m[7]) {
|
|
62352
|
+
var tys = 'string,number,boolean,any,void,never,unknown,STEP,STRING,NUMBER,BOOLEAN,ARRAY,OBJECT,FUNCTION,ANY';
|
|
62353
|
+
if (tys.split(',').indexOf(m[7]) >= 0) {
|
|
62354
|
+
out += '<span class="hl-ty">' + m[7] + '</span>';
|
|
62355
|
+
} else {
|
|
62356
|
+
out += '<span class="hl-id">' + m[7] + '</span>';
|
|
62357
|
+
}
|
|
62358
|
+
} else {
|
|
62359
|
+
out += m[0];
|
|
62360
|
+
}
|
|
62361
|
+
}
|
|
62362
|
+
return out;
|
|
62363
|
+
}
|
|
62364
|
+
|
|
62365
|
+
function highlightTS(code) {
|
|
62366
|
+
var tokens = [];
|
|
62367
|
+
var i = 0;
|
|
62368
|
+
while (i < code.length) {
|
|
62369
|
+
// Line comments
|
|
62370
|
+
if (code[i] === '/' && code[i+1] === '/') {
|
|
62371
|
+
var end = code.indexOf('\\n', i);
|
|
62372
|
+
if (end === -1) end = code.length;
|
|
62373
|
+
tokens.push({ t: 'cm', v: code.slice(i, end) });
|
|
62374
|
+
i = end;
|
|
62375
|
+
continue;
|
|
62376
|
+
}
|
|
62377
|
+
// Block comments (detect JSDoc for annotation highlighting)
|
|
62378
|
+
if (code[i] === '/' && code[i+1] === '*') {
|
|
62379
|
+
var end = code.indexOf('*/', i + 2);
|
|
62380
|
+
if (end === -1) end = code.length; else end += 2;
|
|
62381
|
+
var block = code.slice(i, end);
|
|
62382
|
+
var hasFW = /@(flowWeaver|input|output|step|node|connect|param|returns)/.test(block);
|
|
62383
|
+
if (hasFW) {
|
|
62384
|
+
tokens.push({ t: 'jsdoc', v: block });
|
|
62385
|
+
} else {
|
|
62386
|
+
tokens.push({ t: 'cm', v: block });
|
|
62387
|
+
}
|
|
62388
|
+
i = end;
|
|
62389
|
+
continue;
|
|
62390
|
+
}
|
|
62391
|
+
// Strings
|
|
62392
|
+
if (code[i] === "'" || code[i] === '"' || code[i] === '\`') {
|
|
62393
|
+
var q = code[i], j = i + 1;
|
|
62394
|
+
while (j < code.length && code[j] !== q) { if (code[j] === '\\\\') j++; j++; }
|
|
62395
|
+
tokens.push({ t: 'str', v: code.slice(i, j + 1) });
|
|
62396
|
+
i = j + 1;
|
|
62397
|
+
continue;
|
|
62398
|
+
}
|
|
62399
|
+
// Numbers
|
|
62400
|
+
if (/[0-9]/.test(code[i]) && (i === 0 || /[^a-zA-Z_$]/.test(code[i-1]))) {
|
|
62401
|
+
var j = i;
|
|
62402
|
+
while (j < code.length && /[0-9a-fA-FxX._]/.test(code[j])) j++;
|
|
62403
|
+
tokens.push({ t: 'num', v: code.slice(i, j) });
|
|
62404
|
+
i = j;
|
|
62405
|
+
continue;
|
|
62406
|
+
}
|
|
62407
|
+
// Words
|
|
62408
|
+
if (/[a-zA-Z_$]/.test(code[i])) {
|
|
62409
|
+
var j = i;
|
|
62410
|
+
while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
|
|
62411
|
+
var w = code.slice(i, j);
|
|
62412
|
+
var kws = 'async,await,break,case,catch,class,const,continue,default,delete,do,else,export,extends,finally,for,from,function,if,import,in,instanceof,let,new,of,return,switch,throw,try,typeof,var,void,while,yield';
|
|
62413
|
+
var tys = 'string,number,boolean,any,void,never,unknown,null,undefined,true,false,Promise,Record,Map,Set,Array,Partial,Required,Omit,Pick';
|
|
62414
|
+
if (kws.split(',').indexOf(w) >= 0) {
|
|
62415
|
+
tokens.push({ t: 'kw', v: w });
|
|
62416
|
+
} else if (tys.split(',').indexOf(w) >= 0) {
|
|
62417
|
+
tokens.push({ t: 'ty', v: w });
|
|
62418
|
+
} else if (j < code.length && code[j] === '(') {
|
|
62419
|
+
tokens.push({ t: 'fn', v: w });
|
|
62420
|
+
} else {
|
|
62421
|
+
tokens.push({ t: '', v: w });
|
|
62422
|
+
}
|
|
62423
|
+
i = j;
|
|
62424
|
+
continue;
|
|
62425
|
+
}
|
|
62426
|
+
// Punctuation
|
|
62427
|
+
if (/[{}()\\[\\];:.,<>=!&|?+\\-*/%^~@]/.test(code[i])) {
|
|
62428
|
+
tokens.push({ t: 'pn', v: code[i] });
|
|
62429
|
+
i++;
|
|
62430
|
+
continue;
|
|
62431
|
+
}
|
|
62432
|
+
// Whitespace and other
|
|
62433
|
+
tokens.push({ t: '', v: code[i] });
|
|
62434
|
+
i++;
|
|
62435
|
+
}
|
|
62436
|
+
return tokens.map(function(tk) {
|
|
62437
|
+
if (tk.t === 'jsdoc') {
|
|
62438
|
+
return '<span class="hl-cm">' + highlightJSDoc(escapeH(tk.v)) + '</span>';
|
|
62439
|
+
}
|
|
62440
|
+
var v = escapeH(tk.v);
|
|
62441
|
+
return tk.t ? '<span class="hl-' + tk.t + '">' + v + '</span>' : v;
|
|
62442
|
+
}).join('');
|
|
62443
|
+
}
|
|
62444
|
+
|
|
62114
62445
|
// Delegate click: port click > node click > background
|
|
62446
|
+
// Use clickTarget (stashed from pointerdown) because setPointerCapture redirects click to canvas
|
|
62115
62447
|
canvas.addEventListener('click', function(e) {
|
|
62116
62448
|
if (didDrag || didDragNode) { didDragNode = false; return; }
|
|
62117
|
-
var target = e.target;
|
|
62449
|
+
var target = clickTarget || e.target;
|
|
62450
|
+
clickTarget = null;
|
|
62118
62451
|
while (target && target !== canvas) {
|
|
62119
62452
|
if (target.hasAttribute && target.hasAttribute('data-port-id')) {
|
|
62120
62453
|
e.stopPropagation();
|
|
@@ -62155,24 +62488,57 @@ function fileToSVG(filePath, options = {}) {
|
|
|
62155
62488
|
return pickAndRender(result.workflows, options);
|
|
62156
62489
|
}
|
|
62157
62490
|
function fileToHTML(filePath, options = {}) {
|
|
62158
|
-
const
|
|
62159
|
-
|
|
62491
|
+
const result = parser.parse(filePath);
|
|
62492
|
+
const ast = pickWorkflow(result.workflows, options);
|
|
62493
|
+
const svg = workflowToSVG(ast, options);
|
|
62494
|
+
return wrapSVGInHTML(svg, { title: options.workflowName ?? ast.name, theme: options.theme, nodeSources: buildNodeSourceMap(ast) });
|
|
62160
62495
|
}
|
|
62161
|
-
function
|
|
62496
|
+
function buildNodeSourceMap(ast) {
|
|
62497
|
+
const typeMap = new Map(ast.nodeTypes.map((nt) => [nt.functionName, nt]));
|
|
62498
|
+
const map3 = {};
|
|
62499
|
+
for (const inst of ast.instances) {
|
|
62500
|
+
const nt = typeMap.get(inst.nodeType);
|
|
62501
|
+
if (!nt) continue;
|
|
62502
|
+
const ports = {};
|
|
62503
|
+
for (const [name, def] of Object.entries(nt.inputs ?? {})) {
|
|
62504
|
+
ports[name] = { type: def.dataType, tsType: def.tsType };
|
|
62505
|
+
}
|
|
62506
|
+
for (const [name, def] of Object.entries(nt.outputs ?? {})) {
|
|
62507
|
+
ports[name] = { type: def.dataType, tsType: def.tsType };
|
|
62508
|
+
}
|
|
62509
|
+
map3[inst.id] = { description: nt.description, source: nt.functionText, ports };
|
|
62510
|
+
}
|
|
62511
|
+
const startPorts = {};
|
|
62512
|
+
for (const [name, def] of Object.entries(ast.startPorts ?? {})) {
|
|
62513
|
+
startPorts[name] = { type: def.dataType, tsType: def.tsType };
|
|
62514
|
+
}
|
|
62515
|
+
if (Object.keys(startPorts).length) {
|
|
62516
|
+
map3["Start"] = { description: ast.description, ports: startPorts };
|
|
62517
|
+
}
|
|
62518
|
+
const exitPorts = {};
|
|
62519
|
+
for (const [name, def] of Object.entries(ast.exitPorts ?? {})) {
|
|
62520
|
+
exitPorts[name] = { type: def.dataType, tsType: def.tsType };
|
|
62521
|
+
}
|
|
62522
|
+
if (Object.keys(exitPorts).length) {
|
|
62523
|
+
map3["Exit"] = { ports: exitPorts };
|
|
62524
|
+
}
|
|
62525
|
+
return map3;
|
|
62526
|
+
}
|
|
62527
|
+
function pickWorkflow(workflows, options) {
|
|
62162
62528
|
if (workflows.length === 0) {
|
|
62163
62529
|
throw new Error("No workflows found in source code");
|
|
62164
62530
|
}
|
|
62165
|
-
let workflow;
|
|
62166
62531
|
if (options.workflowName) {
|
|
62167
62532
|
const found = workflows.find((w) => w.name === options.workflowName);
|
|
62168
62533
|
if (!found) {
|
|
62169
62534
|
throw new Error(`Workflow "${options.workflowName}" not found. Available: ${workflows.map((w) => w.name).join(", ")}`);
|
|
62170
62535
|
}
|
|
62171
|
-
|
|
62172
|
-
} else {
|
|
62173
|
-
workflow = workflows[0];
|
|
62536
|
+
return found;
|
|
62174
62537
|
}
|
|
62175
|
-
return
|
|
62538
|
+
return workflows[0];
|
|
62539
|
+
}
|
|
62540
|
+
function pickAndRender(workflows, options) {
|
|
62541
|
+
return workflowToSVG(pickWorkflow(workflows, options), options);
|
|
62176
62542
|
}
|
|
62177
62543
|
|
|
62178
62544
|
// src/cli/commands/diagram.ts
|
|
@@ -96730,7 +97096,7 @@ function displayInstalledPackage(pkg) {
|
|
|
96730
97096
|
}
|
|
96731
97097
|
|
|
96732
97098
|
// src/cli/index.ts
|
|
96733
|
-
var version2 = true ? "0.10.
|
|
97099
|
+
var version2 = true ? "0.10.4" : "0.0.0-dev";
|
|
96734
97100
|
var program2 = new Command();
|
|
96735
97101
|
program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").version(version2, "-v, --version", "Output the current version");
|
|
96736
97102
|
program2.configureOutput({
|