@nrutils/node-red-utils 0.0.0-stage → 0.2.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 +20 -0
  2. package/LICENSE +190 -0
  3. package/README.md +187 -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 +508 -0
  29. package/node-red-popup/README.md +113 -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 +443 -0
  39. package/node-red-worker/NodeRedWorkerRuntime.js +659 -0
  40. package/node-red-worker/README.md +273 -0
  41. package/node-red-worker/workerRuntimeLoader.js +55 -0
  42. package/package.json +70 -5
@@ -0,0 +1,461 @@
1
+ 'use strict';
2
+
3
+ const { GpuCompileError } = require('../errors.js');
4
+ const { isReservedGlslName } = require('./glslNames.js');
5
+
6
+ const ALLOWED_BINARY_OPS = new Set([
7
+ '+', '-', '*', '/', '%',
8
+ '&', '|', '^', '<<', '>>',
9
+ '==', '!=', '===', '!==',
10
+ '<', '>', '<=', '>=',
11
+ '&&', '||',
12
+ ]);
13
+
14
+ const ALLOWED_UNARY_OPS = new Set(['-', '~', '!']);
15
+
16
+ const ALLOWED_ASSIGN_OPS = new Set([
17
+ '=', '+=', '-=', '*=', '/=', '%=',
18
+ '&=', '|=', '^=', '<<=', '>>=',
19
+ ]);
20
+
21
+ const ATOMIC_FUNCS = new Set([
22
+ 'atomicAdd', 'atomicMin', 'atomicMax',
23
+ 'atomicOr', 'atomicAnd', 'atomicXor',
24
+ 'atomicExchange', 'atomicCompSwap',
25
+ ]);
26
+
27
+ const ALLOWED_MATH_FUNCS = new Set([
28
+ 'abs', 'ceil', 'floor', 'round', 'sign',
29
+ 'min', 'max', 'clamp',
30
+ 'sqrt', 'pow', 'exp', 'exp2', 'log', 'log2',
31
+ 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2',
32
+ 'mix', 'step', 'smoothstep', 'fract', 'mod',
33
+ ]);
34
+
35
+ const THREAD_PROPS = new Set(['x', 'y', 'z', 'localX', 'localY', 'localZ']);
36
+
37
+ function loc(node) {
38
+ if (node.loc) {
39
+ return `(line ${node.loc.start.line}, col ${node.loc.start.column})`;
40
+ }
41
+ return '';
42
+ }
43
+
44
+ function err(node, msg) {
45
+ throw new GpuCompileError(
46
+ `${msg} ${loc(node)}`.trim(),
47
+ loc(node)
48
+ );
49
+ }
50
+
51
+ function validateMemberExpression(node, paramNames) {
52
+ // this.thread.x / this.thread.y / this.thread.z
53
+ if (
54
+ node.object.type === 'MemberExpression' &&
55
+ node.object.object.type === 'ThisExpression' &&
56
+ node.object.property.type === 'Identifier' &&
57
+ node.object.property.name === 'thread' &&
58
+ !node.computed &&
59
+ node.property.type === 'Identifier' &&
60
+ THREAD_PROPS.has(node.property.name)
61
+ ) {
62
+ return;
63
+ }
64
+
65
+ // this.constants.NAME
66
+ if (
67
+ node.object.type === 'MemberExpression' &&
68
+ node.object.object.type === 'ThisExpression' &&
69
+ node.object.property.type === 'Identifier' &&
70
+ node.object.property.name === 'constants' &&
71
+ !node.computed &&
72
+ node.property.type === 'Identifier'
73
+ ) {
74
+ return;
75
+ }
76
+
77
+ // Math.FUNC (validated further at CallExpression level)
78
+ if (
79
+ node.object.type === 'Identifier' &&
80
+ node.object.name === 'Math' &&
81
+ !node.computed &&
82
+ node.property.type === 'Identifier'
83
+ ) {
84
+ return;
85
+ }
86
+
87
+ // args[N][expr] — args is a special implicit buffer array
88
+ if (
89
+ node.computed &&
90
+ node.object.type === 'MemberExpression' &&
91
+ node.object.object.type === 'Identifier' &&
92
+ node.object.object.name === 'args' &&
93
+ node.object.computed
94
+ ) {
95
+ walk(node.object.property, paramNames);
96
+ walk(node.property, paramNames);
97
+ return;
98
+ }
99
+
100
+ // paramName[expr] — direct buffer param access
101
+ if (
102
+ node.computed &&
103
+ node.object.type === 'Identifier' &&
104
+ paramNames.includes(node.object.name)
105
+ ) {
106
+ walk(node.property, paramNames);
107
+ return;
108
+ }
109
+
110
+ if (node.computed && node.object.type === 'Identifier') {
111
+ err(node, `'${node.object.name}' is not a buffer. Only buffers can be indexed, and a kernel cannot use variables from outside its function.`);
112
+ }
113
+ err(node, `Unsupported member expression on unknown object.`);
114
+ }
115
+
116
+ function validateCallExpression(node, paramNames) {
117
+ // Allow barrier() call
118
+ if (node.callee.type === 'Identifier' && node.callee.name === 'barrier') {
119
+ if (node.arguments.length !== 0) err(node, 'barrier() takes no arguments');
120
+ return;
121
+ }
122
+
123
+ // Allow type cast calls: float(x), int(x), uint(x)
124
+ if (node.callee.type === 'Identifier') {
125
+ const castFuncs = new Set(['float', 'int', 'uint']);
126
+ if (castFuncs.has(node.callee.name)) {
127
+ if (node.arguments.length !== 1) err(node, `${node.callee.name}() requires exactly 1 argument`);
128
+ walk(node.arguments[0], paramNames);
129
+ return;
130
+ }
131
+ }
132
+
133
+ // Atomic operations: atomicAdd(buf, idx, val), atomicCompSwap(buf, idx, cmp, val)
134
+ if (node.callee.type === 'Identifier' && ATOMIC_FUNCS.has(node.callee.name)) {
135
+ if (node.arguments.length < 3)
136
+ err(node, `${node.callee.name}(buffer, index, value) requires at least 3 arguments`);
137
+ if (node.callee.name === 'atomicCompSwap' && node.arguments.length < 4)
138
+ err(node, 'atomicCompSwap(buffer, index, compare, value) requires 4 arguments');
139
+ for (const arg of node.arguments) walk(arg, paramNames);
140
+ return;
141
+ }
142
+
143
+ if (
144
+ node.callee.type !== 'MemberExpression' ||
145
+ node.callee.object.type !== 'Identifier' ||
146
+ node.callee.object.name !== 'Math' ||
147
+ node.callee.computed ||
148
+ node.callee.property.type !== 'Identifier'
149
+ ) {
150
+ err(node, `Only Math.* calls are allowed in kernel functions.`);
151
+ }
152
+
153
+ const funcName = node.callee.property.name;
154
+ if (!ALLOWED_MATH_FUNCS.has(funcName)) {
155
+ err(node, `Math.${funcName} is not a supported math function.`);
156
+ }
157
+
158
+ for (const arg of node.arguments) {
159
+ walk(arg, paramNames);
160
+ }
161
+ }
162
+
163
+ function walk(node, paramNames) {
164
+ if (!node) return;
165
+
166
+ switch (node.type) {
167
+ case 'File':
168
+ walk(node.program, paramNames);
169
+ break;
170
+
171
+ case 'Program':
172
+ for (const stmt of node.body) walk(stmt, paramNames);
173
+ break;
174
+
175
+ case 'BlockStatement':
176
+ for (const stmt of node.body) walk(stmt, paramNames);
177
+ break;
178
+
179
+ case 'ExpressionStatement':
180
+ walk(node.expression, paramNames);
181
+ break;
182
+
183
+ case 'VariableDeclaration':
184
+ if (node.kind === 'var') {
185
+ err(node, `'var' declarations are not allowed; use 'let' or 'const'.`);
186
+ }
187
+ for (const decl of node.declarations) {
188
+ if (decl.init) walk(decl.init, paramNames);
189
+ }
190
+ break;
191
+
192
+ case 'AssignmentExpression':
193
+ if (!ALLOWED_ASSIGN_OPS.has(node.operator)) {
194
+ err(node, `Assignment operator '${node.operator}' is not supported.`);
195
+ }
196
+ walk(node.left, paramNames);
197
+ walk(node.right, paramNames);
198
+ break;
199
+
200
+ case 'BinaryExpression':
201
+ case 'LogicalExpression':
202
+ if (!ALLOWED_BINARY_OPS.has(node.operator)) {
203
+ err(node, `Binary operator '${node.operator}' is not supported.`);
204
+ }
205
+ walk(node.left, paramNames);
206
+ walk(node.right, paramNames);
207
+ break;
208
+
209
+ case 'UnaryExpression':
210
+ if (!ALLOWED_UNARY_OPS.has(node.operator)) {
211
+ err(node, `Unary operator '${node.operator}' is not supported.`);
212
+ }
213
+ walk(node.argument, paramNames);
214
+ break;
215
+
216
+ case 'UpdateExpression':
217
+ // ++ and -- only
218
+ if (node.operator !== '++' && node.operator !== '--') {
219
+ err(node, `Update operator '${node.operator}' is not supported.`);
220
+ }
221
+ walk(node.argument, paramNames);
222
+ break;
223
+
224
+ case 'ConditionalExpression':
225
+ walk(node.test, paramNames);
226
+ walk(node.consequent, paramNames);
227
+ walk(node.alternate, paramNames);
228
+ break;
229
+
230
+ case 'IfStatement':
231
+ walk(node.test, paramNames);
232
+ walk(node.consequent, paramNames);
233
+ if (node.alternate) walk(node.alternate, paramNames);
234
+ break;
235
+
236
+ case 'ForStatement': {
237
+ if (node.init) {
238
+ if (
239
+ node.init.type === 'VariableDeclaration' &&
240
+ node.init.kind === 'var'
241
+ ) {
242
+ err(node.init, `'var' declarations are not allowed in for-loop init; use 'let'.`);
243
+ }
244
+ if (
245
+ node.init.type !== 'VariableDeclaration' &&
246
+ node.init.type !== 'AssignmentExpression' &&
247
+ node.init.type !== 'ExpressionStatement'
248
+ ) {
249
+ err(node.init, `For-loop init must be a let declaration or assignment expression.`);
250
+ }
251
+ walk(node.init, paramNames);
252
+ }
253
+ if (!node.test) {
254
+ err(node, `For-loop must have a test condition.`);
255
+ }
256
+ if (!node.update) {
257
+ err(node, `For-loop must have an update expression.`);
258
+ }
259
+ walk(node.test, paramNames);
260
+ walk(node.update, paramNames);
261
+ walk(node.body, paramNames);
262
+ break;
263
+ }
264
+
265
+ case 'BreakStatement':
266
+ case 'ContinueStatement':
267
+ // Allowed inside for-loops; loop structure enforcement left to codegen
268
+ break;
269
+
270
+ case 'ReturnStatement':
271
+ if (node.argument) walk(node.argument, paramNames);
272
+ break;
273
+
274
+ case 'MemberExpression':
275
+ validateMemberExpression(node, paramNames);
276
+ break;
277
+
278
+ case 'CallExpression':
279
+ validateCallExpression(node, paramNames);
280
+ break;
281
+
282
+ case 'Identifier':
283
+ // Plain identifiers (variable names) are always allowed
284
+ break;
285
+
286
+ case 'Literal':
287
+ if (typeof node.value === 'string') {
288
+ err(node, `String literals are not allowed in kernel functions.`);
289
+ }
290
+ if (node.regex !== undefined) {
291
+ err(node, `RegExp literals are not allowed in kernel functions.`);
292
+ }
293
+ // numeric literals (int / float) are fine
294
+ break;
295
+
296
+ case 'ThisExpression':
297
+ // Allowed as part of this.thread.* / this.constants.*
298
+ break;
299
+
300
+ // --- Rejected constructs ---
301
+
302
+ case 'WhileStatement':
303
+ case 'DoWhileStatement':
304
+ err(node, `'${node.type}' is not supported; use a for-loop.`);
305
+ break;
306
+
307
+ case 'FunctionDeclaration':
308
+ case 'FunctionExpression':
309
+ case 'ArrowFunctionExpression':
310
+ err(node, `Nested functions are not allowed in kernel functions.`);
311
+ break;
312
+
313
+ case 'ClassDeclaration':
314
+ case 'ClassExpression':
315
+ err(node, `Class declarations are not allowed in kernel functions.`);
316
+ break;
317
+
318
+ case 'TryStatement':
319
+ err(node, `'try/catch' is not allowed in kernel functions.`);
320
+ break;
321
+
322
+ case 'ThrowStatement':
323
+ err(node, `'throw' is not allowed in kernel functions.`);
324
+ break;
325
+
326
+ case 'ImportDeclaration':
327
+ case 'ExportNamedDeclaration':
328
+ case 'ExportDefaultDeclaration':
329
+ case 'ExportAllDeclaration':
330
+ err(node, `Import/export declarations are not allowed in kernel functions.`);
331
+ break;
332
+
333
+ case 'TemplateLiteral':
334
+ case 'TaggedTemplateExpression':
335
+ err(node, `Template literals are not allowed in kernel functions.`);
336
+ break;
337
+
338
+ case 'ObjectExpression':
339
+ err(node, `Object literals are not allowed in kernel functions.`);
340
+ break;
341
+
342
+ case 'ArrayExpression':
343
+ err(node, `Array literals are not allowed in kernel functions.`);
344
+ break;
345
+
346
+ case 'NewExpression':
347
+ err(node, `'new' expressions are not allowed in kernel functions.`);
348
+ break;
349
+
350
+ case 'AwaitExpression':
351
+ err(node, `'await' is not allowed in kernel functions.`);
352
+ break;
353
+
354
+ case 'YieldExpression':
355
+ err(node, `'yield' is not allowed in kernel functions.`);
356
+ break;
357
+
358
+ case 'ForInStatement':
359
+ err(node, `'for...in' is not allowed in kernel functions; use a numeric for-loop.`);
360
+ break;
361
+
362
+ case 'ForOfStatement':
363
+ err(node, `'for...of' is not allowed in kernel functions; use a numeric for-loop.`);
364
+ break;
365
+
366
+ case 'SwitchStatement':
367
+ err(node, `'switch' is not allowed in kernel functions; use if/else.`);
368
+ break;
369
+
370
+ default:
371
+ err(node, `Unsupported AST node type '${node.type}'.`);
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Validate an acorn AST against the supported JS subset for GLSL 450 compute shader transpilation.
377
+ *
378
+ * @param {object} ast - acorn AST node (Program or BlockStatement of the kernel function body)
379
+ * @param {string[]} paramNames - names of buffer parameters declared for the kernel
380
+ * @throws {GpuCompileError} on first unsupported construct
381
+ */
382
+ function validate(ast, paramNames, opts = {}) {
383
+ walk(ast, paramNames || []);
384
+ checkNames(ast, paramNames || [], opts);
385
+ }
386
+
387
+ /** Collect the names of all `let` and `const` variables in the kernel body. */
388
+ function collectLocals(node, locals = new Map()) {
389
+ if (!node || typeof node !== 'object') return locals;
390
+ if (node.type === 'VariableDeclarator') {
391
+ if (node.id.type !== 'Identifier') err(node, 'Destructuring is not allowed in kernel functions.');
392
+ locals.set(node.id.name, node.id);
393
+ }
394
+ for (const key of Object.keys(node)) {
395
+ if (key === 'loc') continue;
396
+ const child = node[key];
397
+ if (Array.isArray(child)) child.forEach(c => collectLocals(c, locals));
398
+ else if (child && typeof child === 'object') collectLocals(child, locals);
399
+ }
400
+ return locals;
401
+ }
402
+
403
+ /** Find variable reads and writes that are not buffers or locals. */
404
+ function findFreeVariables(node, known, found = new Map()) {
405
+ if (!node || typeof node !== 'object') return found;
406
+ switch (node.type) {
407
+ case 'Identifier':
408
+ if (!known.has(node.name) && !found.has(node.name)) found.set(node.name, node);
409
+ return found;
410
+ case 'MemberExpression':
411
+ // Math.x and this.* are allowed; a non-computed property is not a variable
412
+ if (!(node.object.type === 'Identifier' && node.object.name === 'Math')) findFreeVariables(node.object, known, found);
413
+ if (node.computed) findFreeVariables(node.property, known, found);
414
+ return found;
415
+ case 'CallExpression':
416
+ // validate() allows only Math.*, casts, barrier() and atomics as callees
417
+ if (node.callee.type !== 'Identifier') findFreeVariables(node.callee, known, found);
418
+ node.arguments.forEach(a => findFreeVariables(a, known, found));
419
+ return found;
420
+ case 'VariableDeclarator':
421
+ findFreeVariables(node.init, known, found);
422
+ return found;
423
+ }
424
+ for (const key of Object.keys(node)) {
425
+ if (key === 'loc') continue;
426
+ const child = node[key];
427
+ if (Array.isArray(child)) child.forEach(c => findFreeVariables(c, known, found));
428
+ else if (child && typeof child === 'object') findFreeVariables(child, known, found);
429
+ }
430
+ return found;
431
+ }
432
+
433
+ /**
434
+ * Reject names that the CPU backend accepts but GLSL does not, and variables from
435
+ * outside the kernel function, which the CPU backend cannot see either.
436
+ */
437
+ function checkNames(ast, paramNames, opts) {
438
+ const specNames = Array.isArray(opts.specConstants) ? opts.specConstants : Object.keys(opts.specConstants || {});
439
+ const locals = collectLocals(ast);
440
+ const named = [
441
+ ...paramNames.map(n => [n, 'buffer']),
442
+ ...Object.keys(opts.constants || {}).map(n => [n, 'constant']),
443
+ ...specNames.map(n => [n, 'constant']),
444
+ ...[...locals.keys()].map(n => [n, 'local variable']),
445
+ ];
446
+ for (const [name, kind] of named) {
447
+ if (isReservedGlslName(name)) {
448
+ const where = kind === 'local variable' ? ` ${loc(locals.get(name))}` : '';
449
+ throw new GpuCompileError(`The ${kind} name '${name}' is a reserved word or built-in name in GLSL. Rename it.${where}`, where);
450
+ }
451
+ }
452
+ const free = findFreeVariables(ast, new Set([...paramNames, ...locals.keys()]));
453
+ if (free.size > 0) {
454
+ const names = [...free.keys()].map(n => `'${n}'`).join(', ');
455
+ const first = free.values().next().value;
456
+ err(first, `The kernel uses ${names}, which ${free.size > 1 ? 'are not buffers or local variables' : 'is not a buffer or a local variable'}. ` +
457
+ 'A kernel cannot use variables from outside its function; pass values in this.constants.');
458
+ }
459
+ }
460
+
461
+ module.exports = { validate, ATOMIC_FUNCS };
@@ -0,0 +1,69 @@
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
+ //
10
+ // Add this to the HTML file
11
+ // <script type="text/javascript" src="node-red-resourceendpoint/node-red-popup/NodeRedPopupFrontend.js"></script>
12
+ //
13
+
14
+ const path = require('path');
15
+ const { NodeRedResourceEndpoint } = require('../node-red-resourceendpoint/NodeRedResourceEndpoint.js');
16
+
17
+ /** Serve NodeRedPopupFrontend.js. Safe to call more than one time. */
18
+ function serveFrontend(RED) {
19
+ new NodeRedResourceEndpoint(RED).registerResources('node-red-popup', path.join(__dirname, 'NodeRedPopupFrontend.js'));
20
+ }
21
+
22
+ /**
23
+ * NodeRedPopup - Class for providing popup support in the Node-RED editor
24
+ * Serves the PopupSupport.js file to add popup functionality to nodes
25
+ */
26
+ class NodeRedPopup {
27
+
28
+ /**
29
+ * Create a new popup support handler
30
+ * @param {object} RED - The Node-RED runtime instance
31
+ * @param {object} node - The node instance
32
+ */
33
+ constructor(RED, node) {
34
+ this.RED = RED;
35
+ this.node = node;
36
+
37
+ serveFrontend(RED);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Mixin that adds popup support capabilities to Node-RED nodes
43
+ *
44
+ * Enables nodes to have popup elements in the editor
45
+ * Following the same functional mixin pattern used by other mixins
46
+ *
47
+ * @param {class} superclass - The class to extend with popup capabilities
48
+ * @returns {class} A new class that extends the superclass with popup functionality
49
+ */
50
+ const nodeRedPopup = (superclass) => class extends superclass {
51
+ // Serve the frontend script before the first deploy
52
+ static onRegisterType(RED) {
53
+ super.onRegisterType?.(RED);
54
+ serveFrontend(RED);
55
+ }
56
+
57
+ constructor(RED, thisNode, config) {
58
+ super(RED, thisNode, config);
59
+
60
+ // No methods needed on the node instance as this is just for serving the JS file
61
+ this.nodeRedPopup = {
62
+ _parent: this
63
+ };
64
+ }
65
+ };
66
+
67
+ // Export both the class and the mixin
68
+ module.exports.nodeRedPopup = nodeRedPopup;
69
+ module.exports.NodeRedPopup = NodeRedPopup;