@nrutils/node-red-utils 0.0.0-stage → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +190 -0
  3. package/README.md +181 -2
  4. package/index.js +50 -0
  5. package/node-red-base/NodeRedNodeBase.js +143 -0
  6. package/node-red-base/README.md +215 -0
  7. package/node-red-editorcomms/EditorCommsBackend.js +155 -0
  8. package/node-red-editorcomms/EditorCommsFrontend.js +105 -0
  9. package/node-red-editorcomms/README.md +164 -0
  10. package/node-red-gpu/GpuBuffer.js +29 -0
  11. package/node-red-gpu/Kernel.js +35 -0
  12. package/node-red-gpu/README.md +220 -0
  13. package/node-red-gpu/backends/CpuBackend.js +109 -0
  14. package/node-red-gpu/backends/VulkanBackend.js +232 -0
  15. package/node-red-gpu/errors.js +48 -0
  16. package/node-red-gpu/index.js +6 -0
  17. package/node-red-gpu/log.js +18 -0
  18. package/node-red-gpu/native/binding.gyp +14 -0
  19. package/node-red-gpu/native/src/gpu_addon.cpp +1066 -0
  20. package/node-red-gpu/nodeRedGpuMain.js +39 -0
  21. package/node-red-gpu/nodeRedGpuWorker.js +242 -0
  22. package/node-red-gpu/transpiler/emitGlsl.js +497 -0
  23. package/node-red-gpu/transpiler/glslNames.js +60 -0
  24. package/node-red-gpu/transpiler/index.js +81 -0
  25. package/node-red-gpu/transpiler/inferTypes.js +302 -0
  26. package/node-red-gpu/transpiler/validate.js +461 -0
  27. package/node-red-popup/NodeRedPopupBackend.js +69 -0
  28. package/node-red-popup/NodeRedPopupFrontend.js +414 -0
  29. package/node-red-popup/README.md +110 -0
  30. package/node-red-pubsub-bus/NodeRedPubsubBus.js +617 -0
  31. package/node-red-pubsub-bus/NodeRedPubsubBusWorkerProxy.js +239 -0
  32. package/node-red-pubsub-bus/README.md +201 -0
  33. package/node-red-resourceendpoint/NodeRedResourceEndpoint.js +205 -0
  34. package/node-red-resourceendpoint/README.md +119 -0
  35. package/node-red-singleton/NodeRedSingletonBackend.js +30 -0
  36. package/node-red-singleton/NodeRedSingletonFrontend.js +54 -0
  37. package/node-red-singleton/README.md +72 -0
  38. package/node-red-worker/NodeRedWorker.js +438 -0
  39. package/node-red-worker/NodeRedWorkerRuntime.js +651 -0
  40. package/node-red-worker/README.md +265 -0
  41. package/node-red-worker/workerRuntimeLoader.js +55 -0
  42. package/package.json +70 -5
@@ -0,0 +1,265 @@
1
+ # node-red-worker
2
+
3
+ Run a node's heavy work in a worker thread so it does not block the Node-RED event loop. Worker nodes that are wired together send messages to each other directly, thread to thread.
4
+
5
+ ## Usage
6
+
7
+ `hash-node.js`:
8
+
9
+ ```js
10
+ const path = require('path');
11
+ const { NodeRedNodeBase, nodeRedWorker } = require('@nrutils/node-red-utils');
12
+
13
+ module.exports = NodeRedNodeBase.createNode('hash-node', [nodeRedWorker], (RED, thisNode, config) => {
14
+ const worker = thisNode.nodeRedWorker.init(path.join(__dirname, 'hash-worker.js'), { rounds: 100000 });
15
+
16
+ // Several messages can be in the worker at the same time. Each request() gets its own answer.
17
+ thisNode.on('input', (msg, send, done) => {
18
+ worker.request('hash', String(msg.payload), [], { timeout: 10000 }).then((digest) => {
19
+ msg.payload = digest; // keep _msgid, topic and the other properties
20
+ send(msg);
21
+ done();
22
+ }, done); // the worker threw or stopped, the data cannot be cloned, or 10 s passed
23
+ });
24
+ });
25
+ ```
26
+
27
+ `hash-worker.js`:
28
+
29
+ ```js
30
+ const crypto = require('crypto');
31
+
32
+ // Runs in the worker thread. "data" is the second argument of init(): settings from config.
33
+ // Settings that change per message go in the data of each request().
34
+ module.exports = function (worker, data) {
35
+ // The return value (or the value of a returned Promise) is the answer to request('hash', text)
36
+ worker.handle('hash', (text) => {
37
+ let digest = text;
38
+ for (let i = 0; i < data.rounds; i++) {
39
+ digest = crypto.createHash('sha256').update(digest).digest('hex');
40
+ }
41
+ return digest;
42
+ });
43
+ };
44
+ ```
45
+
46
+ If the handler throws, `request()` rejects with an `Error` that has the same `name` and `message`, so `done(err)` shows a Catch node the same text as a local throw.
47
+
48
+ ## Example: an image pipeline across two worker nodes
49
+
50
+ Wire an `img-gray` node to an `img-threshold` node. The gray worker sends each frame straight to the threshold worker, thread to thread. The frame does not go through the Node-RED runtime, and the pixels move without a copy.
51
+
52
+ `stages.js` (both node types; the HTML file gives each type one input and one output, and `img-threshold` a `level` field):
53
+
54
+ ```js
55
+ const path = require('path');
56
+ const { NodeRedNodeBase, nodeRedWorker } = require('@nrutils/node-red-utils');
57
+
58
+ // msg.payload is { width, height, rgba }: a Buffer with 4 bytes per pixel
59
+ const gray = NodeRedNodeBase.createNode('img-gray', [nodeRedWorker], (RED, thisNode) => {
60
+ const worker = thisNode.nodeRedWorker.init(path.join(__dirname, 'gray-worker.js'));
61
+ thisNode.on('input', (msg, send, done) => {
62
+ worker.publish('frame', msg.payload); // the result goes to the next worker, not back here
63
+ done();
64
+ });
65
+ });
66
+
67
+ // Gets each frame from the thread of the img-gray node that is wired to it
68
+ const threshold = NodeRedNodeBase.createNode('img-threshold', [nodeRedWorker], (RED, thisNode, config) => {
69
+ const level = Number(config.level) || 128;
70
+ const worker = thisNode.nodeRedWorker.init(path.join(__dirname, 'threshold-worker.js'), { level });
71
+ worker.subscribe('mask', ({ width, height, mask, lit }) => {
72
+ thisNode.status({ text: `${Math.round(100 * lit / mask.length)} % at or above ${level}` });
73
+ thisNode.send({ payload: { width, height, mask: Buffer.from(mask.buffer) } });
74
+ });
75
+ });
76
+
77
+ module.exports = function (RED) {
78
+ gray(RED);
79
+ threshold(RED);
80
+ };
81
+ ```
82
+
83
+ `gray-worker.js`:
84
+
85
+ ```js
86
+ // Runs in the thread of an img-gray node
87
+ module.exports = function (worker) {
88
+ worker.parentPorts.subscribe('frame', ({ width, height, rgba }) => {
89
+ const gray = new Uint8Array(width * height);
90
+ for (let i = 0; i < gray.length; i++) {
91
+ gray[i] = (77 * rgba[4 * i] + 150 * rgba[4 * i + 1] + 29 * rgba[4 * i + 2]) >> 8;
92
+ }
93
+ // To the threads of the wired worker nodes. The transferList moves the pixels
94
+ // without a copy, so it works for one wire only.
95
+ worker.outputPorts.publish('gray', { width, height, gray }, [gray.buffer]);
96
+ });
97
+ };
98
+ ```
99
+
100
+ `threshold-worker.js`:
101
+
102
+ ```js
103
+ // Runs in the thread of an img-threshold node
104
+ module.exports = function (worker, data) {
105
+ worker.inputPorts.subscribe('gray', ({ width, height, gray }) => {
106
+ const mask = new Uint8Array(gray.length);
107
+ let lit = 0;
108
+ for (let i = 0; i < gray.length; i++) {
109
+ if (gray[i] >= data.level) { mask[i] = 255; lit++; }
110
+ }
111
+ worker.parentPorts.publish('mask', { width, height, mask, lit }, [mask.buffer]);
112
+ });
113
+ };
114
+ ```
115
+
116
+ The main thread of `img-threshold` gets no input message. The wire from `img-gray` only tells the mixin which workers to connect. A second wire on the `img-gray` output gets no frame: the first wire has the buffer already, and the publish to the second wire fails with a `DataCloneError`. For more wires, leave out the transferList; then each wire gets a copy.
117
+
118
+ ## Request, publish and subscribe
119
+
120
+ | Call | Direction | Answer |
121
+ | --- | --- | --- |
122
+ | `worker.request(type, data)` | main thread to the one `worker.handle(type, fn)` handler in the worker | a Promise for the value that the handler returns |
123
+ | `publish(type, data)` | either side, to every `subscribe(type)` and `'default'` handler on the other side | none |
124
+ | `subscribe(type, handler)` | gets the other side's `publish(type)` messages | none |
125
+
126
+ The worker can send without a question, for example progress:
127
+
128
+ ```js
129
+ // worker file
130
+ worker.parentPorts.publish('progress', 50);
131
+
132
+ // node
133
+ worker.subscribe('progress', (p) => thisNode.status({ text: `${p} %` }));
134
+ ```
135
+
136
+ ## Worker file
137
+
138
+ The worker file runs in its own thread. It has no `RED`, no node object, no flow or global context. It talks to the node only through messages. Objects arrive as structured clones (a `Buffer` arrives as a `Uint8Array`). `require` your own modules in the worker file.
139
+
140
+ The worker file exports one function, `(worker, data)`. CommonJS and ES modules both work:
141
+
142
+ ```js
143
+ // my-worker.js (CommonJS)
144
+ module.exports = function (worker, data) { /* ... */ };
145
+ ```
146
+
147
+ ```js
148
+ // my-worker.mjs, or a .js file in a package with "type": "module"
149
+ export default function (worker, data) { /* ... */ }
150
+ ```
151
+
152
+ A file that exports something else (for example only named exports) gives a load error that shows the two shapes above.
153
+
154
+ ## Lifecycle
155
+
156
+ 1. `init()` starts the thread. The thread loads the runtime, not your worker file yet. Call `init()` in the setup function, before any `await`.
157
+ 2. After each deploy, on `flows:started`, the mixin connects each new worker to the worker nodes it is wired to. A worker that `init()` starts after `flows:started` is connected at once.
158
+ 3. When all connections of a worker are ready, the optional `init()` callback runs on the main thread. Then the runtime calls your worker function one time.
159
+ 4. When the node closes, the mixin publishes `close` to the worker, waits up to 2 seconds for its `close` handlers to finish, then terminates the thread. A worker in a long synchronous loop cannot run its `close` handler until the loop ends, so the deploy waits the full 2 seconds and the cleanup does not run. To let a long job stop in time, split it into parts with `await new Promise(setImmediate)` between them, and check a flag that your `close` handler sets.
160
+
161
+ A message can arrive before your worker function has run: for example the parent publishes on the first input, or an upstream worker publishes when it starts. The runtime holds such messages and delivers them in order after your function returns, so you do not need a `setTimeout` before the first publish. Subscribe synchronously in your worker function, not after an `await`. After the function has run, a message with no subscriber is dropped.
162
+
163
+ Call `worker.handle()` synchronously in your worker function too. A request that arrives before your function has run waits, as above. A request for a type with no handler rejects with an error that names the type.
164
+
165
+ To clean up in the worker, subscribe to `close`:
166
+
167
+ ```js
168
+ worker.parentPorts.subscribe('close', () => { /* free resources */ });
169
+ ```
170
+
171
+ ## Errors
172
+
173
+ If your worker file throws while it loads, or worker code throws later and nothing catches it (for example in a `setTimeout` callback), Node-RED keeps running. The node:
174
+
175
+ - logs `Worker error: <message>` with `node.error`, so a Catch node gets it too,
176
+ - shows a red ring status with the text `worker error`.
177
+
178
+ The worker is not restarted. Its wires to other workers are closed. A Full deploy, or Restart Flows, starts it again; the default Modified Nodes deploy does not restart an unchanged node. Every `request()` that waits for an answer rejects, with `worker error: <message>` or `worker stopped with exit code <code>`, and later calls reject at once. Messages that the node publishes to the stopped worker are dropped.
179
+
180
+ When a redeploy or a delete closes the node, the pending requests reject with `worker request ended: the node closed`.
181
+
182
+ A throw inside a `subscribe` handler in the worker is different: the runtime catches it and logs it with `console.error`, and the worker keeps running. A throw inside a `handle` handler goes back to the caller of `request()`.
183
+
184
+ `publish()` and `request()` use the structured clone algorithm. Data that cannot be cloned, such as a function or an object that contains a function, makes `publish()` throw a `DataCloneError` at once, and makes `request()` reject with it. An answer from `handle()` that cannot be cloned rejects the request.
185
+
186
+ ## One worker per node, jobs queue
187
+
188
+ Each node has one worker thread. A worker runs one piece of synchronous code at a time, so requests and messages to it wait in a queue. Ten messages that each need 1 s of CPU take 10 s on one node. To use more cores, put more nodes in the flow (for example behind a switch node that splits the messages), or start your own pool of `worker_threads` in the worker or the node.
189
+
190
+ A worker handler that is `async` and waits for I/O lets the next request start while it waits.
191
+
192
+ ## Big results
193
+
194
+ The answer of a request, and the data of a publish, is copied between the threads (structured clone). The copy of a big object runs on the main thread when it arrives, and the event loop of Node-RED waits for it. Thus:
195
+
196
+ - send less: only the fields that the flow needs, or a summary;
197
+ - send big data in chunks, with `publish()`;
198
+ - move binary data without a copy: put the data in an `ArrayBuffer` and transfer it. In a `handle` handler, push the buffer into `transferList`, the second argument of the handler:
199
+
200
+ ```js
201
+ worker.handle('render', (spec, transferList) => {
202
+ const pixels = new Uint8Array(spec.width * spec.height * 4);
203
+ // ... fill pixels ...
204
+ transferList.push(pixels.buffer); // moved, not copied; the worker cannot use it after this
205
+ return pixels;
206
+ });
207
+ ```
208
+
209
+ On the main thread, `request(type, data, transferList)` and `publish(type, data, transferList)` do the same in the other direction.
210
+
211
+ ## API
212
+
213
+ ### Main thread
214
+
215
+ `thisNode.nodeRedWorker.init(workerFile, [workerData], [callback])` starts the worker. `init()` returns the worker object (a `NodeRedWorker`, a subclass of Node's `Worker`); use the returned value. `workerFile` must be an absolute path, for example `path.join(__dirname, 'my-worker.js')`. A relative path throws a `TypeError` that names the path. `workerData` must be cloneable with the structured clone algorithm. Call `init()` one time for each node, in the setup function (see [Lifecycle](#lifecycle)). A second call throws. A call to `publish()`, `request()`, `subscribe()`, `on()` or `terminate()` before `init()` throws an error that says to call `init()` first.
216
+
217
+ - `publish(type, [data], [transferList])`: send to the worker. For `transferList`, see [Big results](#big-results). Throws a `DataCloneError` if `data` cannot be cloned. A Node `Buffer` arrives as a `Uint8Array` (use `Buffer.from(u8.buffer, u8.byteOffset, u8.length)` to get a `Buffer` again). The same is true in the other direction and between workers.
218
+ - `request(type, [data], [transferList], [options])`: send `data` to the worker's `handle(type)` handler. Returns a Promise for the value that the handler returns. It rejects with the error that the handler throws, with a `DataCloneError`, when the worker stops or fails, and when the node closes. `options.timeout`: reject after this many milliseconds with `worker request '<type>' timed out after <n> ms`. Default: no timeout. A timeout does not stop the work in the worker.
219
+ - `subscribe(type, handler)`: call `handler(data, senderTrace, receiverTrace, type)` for each message of `type` from the worker. See [the `default` type](#the-default-type). If a handler throws (or returns a promise that rejects), the node logs `worker message handler for '<type>' failed: <message>` with `node.error`, and a Catch node gets it. The other handlers for the same message still run. To tie the error to a message, put your own `try`/`catch` in the handler and call `done(err)` there.
220
+ - `namedPort(name)`: a `MessagePort` that the worker made with `parentPorts[0][0].createNamedPort()`, or `null`.
221
+ - `terminate()`: the close sequence in [Lifecycle](#lifecycle). The mixin calls it for you.
222
+
223
+ ### Worker thread
224
+
225
+ Your worker file exports `function (worker, data)` (see [Worker file](#worker-file)).
226
+
227
+ `worker.handle(type, handler)`: answer the main thread's `request(type, ...)`. `handler(data, transferList)` returns the answer or a Promise for it. For `transferList`, see [Big results](#big-results). One handler for each type; a second `handle()` for the same type throws.
228
+
229
+ `worker` has three port groups:
230
+
231
+ - `worker.parentPorts`: to and from the main-thread node.
232
+ - `worker.inputPorts`: from upstream worker nodes.
233
+ - `worker.outputPorts`: to downstream worker nodes.
234
+
235
+ Each group has `publish(type, [data], [transferList])` and `subscribe(type, handler)`. See [Advanced: worker to worker](#advanced-worker-to-worker) for ports, wires and the other connection calls.
236
+
237
+ ## Advanced: worker to worker
238
+
239
+ The [image pipeline](#example-an-image-pipeline-across-two-worker-nodes) shows the basic case: `outputPorts.publish()` in the upstream worker, `inputPorts.subscribe()` in the downstream worker.
240
+
241
+ Messages between workers do not go through the Node-RED runtime, so the Debug sidebar does not see them. Only nodes that use this mixin get these direct connections; a wire to a normal node does nothing on the worker side.
242
+
243
+ Subflows: worker nodes that are wired to each other inside a subflow are connected, in each subflow instance. A worker inside the subflow that is wired to a subflow output is also connected to the worker nodes that the output of the instance is wired to. A wire into a subflow input is not: the upstream worker is wired to the subflow instance node, which is not a worker. Send such data on the main thread instead (`node.send()` and an input handler).
244
+
245
+ Known limit: two worker nodes from different node packages, each with its own copy of this library, are not connected to each other.
246
+
247
+ Each group, each port (`worker.outputPorts[0]`) and each wire (`worker.outputPorts[0][1]`) has `publish(type, [data], [transferList])` and `subscribe(type, handler)`. The handler gets `(data, senderTrace, receiverTrace, type)`. The traces hold the node id and the port and wire numbers. A connection object also has:
248
+
249
+ - `unsubscribe(type, handler)`.
250
+ - `publishUntilAcknowledged(type, ackType, intervalMs, [data], [transferList])`: publish again every `intervalMs` until an `ackType` message arrives. Returns a function that stops it. If this connection already polls the same `type`, nothing is sent again and the stop function of that poll is returned. If it polls another type, that poll stops first.
251
+ - `pipe(target)`: forward every raw message from this connection to another connection, port or port group.
252
+ - `createNamedPort(myName, [theirName])`: open an extra `MessageChannel` to the other end and return your `MessagePort`. A downstream worker finds the other end in `worker.inputPorts[p][c].namedPorts[theirName]`. On a `parentPorts` connection, the main thread gets it from `namedPort(theirName)`.
253
+
254
+ A `publish` to a port with no connected worker is dropped without an error, as in Node-RED when an output has no wire. Use the `outputPorts[p][w]` numbers that match the node's `wires`.
255
+
256
+ Port and wire numbers:
257
+
258
+ - `outputPorts[p][w]`: `p` is the output of the node and `w` is the wire on that output, in the order of the node's `wires` in the flow. These numbers are stable.
259
+ - `inputPorts[0][w]`: a node has one input, so `p` is always `0`. `w` is a slot that the runtime gives to each upstream wire in the order the connections are made. When an upstream worker stops (for example a deploy changes it), its slot becomes free and the next new connection uses it. Thus the number of slots stays the same across deploys, but do not use `w` to find out which node sent a message. Use `senderTrace.nodeId`.
260
+
261
+ ### The `default` type
262
+
263
+ A handler subscribed to `'default'` gets every message type, also the types that have their own handler, on the main thread and in the worker. It does not get the types that the library uses itself (`ack` and names that start with `_`). It gets `close`.
264
+
265
+ [Back to node-red-utils](../README.md)
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Copyright 2024 Contributors
3
+ * Licensed under the Apache License, Version 2.0
4
+ * http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { workerData } = require('worker_threads');
10
+ const { pathToFileURL } = require('url');
11
+ const { NodeRedWorkerRuntime } = require('./NodeRedWorkerRuntime');
12
+
13
+ /**
14
+ * Loads the worker file and returns its function.
15
+ * CommonJS: module.exports = function (worker, data) {}. ES module: export default function (worker, data) {}.
16
+ */
17
+ async function loadWorkerFunction(file) {
18
+ let exported;
19
+ try {
20
+ exported = require(file);
21
+ } catch (error) {
22
+ // An ES module that require() cannot load (.mjs, or "type": "module")
23
+ if (error.code !== 'ERR_REQUIRE_ESM' && error.code !== 'ERR_REQUIRE_ASYNC_MODULE') throw error;
24
+ exported = await import(pathToFileURL(file).href);
25
+ }
26
+ const func = typeof exported === 'function' ? exported : exported && exported.default;
27
+ if (typeof func !== 'function') {
28
+ throw new TypeError('the worker file must export a function (worker, data): ' +
29
+ 'module.exports = function (worker, data) {} or export default function (worker, data) {}');
30
+ }
31
+ return func;
32
+ }
33
+
34
+ /**
35
+ * Sets up the worker environment and initializes communication
36
+ */
37
+ function workerRuntimeLoader() {
38
+ const nodeRedWorker = new NodeRedWorkerRuntime({
39
+ ConnectionsCompleteCallback: async () => {
40
+ try {
41
+ const developerWorkerFunc = await loadWorkerFunction(workerData.workerFile);
42
+ developerWorkerFunc(nodeRedWorker, workerData.myWorkerData);
43
+ } catch (error) {
44
+ // The main thread reports it on the node (log and red status)
45
+ nodeRedWorker.parentPorts.publish('_workerError',
46
+ `${workerData.workerFile}: ${error && error.message ? error.message : String(error)}`);
47
+ }
48
+ }
49
+ });
50
+
51
+ return nodeRedWorker;
52
+ }
53
+
54
+ // This file is always run as a worker, so immediately set up the environment
55
+ workerRuntimeLoader();
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
- "name": "@nrutils/node-red-utils",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@nrutils/node-red-utils",
3
+ "version": "0.1.0",
4
+ "description": "Mixin toolkit for authors of Node-RED custom nodes: editor messaging, worker threads, popups, singletons, resource serving, pub/sub and optional GPU compute.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node --test test/*.test.js",
8
+ "test:gpu": "node node-red-gpu/smoke-test.js"
9
+ },
10
+ "exports": {
11
+ ".": "./index.js",
12
+ "./node-red-base": "./node-red-base/NodeRedNodeBase.js",
13
+ "./node-red-editorcomms": "./node-red-editorcomms/EditorCommsBackend.js",
14
+ "./node-red-resourceendpoint": "./node-red-resourceendpoint/NodeRedResourceEndpoint.js",
15
+ "./node-red-worker": "./node-red-worker/NodeRedWorker.js",
16
+ "./node-red-workerruntime": "./node-red-worker/NodeRedWorkerRuntime.js",
17
+ "./node-red-popup": "./node-red-popup/NodeRedPopupBackend.js",
18
+ "./node-red-singleton": "./node-red-singleton/NodeRedSingletonBackend.js",
19
+ "./node-red-pubsub-bus": "./node-red-pubsub-bus/NodeRedPubsubBus.js",
20
+ "./node-red-pubsub-bus-workerproxy": "./node-red-pubsub-bus/NodeRedPubsubBusWorkerProxy.js",
21
+ "./node-red-gpu": "./node-red-gpu/index.js",
22
+ "./node-red-gpu/backends/VulkanBackend": "./node-red-gpu/backends/VulkanBackend.js",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "keywords": [
26
+ "node-red",
27
+ "node-red-utils",
28
+ "mixin",
29
+ "custom-nodes",
30
+ "worker-threads"
31
+ ],
32
+ "author": "UpDownMe733",
33
+ "license": "Apache-2.0",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/UpDownMe733/node-red-utils.git"
37
+ },
38
+ "homepage": "https://github.com/UpDownMe733/node-red-utils#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/UpDownMe733/node-red-utils/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "files": [
46
+ "index.js",
47
+ "node-red-base/",
48
+ "node-red-editorcomms/",
49
+ "node-red-gpu/",
50
+ "node-red-popup/",
51
+ "node-red-pubsub-bus/",
52
+ "node-red-resourceendpoint/",
53
+ "node-red-singleton/",
54
+ "node-red-worker/",
55
+ "!node-red-gpu/native/build/",
56
+ "!node-red-gpu/smoke-test.js",
57
+ "CHANGELOG.md"
58
+ ],
59
+ "dependencies": {
60
+ "acorn": "^8.16.0"
61
+ },
62
+ "optionalDependencies": {
63
+ "node-addon-api": "^8.7.0"
64
+ },
65
+ "devDependencies": {
66
+ "node-red": "^4.1.17"
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ }
71
+ }