@particle-academy/fancy-flow 0.42.0 → 0.43.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/dist/index.d.cts CHANGED
@@ -661,4 +661,82 @@ type ExpressionFieldProps = {
661
661
  */
662
662
  declare function ExpressionField({ value, onChange, placeholder, rows, handle, graph, nodeId, }: ExpressionFieldProps): react.JSX.Element;
663
663
 
664
- export { ActionNode, type AlignEdge, AutoLayoutOptions, ConfigFieldRenderFn, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, ExpressionField, type ExpressionFieldProps, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, LaneNode, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, alignNodes, cloneSubgraph, defaultNodeTypes, defineNode, distributeNodes, flowKeys, flowLive, paletteDropHandlers, reconnectEdge, useFlowEditor, useFlowEditorOptional };
664
+ /**
665
+ * `{{ }}` resolution — the TypeScript twin of `FancyFlow\Nodes\Support\Expr`.
666
+ *
667
+ * ## Why this is opt-in and not wired into `runFlow`
668
+ *
669
+ * The PHP runtime resolves expressions inside its batteries-included executors.
670
+ * The JS runtime never has: `runFlow` hands `node.data.config` to your executor
671
+ * verbatim, so every host that uses `{{ }}` today already resolves it itself.
672
+ * Turning resolution on inside `runFlow` would interpolate a second time over
673
+ * values those hosts had already substituted — silently, and only for graphs
674
+ * that happen to contain `{{` in their DATA.
675
+ *
676
+ * So this exports the semantics and changes no behaviour. Call it yourself:
677
+ *
678
+ * ```ts
679
+ * const url = evaluateExpression(node.data.config.url, inputs);
680
+ * ```
681
+ *
682
+ * ## The semantics are the PHP file's, deliberately
683
+ *
684
+ * This is not a general expression language and must not grow into one. It
685
+ * resolves a dot-path against a context and nothing else — no arithmetic, no
686
+ * comparisons, no calls. Hosts that want real expressions override the executor
687
+ * (the PHP docblock points at symfony/expression-language for the same reason).
688
+ *
689
+ * Divergence here is a correctness bug rather than a style difference: the same
690
+ * graph is authored once and may run on either runtime. `suites/shared/expr`
691
+ * in `@particle-academy/fancy-conformance` is the fixture table both sides run,
692
+ * so parity is a test result instead of a claim.
693
+ */
694
+ /** Anything a context or a resolved value can be. */
695
+ type ExprValue = unknown;
696
+ /** The context an expression resolves against — the executor's `inputs`. */
697
+ type ExprContext = Record<string, ExprValue>;
698
+ /**
699
+ * Resolve a dot-path against the context, honouring the `$json` / `$input`
700
+ * alias.
701
+ *
702
+ * Both aliases point at the `in` port value when the context has one, and at
703
+ * the whole context otherwise — the same fallback the PHP does, which is what
704
+ * makes `{{ $json.x }}` work on a trigger node that has no upstream input.
705
+ *
706
+ * A path that does not resolve returns `null`, never `undefined`: PHP has one
707
+ * absent value and JS has two, and letting the difference leak would make the
708
+ * two runtimes disagree about `{{ missing }}` for no useful reason.
709
+ */
710
+ declare function resolvePath(path: string, context: ExprContext): ExprValue;
711
+ /**
712
+ * Evaluate a template against a context.
713
+ *
714
+ * A string that is EXACTLY one expression returns the resolved value with its
715
+ * type intact — `{{ $json.count }}` gives you a number, not `"3"`. Anything
716
+ * else interpolates each run as text. That distinction is load-bearing: it is
717
+ * what lets a config field carry either a value or a sentence.
718
+ *
719
+ * Non-string templates pass through untouched, so this is safe to map over a
720
+ * whole config object.
721
+ */
722
+ declare function evaluateExpression(template: ExprValue, context: ExprContext): ExprValue;
723
+ /**
724
+ * Truthiness for branch / switch decisions.
725
+ *
726
+ * Mirrors PHP's rules rather than JavaScript's, because the graph is authored
727
+ * once and may run on either side. The two disagree in exactly the places a
728
+ * workflow hits: `"0"` and `"false"` are truthy in JS and falsy here, and an
729
+ * empty array is truthy in JS and falsy here.
730
+ */
731
+ declare function truthy(value: ExprValue): boolean;
732
+ /** Coerce a value to text the way interpolation does. */
733
+ declare function text(value: ExprValue): string;
734
+ /**
735
+ * Resolve every string in a config object, one level of nesting at a time.
736
+ *
737
+ * The convenience most hosts actually want, and the shape their hand-rolled
738
+ * version usually takes. Opt-in like the rest of this module.
739
+ */
740
+ declare function evaluateConfig<T extends Record<string, ExprValue>>(config: T, context: ExprContext): T;
741
+
742
+ export { ActionNode, type AlignEdge, AutoLayoutOptions, ConfigFieldRenderFn, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, type ExprContext, type ExprValue, ExpressionField, type ExpressionFieldProps, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, LaneNode, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, alignNodes, cloneSubgraph, defaultNodeTypes, defineNode, distributeNodes, evaluateConfig, evaluateExpression, flowKeys, flowLive, paletteDropHandlers, reconnectEdge, resolvePath, text, truthy, useFlowEditor, useFlowEditorOptional };
package/dist/index.d.ts CHANGED
@@ -661,4 +661,82 @@ type ExpressionFieldProps = {
661
661
  */
662
662
  declare function ExpressionField({ value, onChange, placeholder, rows, handle, graph, nodeId, }: ExpressionFieldProps): react.JSX.Element;
663
663
 
664
- export { ActionNode, type AlignEdge, AutoLayoutOptions, ConfigFieldRenderFn, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, ExpressionField, type ExpressionFieldProps, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, LaneNode, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, alignNodes, cloneSubgraph, defaultNodeTypes, defineNode, distributeNodes, flowKeys, flowLive, paletteDropHandlers, reconnectEdge, useFlowEditor, useFlowEditorOptional };
664
+ /**
665
+ * `{{ }}` resolution — the TypeScript twin of `FancyFlow\Nodes\Support\Expr`.
666
+ *
667
+ * ## Why this is opt-in and not wired into `runFlow`
668
+ *
669
+ * The PHP runtime resolves expressions inside its batteries-included executors.
670
+ * The JS runtime never has: `runFlow` hands `node.data.config` to your executor
671
+ * verbatim, so every host that uses `{{ }}` today already resolves it itself.
672
+ * Turning resolution on inside `runFlow` would interpolate a second time over
673
+ * values those hosts had already substituted — silently, and only for graphs
674
+ * that happen to contain `{{` in their DATA.
675
+ *
676
+ * So this exports the semantics and changes no behaviour. Call it yourself:
677
+ *
678
+ * ```ts
679
+ * const url = evaluateExpression(node.data.config.url, inputs);
680
+ * ```
681
+ *
682
+ * ## The semantics are the PHP file's, deliberately
683
+ *
684
+ * This is not a general expression language and must not grow into one. It
685
+ * resolves a dot-path against a context and nothing else — no arithmetic, no
686
+ * comparisons, no calls. Hosts that want real expressions override the executor
687
+ * (the PHP docblock points at symfony/expression-language for the same reason).
688
+ *
689
+ * Divergence here is a correctness bug rather than a style difference: the same
690
+ * graph is authored once and may run on either runtime. `suites/shared/expr`
691
+ * in `@particle-academy/fancy-conformance` is the fixture table both sides run,
692
+ * so parity is a test result instead of a claim.
693
+ */
694
+ /** Anything a context or a resolved value can be. */
695
+ type ExprValue = unknown;
696
+ /** The context an expression resolves against — the executor's `inputs`. */
697
+ type ExprContext = Record<string, ExprValue>;
698
+ /**
699
+ * Resolve a dot-path against the context, honouring the `$json` / `$input`
700
+ * alias.
701
+ *
702
+ * Both aliases point at the `in` port value when the context has one, and at
703
+ * the whole context otherwise — the same fallback the PHP does, which is what
704
+ * makes `{{ $json.x }}` work on a trigger node that has no upstream input.
705
+ *
706
+ * A path that does not resolve returns `null`, never `undefined`: PHP has one
707
+ * absent value and JS has two, and letting the difference leak would make the
708
+ * two runtimes disagree about `{{ missing }}` for no useful reason.
709
+ */
710
+ declare function resolvePath(path: string, context: ExprContext): ExprValue;
711
+ /**
712
+ * Evaluate a template against a context.
713
+ *
714
+ * A string that is EXACTLY one expression returns the resolved value with its
715
+ * type intact — `{{ $json.count }}` gives you a number, not `"3"`. Anything
716
+ * else interpolates each run as text. That distinction is load-bearing: it is
717
+ * what lets a config field carry either a value or a sentence.
718
+ *
719
+ * Non-string templates pass through untouched, so this is safe to map over a
720
+ * whole config object.
721
+ */
722
+ declare function evaluateExpression(template: ExprValue, context: ExprContext): ExprValue;
723
+ /**
724
+ * Truthiness for branch / switch decisions.
725
+ *
726
+ * Mirrors PHP's rules rather than JavaScript's, because the graph is authored
727
+ * once and may run on either side. The two disagree in exactly the places a
728
+ * workflow hits: `"0"` and `"false"` are truthy in JS and falsy here, and an
729
+ * empty array is truthy in JS and falsy here.
730
+ */
731
+ declare function truthy(value: ExprValue): boolean;
732
+ /** Coerce a value to text the way interpolation does. */
733
+ declare function text(value: ExprValue): string;
734
+ /**
735
+ * Resolve every string in a config object, one level of nesting at a time.
736
+ *
737
+ * The convenience most hosts actually want, and the shape their hand-rolled
738
+ * version usually takes. Opt-in like the rest of this module.
739
+ */
740
+ declare function evaluateConfig<T extends Record<string, ExprValue>>(config: T, context: ExprContext): T;
741
+
742
+ export { ActionNode, type AlignEdge, AutoLayoutOptions, ConfigFieldRenderFn, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, type ExprContext, type ExprValue, ExpressionField, type ExpressionFieldProps, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, LaneNode, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, alignNodes, cloneSubgraph, defaultNodeTypes, defineNode, distributeNodes, evaluateConfig, evaluateExpression, flowKeys, flowLive, paletteDropHandlers, reconnectEdge, resolvePath, text, truthy, useFlowEditor, useFlowEditorOptional };
package/dist/index.js CHANGED
@@ -173,8 +173,8 @@ function describeExpressionGrammar() {
173
173
  note: "Dot-paths only \u2014 no arithmetic, comparisons or function calls; anything else resolves to nothing. A field that is exactly one expression keeps the resolved value's type; mixed text interpolates as a string. fancy-flow's own runFlow does not interpolate: it hands config to your executor verbatim, and the fancy-flow-php runtime is what resolves these."
174
174
  };
175
175
  }
176
- function findTrigger(text, caret) {
177
- const before = text.slice(0, caret);
176
+ function findTrigger(text2, caret) {
177
+ const before = text2.slice(0, caret);
178
178
  const open = before.lastIndexOf("{{");
179
179
  if (open === -1) return null;
180
180
  if (before.indexOf("}}", open) !== -1) return null;
@@ -185,9 +185,9 @@ function filterVariables(vars, query) {
185
185
  if (q === "") return vars;
186
186
  return vars.filter((v) => v.path.toLowerCase().includes(q));
187
187
  }
188
- function applyCompletion(text, caret, trigger, variable) {
189
- const head = text.slice(0, trigger.open);
190
- const tail = text.slice(caret);
188
+ function applyCompletion(text2, caret, trigger, variable) {
189
+ const head = text2.slice(0, trigger.open);
190
+ const tail = text2.slice(caret);
191
191
  return { value: `${head}${variable.expression}${tail}`, caret: head.length + variable.expression.length };
192
192
  }
193
193
  function ExpressionField({
@@ -711,7 +711,7 @@ function JsonField({
711
711
  return "";
712
712
  }
713
713
  }, [value]);
714
- const [text, setText] = useState(serialized);
714
+ const [text2, setText] = useState(serialized);
715
715
  const [error, setError] = useState(null);
716
716
  const [touched, setTouched] = useState(false);
717
717
  const lastProps = useRef(serialized);
@@ -749,7 +749,7 @@ function JsonField({
749
749
  ...handle,
750
750
  className: "ff-panel__input ff-panel__input--json",
751
751
  rows: rows ?? 6,
752
- value: text,
752
+ value: text2,
753
753
  spellCheck: false,
754
754
  "aria-invalid": showError || void 0,
755
755
  "aria-describedby": showError ? errorId : void 0,
@@ -1629,8 +1629,8 @@ function FlowEditorInner({
1629
1629
  x: labelEdit.x,
1630
1630
  y: labelEdit.y,
1631
1631
  initial: flow.edges.find((e) => e.id === labelEdit.edgeId)?.label ?? "",
1632
- onCommit: (text) => {
1633
- api.setEdgeLabel(labelEdit.edgeId, text);
1632
+ onCommit: (text2) => {
1633
+ api.setEdgeLabel(labelEdit.edgeId, text2);
1634
1634
  setLabelEdit(null);
1635
1635
  },
1636
1636
  onCancel: () => setLabelEdit(null)
@@ -1735,7 +1735,7 @@ function EdgeLabelEditor({
1735
1735
  onCommit,
1736
1736
  onCancel
1737
1737
  }) {
1738
- const [text, setText] = useState(initial);
1738
+ const [text2, setText] = useState(initial);
1739
1739
  const ref = useRef(null);
1740
1740
  useEffect(() => {
1741
1741
  ref.current?.focus();
@@ -1752,17 +1752,17 @@ function EdgeLabelEditor({
1752
1752
  {
1753
1753
  ref,
1754
1754
  className: "ff-panel__input",
1755
- value: text,
1755
+ value: text2,
1756
1756
  placeholder: "Label this connection",
1757
1757
  "aria-label": "Connection label",
1758
1758
  "data-action": "edge-label-input",
1759
1759
  onChange: (e) => setText(e.target.value),
1760
1760
  onKeyDown: (e) => {
1761
1761
  e.stopPropagation();
1762
- if (e.key === "Enter") onCommit(text);
1762
+ if (e.key === "Enter") onCommit(text2);
1763
1763
  if (e.key === "Escape") onCancel();
1764
1764
  },
1765
- onBlur: () => onCommit(text)
1765
+ onBlur: () => onCommit(text2)
1766
1766
  }
1767
1767
  )
1768
1768
  }
@@ -1823,9 +1823,70 @@ var flowKeys = {
1823
1823
  run: (runId) => ["flow", "runs", runId]
1824
1824
  };
1825
1825
 
1826
+ // src/expressions/expr.ts
1827
+ var WHOLE = /^\{\{\s*([\s\S]*?)\s*\}\}$/;
1828
+ var EACH = /\{\{\s*([\s\S]*?)\s*\}\}/g;
1829
+ var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
1830
+ function resolvePath(path, context) {
1831
+ const trimmed = path.trim();
1832
+ if (trimmed === "") return null;
1833
+ const segments = trimmed.split(".");
1834
+ let cursor;
1835
+ const head = segments[0];
1836
+ if (head === "$json" || head === "$input") {
1837
+ cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
1838
+ segments.shift();
1839
+ } else {
1840
+ cursor = context;
1841
+ }
1842
+ for (const segment of segments) {
1843
+ if (cursor === null || cursor === void 0) return null;
1844
+ if (typeof cursor !== "object") return null;
1845
+ const next = cursor[segment];
1846
+ if (next === void 0) return null;
1847
+ cursor = next;
1848
+ }
1849
+ return cursor === void 0 ? null : cursor;
1850
+ }
1851
+ function evaluateExpression(template, context) {
1852
+ if (typeof template !== "string") return template;
1853
+ const whole = WHOLE.exec(template.trim());
1854
+ if (whole) return resolvePath(whole[1] ?? "", context);
1855
+ return template.replace(EACH, (_m, path) => stringify(resolvePath(path, context)));
1856
+ }
1857
+ function truthy(value) {
1858
+ if (typeof value === "boolean") return value;
1859
+ if (value === null || value === void 0) return false;
1860
+ if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
1861
+ if (Array.isArray(value)) return value.length > 0;
1862
+ if (typeof value === "number") return value !== 0;
1863
+ return Boolean(value);
1864
+ }
1865
+ function text(value) {
1866
+ return stringify(value);
1867
+ }
1868
+ function stringify(value) {
1869
+ if (typeof value === "string") return value;
1870
+ if (typeof value === "boolean") return value ? "true" : "false";
1871
+ if (value === null || value === void 0) return "";
1872
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
1873
+ try {
1874
+ return JSON.stringify(value) ?? "";
1875
+ } catch {
1876
+ return "";
1877
+ }
1878
+ }
1879
+ function evaluateConfig(config, context) {
1880
+ const out = {};
1881
+ for (const [key, value] of Object.entries(config)) {
1882
+ out[key] = Array.isArray(value) ? value.map((v) => evaluateExpression(v, context)) : value !== null && typeof value === "object" ? evaluateConfig(value, context) : evaluateExpression(value, context);
1883
+ }
1884
+ return out;
1885
+ }
1886
+
1826
1887
  // src/index.ts
1827
1888
  registerBuiltinKinds();
1828
1889
 
1829
- export { ConfigFieldRenderer, ExpressionField, FlowEditor, FlowRunControls, FlowRunFeed, NodeConfigPanel, NodePalette, NodePort, availableVariables, baseVariables, defineNode, describeExpressionGrammar, flowKeys, flowLive, outputFieldsFor, paletteDropHandlers };
1890
+ export { ConfigFieldRenderer, ExpressionField, FlowEditor, FlowRunControls, FlowRunFeed, NodeConfigPanel, NodePalette, NodePort, availableVariables, baseVariables, defineNode, describeExpressionGrammar, evaluateConfig, evaluateExpression, flowKeys, flowLive, outputFieldsFor, paletteDropHandlers, resolvePath, text, truthy };
1830
1891
  //# sourceMappingURL=index.js.map
1831
1892
  //# sourceMappingURL=index.js.map