@bonsae/nrg 0.20.0 → 0.21.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/README.md +8 -0
- package/package.json +1 -1
- package/server/index.cjs +98 -86
- package/server/resources/nrg-client.js +2361 -2105
- package/test/client/component/setup.js +22 -21
- package/test/client/e2e/index.js +307 -52
- package/types/client.d.ts +17 -4
- package/types/server.d.ts +41 -25
- package/types/shims/client/types.d.ts +17 -4
- package/types/test-client-component.d.ts +9 -4
- package/types/test-client-unit.d.ts +9 -4
- package/types/vite.d.ts +1 -1
- package/vite/index.js +302 -51
package/README.md
CHANGED
|
@@ -130,6 +130,14 @@ export default defineModule({
|
|
|
130
130
|
|
|
131
131
|
See the [consumer template](https://github.com/AllanOricil/node-red-vue-template) for a complete example.
|
|
132
132
|
|
|
133
|
+
### The generated editor form
|
|
134
|
+
|
|
135
|
+
nrg builds the node's edit dialog from your schema — no HTML or jQuery. Your config fields render first, then a **Ports Settings** section (input/output validation, return key, and per-port [context modes](https://bonsaedev.github.io/nrg/guide/schemas#context-modes)) and a **Lifecycle Ports** section (error / complete / status):
|
|
136
|
+
|
|
137
|
+
<p align="center">
|
|
138
|
+
<img alt="nrg generated editor form" src="docs/public/editor-form.png" width="360"/>
|
|
139
|
+
</p>
|
|
140
|
+
|
|
133
141
|
## Testing
|
|
134
142
|
|
|
135
143
|
NRG provides five test libraries and bundles most test infrastructure as direct dependencies. Install `vitest` plus any optional peer dependencies you need:
|
package/package.json
CHANGED
package/server/index.cjs
CHANGED
|
@@ -443,8 +443,8 @@ var IONode = class extends Node {
|
|
|
443
443
|
static align;
|
|
444
444
|
static color;
|
|
445
445
|
static inputSchema;
|
|
446
|
-
// outputsSchema accepts any schema shape:
|
|
447
|
-
//
|
|
446
|
+
// outputsSchema accepts any schema shape: the raw sent value (per port) is
|
|
447
|
+
// validated, and results are frequently non-objects.
|
|
448
448
|
static outputsSchema;
|
|
449
449
|
static validateInput = false;
|
|
450
450
|
static validateOutput = false;
|
|
@@ -473,7 +473,7 @@ var IONode = class extends Node {
|
|
|
473
473
|
}
|
|
474
474
|
#send;
|
|
475
475
|
/**
|
|
476
|
-
* Most recent input message — the spread base for
|
|
476
|
+
* Most recent input message — the spread base for output wrapping. Not
|
|
477
477
|
* cleared after input() so late async sends merge with the last received
|
|
478
478
|
* message.
|
|
479
479
|
*/
|
|
@@ -490,11 +490,15 @@ var IONode = class extends Node {
|
|
|
490
490
|
fn.flow = setupContext(context.flow);
|
|
491
491
|
fn.global = setupContext(context.global);
|
|
492
492
|
this.context = fn;
|
|
493
|
-
const
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
493
|
+
const outputReturnProperties = this.config.outputReturnProperties;
|
|
494
|
+
if (outputReturnProperties) {
|
|
495
|
+
for (const [port, key] of Object.entries(outputReturnProperties)) {
|
|
496
|
+
if (typeof key === "string" && key.trim() && !RETURN_PROPERTY_PATTERN.test(key.trim())) {
|
|
497
|
+
throw new NrgError(
|
|
498
|
+
`Invalid return property "${key}" for output port ${port} in ${this.constructor.type} \u2014 it must be a valid JavaScript identifier (letters, digits, _, $; not starting with a digit)`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
498
502
|
}
|
|
499
503
|
}
|
|
500
504
|
[WIRE_HANDLERS](nodeRedNode, createdPromise) {
|
|
@@ -569,55 +573,33 @@ var IONode = class extends Node {
|
|
|
569
573
|
this.#send = void 0;
|
|
570
574
|
}
|
|
571
575
|
}
|
|
572
|
-
send(msg,
|
|
573
|
-
const NodeClass = this.constructor;
|
|
576
|
+
send(msg, options) {
|
|
574
577
|
const sendsValue = this.baseOutputs <= 1;
|
|
575
|
-
const
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
this
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
this.RED.validator.validate(msgs[i], rawSchema, {
|
|
594
|
-
cacheKey: rawSchema.$id || `${NodeClass.type}:output-schema`,
|
|
595
|
-
throwOnError: true
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
} else {
|
|
599
|
-
this.RED.validator.validate(msg, rawSchema, {
|
|
600
|
-
cacheKey: rawSchema.$id || `${NodeClass.type}:output-schema`,
|
|
601
|
-
throwOnError: true
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
} else {
|
|
605
|
-
const schemaArray = Object.values(rawSchema);
|
|
606
|
-
const msgs = msg;
|
|
607
|
-
for (let i = 0; i < schemaArray.length; i++) {
|
|
608
|
-
if (msgs[i] == null) continue;
|
|
609
|
-
this.RED.validator.validate(msgs[i], schemaArray[i], {
|
|
610
|
-
cacheKey: schemaArray[i].$id || `${NodeClass.type}:output-schema:${i}`,
|
|
611
|
-
throwOnError: true
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
this.log("Output is valid");
|
|
578
|
+
const defaultMode = options?.defaultMode;
|
|
579
|
+
if (Array.isArray(msg) && !sendsValue) {
|
|
580
|
+
const slots = msg.slice(0, this.baseOutputs);
|
|
581
|
+
const out = slots.map((m, port) => {
|
|
582
|
+
if (m == null) return m;
|
|
583
|
+
this.#validatePort(m, port);
|
|
584
|
+
return this.#wrapOutgoing(
|
|
585
|
+
m,
|
|
586
|
+
this.#resolveContextMode(port, defaultMode),
|
|
587
|
+
port
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
this.#deliver(out);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (msg == null) {
|
|
594
|
+
this.#deliver(msg);
|
|
595
|
+
return;
|
|
616
596
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
(
|
|
620
|
-
)
|
|
597
|
+
this.#validatePort(msg, 0);
|
|
598
|
+
this.#deliver(
|
|
599
|
+
this.#wrapOutgoing(msg, this.#resolveContextMode(0, defaultMode), 0)
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
#deliver(out) {
|
|
621
603
|
if (this.#send) {
|
|
622
604
|
this.#send(out);
|
|
623
605
|
} else {
|
|
@@ -625,45 +607,70 @@ var IONode = class extends Node {
|
|
|
625
607
|
}
|
|
626
608
|
}
|
|
627
609
|
/**
|
|
628
|
-
*
|
|
629
|
-
* `
|
|
630
|
-
*
|
|
610
|
+
* Per-port output validation. A port validates when its flow-author flag
|
|
611
|
+
* (`config.validateOutputs[port]`) — or the node's static `validateOutput`
|
|
612
|
+
* fallback — is on and a schema exists for that port.
|
|
631
613
|
*/
|
|
614
|
+
#validatePort(value, port) {
|
|
615
|
+
const NodeClass = this.constructor;
|
|
616
|
+
const configured = this.config.validateOutputs?.[port];
|
|
617
|
+
if (!(configured ?? NodeClass.validateOutput)) return;
|
|
618
|
+
const schema = this.#outputSchemaForPort(port);
|
|
619
|
+
if (!schema) return;
|
|
620
|
+
this.log("Validating output");
|
|
621
|
+
this.RED.validator.validate(value, schema, {
|
|
622
|
+
cacheKey: schema.$id || `${NodeClass.type}:output-schema:${port}`,
|
|
623
|
+
throwOnError: true
|
|
624
|
+
});
|
|
625
|
+
this.log("Output is valid");
|
|
626
|
+
}
|
|
627
|
+
/** Resolves the output schema for a base-output port: array → `[port]`,
|
|
628
|
+
* record → the port-th value, single schema → itself. */
|
|
629
|
+
#outputSchemaForPort(port) {
|
|
630
|
+
const raw = this.constructor.outputsSchema;
|
|
631
|
+
if (!raw) return void 0;
|
|
632
|
+
if (Array.isArray(raw)) return raw[port];
|
|
633
|
+
if (isSchemaLike(raw)) return raw;
|
|
634
|
+
return Object.values(raw)[port];
|
|
635
|
+
}
|
|
632
636
|
/**
|
|
633
|
-
*
|
|
634
|
-
* `
|
|
635
|
-
* only
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
* outgoing message".
|
|
637
|
+
* The return key for an output port — `"output"` unless a custom one is set
|
|
638
|
+
* via `outputReturnProperties[port]` (author default and/or flow-author
|
|
639
|
+
* override, only possible when the node declares `outputReturnProperties`).
|
|
640
|
+
* `this.send(x)` always means "x is the value at this port's return key",
|
|
641
|
+
* never "x is the whole outgoing message".
|
|
639
642
|
*/
|
|
640
|
-
#returnPropertyKey() {
|
|
641
|
-
const
|
|
642
|
-
const declared = NodeClass.configSchema?.properties?.returnProperty;
|
|
643
|
-
const configured = this.config.returnProperty;
|
|
643
|
+
#returnPropertyKey(port) {
|
|
644
|
+
const configured = this.config.outputReturnProperties?.[port];
|
|
644
645
|
if (typeof configured === "string" && configured.trim()) {
|
|
645
646
|
return configured.trim();
|
|
646
647
|
}
|
|
647
|
-
if (declared && typeof declared.default === "string" && declared.default) {
|
|
648
|
-
return declared.default;
|
|
649
|
-
}
|
|
650
648
|
return "output";
|
|
651
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Resolves the context mode for a base-output port. Precedence: the flow
|
|
652
|
+
* author's per-port override (`config.contextModes[port]`) → the node author's
|
|
653
|
+
* `defaultMode` → `"carry"`.
|
|
654
|
+
*/
|
|
655
|
+
#resolveContextMode(port, defaultMode) {
|
|
656
|
+
const configured = this.config.contextModes?.[port];
|
|
657
|
+
return configured ?? defaultMode ?? "carry";
|
|
658
|
+
}
|
|
652
659
|
/**
|
|
653
660
|
* Merges a sent value into the incoming message at the returnProperty key so
|
|
654
661
|
* upstream message properties propagate. A fresh base is built per call so
|
|
655
662
|
* multi-port sends never share an object.
|
|
656
663
|
*/
|
|
657
|
-
#wrapOutgoing(value, mode
|
|
658
|
-
const key = this.#returnPropertyKey();
|
|
664
|
+
#wrapOutgoing(value, mode, port) {
|
|
665
|
+
const key = this.#returnPropertyKey(port);
|
|
659
666
|
const input = this.#currentInputMsg ?? {};
|
|
660
667
|
if (mode === "reset") {
|
|
661
668
|
return { [key]: value };
|
|
662
669
|
}
|
|
663
|
-
if (mode === "
|
|
664
|
-
return { ...input, [key]: value };
|
|
670
|
+
if (mode === "trace") {
|
|
671
|
+
return { ...input, [key]: value, [INPUT_KEY]: input };
|
|
665
672
|
}
|
|
666
|
-
return { ...input, [key]: value
|
|
673
|
+
return { ...input, [key]: value };
|
|
667
674
|
}
|
|
668
675
|
// --- Built-in port management ---
|
|
669
676
|
get baseOutputs() {
|
|
@@ -687,15 +694,17 @@ var IONode = class extends Node {
|
|
|
687
694
|
* throw an error or call `this.error()` for the error port, and the complete
|
|
688
695
|
* port is sent automatically on successful input processing.
|
|
689
696
|
*/
|
|
690
|
-
sendToPort(port, msg,
|
|
697
|
+
sendToPort(port, msg, options) {
|
|
691
698
|
if (port === "error" || port === "complete" || port === "status") {
|
|
692
699
|
throw new NrgError(
|
|
693
700
|
`sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
|
|
694
701
|
);
|
|
695
702
|
}
|
|
703
|
+
const portIndex = typeof port === "number" ? port : this.#getNamedPortIndex(port);
|
|
704
|
+
const mode = this.#resolveContextMode(portIndex ?? 0, options?.defaultMode);
|
|
696
705
|
this.#sendToPort(
|
|
697
706
|
port,
|
|
698
|
-
msg == null ? msg : this.#wrapOutgoing(msg,
|
|
707
|
+
msg == null ? msg : this.#wrapOutgoing(msg, mode, portIndex ?? 0)
|
|
699
708
|
);
|
|
700
709
|
}
|
|
701
710
|
#sendToPort(port, msg) {
|
|
@@ -1163,18 +1172,21 @@ function TypedInput2(options) {
|
|
|
1163
1172
|
[import_typebox2.Kind]: "TypedInput"
|
|
1164
1173
|
};
|
|
1165
1174
|
}
|
|
1166
|
-
function
|
|
1167
|
-
return import_typebox2.Type.
|
|
1168
|
-
|
|
1169
|
-
pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$",
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1175
|
+
function OutputReturnProperties(options) {
|
|
1176
|
+
return import_typebox2.Type.Record(
|
|
1177
|
+
import_typebox2.Type.Number(),
|
|
1178
|
+
import_typebox2.Type.String({ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }),
|
|
1179
|
+
{
|
|
1180
|
+
description: "Per-port return property, keyed by output port index. A missing entry falls back to `output`.",
|
|
1181
|
+
default: {},
|
|
1182
|
+
...options
|
|
1183
|
+
}
|
|
1184
|
+
);
|
|
1173
1185
|
}
|
|
1174
1186
|
var SchemaType = Object.assign({}, import_typebox2.Type, {
|
|
1175
1187
|
NodeRef,
|
|
1176
1188
|
TypedInput: TypedInput2,
|
|
1177
|
-
|
|
1189
|
+
OutputReturnProperties
|
|
1178
1190
|
});
|
|
1179
1191
|
function markNonValidatable(schema) {
|
|
1180
1192
|
const type = schema.type;
|