@bonsae/nrg 0.18.5 → 0.19.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 (36) hide show
  1. package/package.json +1 -1
  2. package/server/index.cjs +86 -9
  3. package/server/resources/nrg-client.js +2020 -1987
  4. package/test/client/component/config.js +11 -0
  5. package/test/client/component/index.js +218 -235
  6. package/test/client/component/nrg.css +1 -0
  7. package/test/client/component/setup.js +1549 -140
  8. package/test/client/e2e/index.js +720 -369
  9. package/test/client/unit/index.js +204 -16
  10. package/test/client/unit/setup.js +209 -19
  11. package/test/server/unit/index.js +25 -4
  12. package/tsconfig/core/client.json +1 -1
  13. package/tsconfig/test/client/component.json +1 -1
  14. package/types/client.d.ts +98 -18
  15. package/types/server.d.ts +50 -12
  16. package/types/shims/brands.d.ts +32 -0
  17. package/types/shims/{form → client/form}/components/node-red-editor-input.vue.d.ts +1 -1
  18. package/types/shims/{form → client/form}/components/node-red-json-schema-form.vue.d.ts +21 -2
  19. package/types/shims/{form → client/form}/components/node-red-select-input.vue.d.ts +1 -0
  20. package/types/shims/{form → client/form}/components/node-red-typed-input.vue.d.ts +1 -0
  21. package/types/shims/client/types.d.ts +206 -0
  22. package/types/shims/components.d.ts +8 -8
  23. package/types/shims/constants.d.ts +4 -0
  24. package/types/shims/schema-options.d.ts +23 -10
  25. package/types/shims/typebox.d.ts +2 -2
  26. package/types/test-client-component.d.ts +170 -55
  27. package/types/test-client-e2e.d.ts +50 -0
  28. package/types/test-client-unit.d.ts +86 -22
  29. package/types/test-server-unit.d.ts +3 -1
  30. package/types/vite.d.ts +38 -9
  31. package/vite/index.js +732 -530
  32. /package/types/shims/{form → client/form}/components/node-red-config-input.vue.d.ts +0 -0
  33. /package/types/shims/{form → client/form}/components/node-red-input-label.vue.d.ts +0 -0
  34. /package/types/shims/{form → client/form}/components/node-red-input.vue.d.ts +0 -0
  35. /package/types/shims/{form → client/form}/components/node-red-toggle.vue.d.ts +0 -0
  36. /package/types/shims/{globals.d.ts → client/globals.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonsae/nrg",
3
- "version": "0.18.5",
3
+ "version": "0.19.0",
4
4
  "description": "NRG framework — build Node-RED nodes with Vue 3, TypeScript, and JSON Schema",
5
5
  "author": "Allan Oricil <allanoricil@duck.com>",
6
6
  "license": "MIT",
package/server/index.cjs CHANGED
@@ -437,10 +437,14 @@ var Node = class _Node {
437
437
  };
438
438
 
439
439
  // src/core/server/nodes/io-node.ts
440
+ var RETURN_PROPERTY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
441
+ var INPUT_KEY = "input";
440
442
  var IONode = class extends Node {
441
443
  static align;
442
444
  static color;
443
445
  static inputSchema;
446
+ // outputsSchema accepts any schema shape: with returnProperty the raw sent
447
+ // value is validated, and results are frequently non-objects.
444
448
  static outputsSchema;
445
449
  static validateInput = false;
446
450
  static validateOutput = false;
@@ -468,6 +472,12 @@ var IONode = class extends Node {
468
472
  return keys.length;
469
473
  }
470
474
  #send;
475
+ /**
476
+ * Most recent input message — the spread base for returnProperty wrapping. Not
477
+ * cleared after input() so late async sends merge with the last received
478
+ * message.
479
+ */
480
+ #currentInputMsg;
471
481
  context;
472
482
  constructor(RED, node, config, credentials) {
473
483
  super(RED, node, config, credentials);
@@ -480,6 +490,12 @@ var IONode = class extends Node {
480
490
  fn.flow = setupContext(context.flow);
481
491
  fn.global = setupContext(context.global);
482
492
  this.context = fn;
493
+ const returnPropertyKey = this.#returnPropertyKey();
494
+ if (!RETURN_PROPERTY_PATTERN.test(returnPropertyKey)) {
495
+ throw new NrgError(
496
+ `Invalid returnProperty key "${returnPropertyKey}" in ${this.constructor.type} \u2014 it must be a valid JavaScript identifier (letters, digits, _, $; not starting with a digit)`
497
+ );
498
+ }
483
499
  }
484
500
  [WIRE_HANDLERS](nodeRedNode, createdPromise) {
485
501
  super[WIRE_HANDLERS](nodeRedNode, createdPromise);
@@ -495,12 +511,14 @@ var IONode = class extends Node {
495
511
  }
496
512
  try {
497
513
  nodeRedNode.log("Calling input");
514
+ this.#currentInputMsg = msg;
498
515
  await Promise.resolve(this.#input(msg, send));
499
516
  this.#sendToPort("complete", {
500
517
  ...msg,
501
518
  complete: {
502
519
  source: this.#nodeSource()
503
- }
520
+ },
521
+ [INPUT_KEY]: msg
504
522
  });
505
523
  done();
506
524
  nodeRedNode.log("Input processed");
@@ -511,7 +529,8 @@ var IONode = class extends Node {
511
529
  error: {
512
530
  message: errorMsg,
513
531
  source: this.#nodeSource()
514
- }
532
+ },
533
+ [INPUT_KEY]: msg
515
534
  });
516
535
  if (error instanceof Error) {
517
536
  nodeRedNode.error(
@@ -550,8 +569,9 @@ var IONode = class extends Node {
550
569
  this.#send = void 0;
551
570
  }
552
571
  }
553
- send(msg) {
572
+ send(msg, contextMode = "nest") {
554
573
  const NodeClass = this.constructor;
574
+ const sendsValue = this.baseOutputs <= 1;
555
575
  const shouldValidateOutput = this.config.validateOutput ?? NodeClass.validateOutput;
556
576
  if (shouldValidateOutput && NodeClass.outputsSchema) {
557
577
  this.log("Validating output");
@@ -566,7 +586,7 @@ var IONode = class extends Node {
566
586
  });
567
587
  }
568
588
  } else if (isSchemaLike(rawSchema)) {
569
- if (Array.isArray(msg)) {
589
+ if (Array.isArray(msg) && !sendsValue) {
570
590
  const msgs = msg;
571
591
  for (let i = 0; i < msgs.length; i++) {
572
592
  if (msgs[i] == null) continue;
@@ -594,13 +614,57 @@ var IONode = class extends Node {
594
614
  }
595
615
  this.log("Output is valid");
596
616
  }
597
- const out = Array.isArray(msg) ? msg.slice(0, this.baseOutputs) : msg;
617
+ const truncated = Array.isArray(msg) && !sendsValue ? msg.slice(0, this.baseOutputs) : msg;
618
+ const out = Array.isArray(truncated) && !sendsValue ? truncated.map(
619
+ (m) => m == null ? m : this.#wrapOutgoing(m, contextMode)
620
+ ) : truncated == null ? truncated : this.#wrapOutgoing(truncated, contextMode);
598
621
  if (this.#send) {
599
622
  this.#send(out);
600
623
  } else {
601
624
  this.node.send(out);
602
625
  }
603
626
  }
627
+ /**
628
+ * Resolves the active return key. `null` = the node did not declare
629
+ * `returnProperty` in its configSchema, so its code owns the outgoing message
630
+ * shape (no wrapping).
631
+ */
632
+ /**
633
+ * Every node has a return property — `"output"` by default. Declaring
634
+ * `SchemaType.ReturnProperty()` in the configSchema doesn't create it; it
635
+ * only exposes the key to the flow author so they can override it in the
636
+ * editor (and lets the node pick a different default). So `this.send(x)`
637
+ * always means "x is the value at the return key", never "x is the whole
638
+ * outgoing message".
639
+ */
640
+ #returnPropertyKey() {
641
+ const NodeClass = this.constructor;
642
+ const declared = NodeClass.configSchema?.properties?.returnProperty;
643
+ const configured = this.config.returnProperty;
644
+ if (typeof configured === "string" && configured.trim()) {
645
+ return configured.trim();
646
+ }
647
+ if (declared && typeof declared.default === "string" && declared.default) {
648
+ return declared.default;
649
+ }
650
+ return "output";
651
+ }
652
+ /**
653
+ * Merges a sent value into the incoming message at the returnProperty key so
654
+ * upstream message properties propagate. A fresh base is built per call so
655
+ * multi-port sends never share an object.
656
+ */
657
+ #wrapOutgoing(value, mode = "nest") {
658
+ const key = this.#returnPropertyKey();
659
+ const input = this.#currentInputMsg ?? {};
660
+ if (mode === "reset") {
661
+ return { [key]: value };
662
+ }
663
+ if (mode === "carry") {
664
+ return { ...input, [key]: value };
665
+ }
666
+ return { ...input, [key]: value, [INPUT_KEY]: input };
667
+ }
604
668
  // --- Built-in port management ---
605
669
  get baseOutputs() {
606
670
  return this.constructor.outputs ?? 0;
@@ -623,13 +687,16 @@ var IONode = class extends Node {
623
687
  * throw an error or call `this.error()` for the error port, and the complete
624
688
  * port is sent automatically on successful input processing.
625
689
  */
626
- sendToPort(port, msg) {
690
+ sendToPort(port, msg, contextMode = "nest") {
627
691
  if (port === "error" || port === "complete" || port === "status") {
628
692
  throw new NrgError(
629
693
  `sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
630
694
  );
631
695
  }
632
- this.#sendToPort(port, msg);
696
+ this.#sendToPort(
697
+ port,
698
+ msg == null ? msg : this.#wrapOutgoing(msg, contextMode)
699
+ );
633
700
  }
634
701
  #sendToPort(port, msg) {
635
702
  let portIndex;
@@ -687,7 +754,8 @@ var IONode = class extends Node {
687
754
  error: {
688
755
  message,
689
756
  source: this.#nodeSource()
690
- }
757
+ },
758
+ [INPUT_KEY]: msg
691
759
  });
692
760
  }
693
761
  }
@@ -1095,9 +1163,18 @@ function TypedInput2(options) {
1095
1163
  [import_typebox2.Kind]: "TypedInput"
1096
1164
  };
1097
1165
  }
1166
+ function ReturnProperty(options) {
1167
+ return import_typebox2.Type.String({
1168
+ description: "Message property that receives this node's result. The rest of the incoming message is propagated unchanged, and the prior message is kept under `input`.",
1169
+ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$",
1170
+ default: "output",
1171
+ ...options
1172
+ });
1173
+ }
1098
1174
  var SchemaType = Object.assign({}, import_typebox2.Type, {
1099
1175
  NodeRef,
1100
- TypedInput: TypedInput2
1176
+ TypedInput: TypedInput2,
1177
+ ReturnProperty
1101
1178
  });
1102
1179
  function markNonValidatable(schema) {
1103
1180
  const type = schema.type;