@tbrandenburg/node-red-cli 0.2.14 → 0.2.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tbrandenburg/node-red-cli",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "Call existing Node-RED flows from Node.js and the command line",
5
5
  "main": "src/link-call.js",
6
6
  "bin": {
@@ -34,6 +34,43 @@ function stderrLogHandler() {
34
34
  };
35
35
  }
36
36
 
37
+ /**
38
+ * Node-RED's flow parser (`@node-red/runtime/lib/flows/util.js`) classifies
39
+ * *any* node lacking both `x` and `y` properties as a global config node --
40
+ * regardless of its actual `type` -- since those coordinates are otherwise
41
+ * only ever used by the editor canvas. A real, editor-exported flow always
42
+ * has them on every wired node, so this never matters there. But hand-authored
43
+ * `--flow-json` flows (this tool's own core use case; see the README's
44
+ * `agent` example) commonly omit them, since they carry no runtime meaning.
45
+ * A wired node misclassified as a config node undergoes `Flow.js`'s
46
+ * config-node circular-dependency scan instead of normal instantiation,
47
+ * which scans every one of its own property values against other node ids
48
+ * and throws "Circular config node dependency detected" the moment any
49
+ * property value happens to equal another node's id -- including its own,
50
+ * e.g. a node whose `name` equals its own `id` (an extremely natural thing
51
+ * to write by hand, and exactly what the README's own agent example does).
52
+ * That aborts the whole flow's instantiation, so downstream preflight
53
+ * validation reports the target/return nodes as "not instantiated" even
54
+ * though the flow is otherwise entirely valid (see issue #28).
55
+ *
56
+ * Fix: assign synthetic coordinates to every node that is unambiguously a
57
+ * regular (wired) node -- i.e. it already declares a `wires` array, or is a
58
+ * `link out` node (which routes via `links` instead of `wires`) -- so
59
+ * Node-RED's parser classifies it correctly. Nodes without either (real
60
+ * config nodes) are left untouched.
61
+ */
62
+ function withDeployCoordinates(flow) {
63
+ let n = 0;
64
+ return flow.map((node) => {
65
+ const isWired = Object.prototype.hasOwnProperty.call(node, "wires") || node.type === "link out";
66
+ const hasCoords =
67
+ Object.prototype.hasOwnProperty.call(node, "x") && Object.prototype.hasOwnProperty.call(node, "y");
68
+ if (!isWired || hasCoords) return node;
69
+ n += 1;
70
+ return { ...node, x: n * 100, y: 100 };
71
+ });
72
+ }
73
+
37
74
  /**
38
75
  * Waits for Node-RED to finish attempting to start the deployed flows.
39
76
  *
@@ -55,20 +92,39 @@ function stderrLogHandler() {
55
92
  * either way; if the flows never actually started, the target/return nodes
56
93
  * simply won't be instantiated and the existing preflight validation in
57
94
  * `createHostLinkCaller` reports the real, specific error instead.
95
+ *
96
+ * A `stop`/`safe` `runtime-state` event and a real `flows:started` are
97
+ * mutually exclusive outcomes of the same deploy attempt in the installed
98
+ * `@node-red/runtime` (each early-return failure path returns before ever
99
+ * reaching the code that emits `flows:started`), so this never races in
100
+ * practice today. Still, resolving on `stop`/`safe` is deferred by one
101
+ * macrotask (`setImmediate`) rather than immediately, so that if a
102
+ * `flows:started` for the same attempt is already scheduled to fire right
103
+ * after, it wins instead -- cheap insurance against exactly the kind of
104
+ * premature-resolution regression reported in issue #28, without delaying
105
+ * genuine failures beyond a single negligible tick.
106
+ *
107
+ * (Uses `setTimeout(fn, 0)` rather than `setImmediate` purely because the
108
+ * latter isn't part of this project's configured ESLint globals; both defer
109
+ * to the next macrotask.)
58
110
  */
59
111
  function waitForFlowsSettled(RED) {
60
112
  return new Promise((resolve) => {
61
- const onStarted = () => {
113
+ let settled = false;
114
+ const finish = () => {
115
+ if (settled) return;
116
+ settled = true;
117
+ RED.events.removeListener("flows:started", onStarted);
62
118
  RED.events.removeListener("runtime-event", onRuntimeEvent);
63
119
  resolve();
64
120
  };
121
+ const onStarted = () => finish();
65
122
  const onRuntimeEvent = (event) => {
66
123
  if (
67
124
  event?.id === "runtime-state" &&
68
125
  (event.payload?.state === "stop" || event.payload?.state === "safe")
69
126
  ) {
70
- RED.events.removeListener("flows:started", onStarted);
71
- resolve();
127
+ setTimeout(finish, 0);
72
128
  }
73
129
  };
74
130
  RED.events.once("flows:started", onStarted);
@@ -113,7 +169,7 @@ async function runFlowInvocation({ flow, flowFile, msg, options }) {
113
169
 
114
170
  let caller;
115
171
  RED.init({
116
- ...(flow ? { storageModule: createMemoryStorageModule(flow) } : { flowFile }),
172
+ ...(flow ? { storageModule: createMemoryStorageModule(withDeployCoordinates(flow)) } : { flowFile }),
117
173
  userDir,
118
174
  httpAdminRoot: false,
119
175
  httpNodeRoot: false,