@bonsae/nrg 0.20.1 → 0.21.1
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 +106 -86
- package/server/resources/nrg-client.js +2671 -2384
- package/test/client/component/setup.js +22 -21
- package/test/client/e2e/index.js +296 -52
- package/types/client.d.ts +18 -4
- package/types/server.d.ts +41 -24
- package/types/shims/client/types.d.ts +18 -4
- package/types/test-client-component.d.ts +10 -4
- package/types/test-client-unit.d.ts +10 -4
- package/vite/index.js +291 -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,26 @@ 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) {
|
|
574
577
|
const sendsValue = this.baseOutputs <= 1;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
cacheKey: rawSchema[i].$id || `${NodeClass.type}:output-schema:${i}`,
|
|
585
|
-
throwOnError: true
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
} else if (isSchemaLike(rawSchema)) {
|
|
589
|
-
if (Array.isArray(msg) && !sendsValue) {
|
|
590
|
-
const msgs = msg;
|
|
591
|
-
for (let i = 0; i < msgs.length; i++) {
|
|
592
|
-
if (msgs[i] == null) continue;
|
|
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
|
+
if (Array.isArray(msg) && !sendsValue) {
|
|
579
|
+
const slots = msg.slice(0, this.baseOutputs);
|
|
580
|
+
const out = slots.map((m, port) => {
|
|
581
|
+
if (m == null) return m;
|
|
582
|
+
this.#validatePort(m, port);
|
|
583
|
+
return this.#wrapOutgoing(m, this.#resolveContextMode(port), port);
|
|
584
|
+
});
|
|
585
|
+
this.#deliver(out);
|
|
586
|
+
return;
|
|
616
587
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
588
|
+
if (msg == null) {
|
|
589
|
+
this.#deliver(msg);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
this.#validatePort(msg, 0);
|
|
593
|
+
this.#deliver(this.#wrapOutgoing(msg, this.#resolveContextMode(0), 0));
|
|
594
|
+
}
|
|
595
|
+
#deliver(out) {
|
|
621
596
|
if (this.#send) {
|
|
622
597
|
this.#send(out);
|
|
623
598
|
} else {
|
|
@@ -625,45 +600,69 @@ var IONode = class extends Node {
|
|
|
625
600
|
}
|
|
626
601
|
}
|
|
627
602
|
/**
|
|
628
|
-
*
|
|
629
|
-
* `
|
|
630
|
-
*
|
|
603
|
+
* Per-port output validation. A port validates when its flow-author flag
|
|
604
|
+
* (`config.validateOutputs[port]`) — or the node's static `validateOutput`
|
|
605
|
+
* fallback — is on and a schema exists for that port.
|
|
631
606
|
*/
|
|
607
|
+
#validatePort(value, port) {
|
|
608
|
+
const NodeClass = this.constructor;
|
|
609
|
+
const configured = this.config.validateOutputs?.[port];
|
|
610
|
+
if (!(configured ?? NodeClass.validateOutput)) return;
|
|
611
|
+
const schema = this.#outputSchemaForPort(port);
|
|
612
|
+
if (!schema) return;
|
|
613
|
+
this.log("Validating output");
|
|
614
|
+
this.RED.validator.validate(value, schema, {
|
|
615
|
+
cacheKey: schema.$id || `${NodeClass.type}:output-schema:${port}`,
|
|
616
|
+
throwOnError: true
|
|
617
|
+
});
|
|
618
|
+
this.log("Output is valid");
|
|
619
|
+
}
|
|
620
|
+
/** Resolves the output schema for a base-output port: array → `[port]`,
|
|
621
|
+
* record → the port-th value, single schema → itself. */
|
|
622
|
+
#outputSchemaForPort(port) {
|
|
623
|
+
const raw = this.constructor.outputsSchema;
|
|
624
|
+
if (!raw) return void 0;
|
|
625
|
+
if (Array.isArray(raw)) return raw[port];
|
|
626
|
+
if (isSchemaLike(raw)) return raw;
|
|
627
|
+
return Object.values(raw)[port];
|
|
628
|
+
}
|
|
632
629
|
/**
|
|
633
|
-
*
|
|
634
|
-
* `
|
|
635
|
-
* only
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
* outgoing message".
|
|
630
|
+
* The return key for an output port — `"output"` unless a custom one is set
|
|
631
|
+
* via `outputReturnProperties[port]` (author default and/or flow-author
|
|
632
|
+
* override, only possible when the node declares `outputReturnProperties`).
|
|
633
|
+
* `this.send(x)` always means "x is the value at this port's return key",
|
|
634
|
+
* never "x is the whole outgoing message".
|
|
639
635
|
*/
|
|
640
|
-
#returnPropertyKey() {
|
|
641
|
-
const
|
|
642
|
-
const declared = NodeClass.configSchema?.properties?.returnProperty;
|
|
643
|
-
const configured = this.config.returnProperty;
|
|
636
|
+
#returnPropertyKey(port) {
|
|
637
|
+
const configured = this.config.outputReturnProperties?.[port];
|
|
644
638
|
if (typeof configured === "string" && configured.trim()) {
|
|
645
639
|
return configured.trim();
|
|
646
640
|
}
|
|
647
|
-
if (declared && typeof declared.default === "string" && declared.default) {
|
|
648
|
-
return declared.default;
|
|
649
|
-
}
|
|
650
641
|
return "output";
|
|
651
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Resolves the context mode for a base-output port from the flow author's
|
|
645
|
+
* per-port config (`config.outputContextModes[port]`, written by the editor
|
|
646
|
+
* when the node declares `outputContextModes`), falling back to `"carry"`.
|
|
647
|
+
*/
|
|
648
|
+
#resolveContextMode(port) {
|
|
649
|
+
return this.config.outputContextModes?.[port] ?? "carry";
|
|
650
|
+
}
|
|
652
651
|
/**
|
|
653
652
|
* Merges a sent value into the incoming message at the returnProperty key so
|
|
654
653
|
* upstream message properties propagate. A fresh base is built per call so
|
|
655
654
|
* multi-port sends never share an object.
|
|
656
655
|
*/
|
|
657
|
-
#wrapOutgoing(value, mode
|
|
658
|
-
const key = this.#returnPropertyKey();
|
|
656
|
+
#wrapOutgoing(value, mode, port) {
|
|
657
|
+
const key = this.#returnPropertyKey(port);
|
|
659
658
|
const input = this.#currentInputMsg ?? {};
|
|
660
659
|
if (mode === "reset") {
|
|
661
660
|
return { [key]: value };
|
|
662
661
|
}
|
|
663
|
-
if (mode === "
|
|
664
|
-
return { ...input, [key]: value };
|
|
662
|
+
if (mode === "trace") {
|
|
663
|
+
return { ...input, [key]: value, [INPUT_KEY]: input };
|
|
665
664
|
}
|
|
666
|
-
return { ...input, [key]: value
|
|
665
|
+
return { ...input, [key]: value };
|
|
667
666
|
}
|
|
668
667
|
// --- Built-in port management ---
|
|
669
668
|
get baseOutputs() {
|
|
@@ -687,15 +686,17 @@ var IONode = class extends Node {
|
|
|
687
686
|
* throw an error or call `this.error()` for the error port, and the complete
|
|
688
687
|
* port is sent automatically on successful input processing.
|
|
689
688
|
*/
|
|
690
|
-
sendToPort(port, msg
|
|
689
|
+
sendToPort(port, msg) {
|
|
691
690
|
if (port === "error" || port === "complete" || port === "status") {
|
|
692
691
|
throw new NrgError(
|
|
693
692
|
`sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
|
|
694
693
|
);
|
|
695
694
|
}
|
|
695
|
+
const portIndex = typeof port === "number" ? port : this.#getNamedPortIndex(port);
|
|
696
|
+
const mode = this.#resolveContextMode(portIndex ?? 0);
|
|
696
697
|
this.#sendToPort(
|
|
697
698
|
port,
|
|
698
|
-
msg == null ? msg : this.#wrapOutgoing(msg,
|
|
699
|
+
msg == null ? msg : this.#wrapOutgoing(msg, mode, portIndex ?? 0)
|
|
699
700
|
);
|
|
700
701
|
}
|
|
701
702
|
#sendToPort(port, msg) {
|
|
@@ -1163,18 +1164,37 @@ function TypedInput2(options) {
|
|
|
1163
1164
|
[import_typebox2.Kind]: "TypedInput"
|
|
1164
1165
|
};
|
|
1165
1166
|
}
|
|
1166
|
-
function
|
|
1167
|
-
return import_typebox2.Type.
|
|
1168
|
-
|
|
1169
|
-
pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$",
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1167
|
+
function OutputReturnProperties(options) {
|
|
1168
|
+
return import_typebox2.Type.Record(
|
|
1169
|
+
import_typebox2.Type.Number(),
|
|
1170
|
+
import_typebox2.Type.String({ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }),
|
|
1171
|
+
{
|
|
1172
|
+
description: "Per-port return property, keyed by output port index. A missing entry falls back to `output`.",
|
|
1173
|
+
default: {},
|
|
1174
|
+
...options
|
|
1175
|
+
}
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
function OutputContextModes(options) {
|
|
1179
|
+
return import_typebox2.Type.Record(
|
|
1180
|
+
import_typebox2.Type.Number(),
|
|
1181
|
+
import_typebox2.Type.Union([
|
|
1182
|
+
import_typebox2.Type.Literal("carry"),
|
|
1183
|
+
import_typebox2.Type.Literal("trace"),
|
|
1184
|
+
import_typebox2.Type.Literal("reset")
|
|
1185
|
+
]),
|
|
1186
|
+
{
|
|
1187
|
+
description: "Per-port context mode, keyed by output port index. A missing entry falls back to `carry`.",
|
|
1188
|
+
default: {},
|
|
1189
|
+
...options
|
|
1190
|
+
}
|
|
1191
|
+
);
|
|
1173
1192
|
}
|
|
1174
1193
|
var SchemaType = Object.assign({}, import_typebox2.Type, {
|
|
1175
1194
|
NodeRef,
|
|
1176
1195
|
TypedInput: TypedInput2,
|
|
1177
|
-
|
|
1196
|
+
OutputReturnProperties,
|
|
1197
|
+
OutputContextModes
|
|
1178
1198
|
});
|
|
1179
1199
|
function markNonValidatable(schema) {
|
|
1180
1200
|
const type = schema.type;
|