@bonsae/nrg 0.18.4 → 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.
- package/README.md +38 -45
- package/package.json +1 -1
- package/server/index.cjs +96 -10
- package/server/resources/nrg-client.js +2269 -2233
- package/test/client/component/config.js +11 -0
- package/test/client/component/index.js +218 -235
- package/test/client/component/nrg.css +1 -0
- package/test/client/component/setup.js +1549 -140
- package/test/client/e2e/index.js +721 -367
- package/test/client/unit/index.js +204 -16
- package/test/client/unit/setup.js +209 -19
- package/test/server/unit/index.js +25 -4
- package/tsconfig/core/client.json +1 -1
- package/tsconfig/test/client/component.json +1 -1
- package/types/client.d.ts +98 -18
- package/types/server.d.ts +50 -12
- package/types/shims/brands.d.ts +32 -0
- package/types/shims/{form → client/form}/components/node-red-editor-input.vue.d.ts +1 -1
- package/types/shims/{form → client/form}/components/node-red-json-schema-form.vue.d.ts +21 -2
- package/types/shims/{form → client/form}/components/node-red-select-input.vue.d.ts +1 -0
- package/types/shims/{form → client/form}/components/node-red-typed-input.vue.d.ts +1 -0
- package/types/shims/client/types.d.ts +206 -0
- package/types/shims/components.d.ts +8 -8
- package/types/shims/constants.d.ts +4 -0
- package/types/shims/schema-options.d.ts +23 -10
- package/types/shims/typebox.d.ts +2 -2
- package/types/test-client-component.d.ts +170 -55
- package/types/test-client-e2e.d.ts +50 -0
- package/types/test-client-unit.d.ts +86 -22
- package/types/test-server-unit.d.ts +3 -1
- package/types/vite.d.ts +38 -9
- package/vite/index.js +733 -528
- /package/types/shims/{form → client/form}/components/node-red-config-input.vue.d.ts +0 -0
- /package/types/shims/{form → client/form}/components/node-red-input-label.vue.d.ts +0 -0
- /package/types/shims/{form → client/form}/components/node-red-input.vue.d.ts +0 -0
- /package/types/shims/{form → client/form}/components/node-red-toggle.vue.d.ts +0 -0
- /package/types/shims/{globals.d.ts → client/globals.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -13,41 +13,25 @@
|
|
|
13
13
|
|
|
14
14
|
Build Node-RED nodes with Vue 3, TypeScript, JSON Schema validations, Vite and Vitest.
|
|
15
15
|
|
|
16
|
-
## Package Exports
|
|
17
|
-
|
|
18
|
-
| Export | Description |
|
|
19
|
-
| --- | --- |
|
|
20
|
-
| `@bonsae/nrg` | Root entry — `defineRuntimeSettings` |
|
|
21
|
-
| `@bonsae/nrg/server` | Server node classes, schema utilities, validation (`IONode`, `ConfigNode`, `defineIONode`, `defineConfigNode`, `defineModule`, `SchemaType`, `defineSchema`, `Infer`) |
|
|
22
|
-
| `@bonsae/nrg/client` | Client-side registration (`registerTypes`, `defineNode`, `useFormNode`, `Infer`) |
|
|
23
|
-
| `@bonsae/nrg/vite` | Vite plugin for building and developing Node-RED packages |
|
|
24
|
-
| `@bonsae/nrg/test/server/unit` | Server unit test helpers (`createNode`, `createRED`, `MockRED`) |
|
|
25
|
-
| `@bonsae/nrg/test/server/unit/config` | Server unit test default vitest config (`defaultConfig`) |
|
|
26
|
-
| `@bonsae/nrg/test/client/unit` | Client unit test mocks (`createRED`, `createJQuery`, `useFormNode`) |
|
|
27
|
-
| `@bonsae/nrg/test/client/unit/config` | Client unit test default vitest config (`defaultConfig`) |
|
|
28
|
-
| `@bonsae/nrg/test/client/unit/setup` | Setup file that installs `RED` and `$` mocks on `window` |
|
|
29
|
-
| `@bonsae/nrg/test/client/component` | Client component test helpers (`createNode`, `createRED`, `createJQuery`, `useFormNode`) |
|
|
30
|
-
| `@bonsae/nrg/test/client/component/config` | Client component test default vitest config (`defaultConfig`) |
|
|
31
|
-
| `@bonsae/nrg/test/client/component/setup` | Setup file that installs `RED` and `$` mocks on `window` with Vue i18n |
|
|
32
|
-
| `@bonsae/nrg/test/client/e2e` | Browser E2E test helpers (`NodeRedEditor`, `NodeRedField`, `setup`, `teardown`) |
|
|
33
|
-
| `@bonsae/nrg/tsconfig/base.json` | Base TypeScript configuration |
|
|
34
|
-
| `@bonsae/nrg/tsconfig/core/server.json` | Core server source tsconfig |
|
|
35
|
-
| `@bonsae/nrg/tsconfig/core/client.json` | Core client source tsconfig |
|
|
36
|
-
| `@bonsae/nrg/tsconfig/test/server/unit.json` | Server unit test tsconfig |
|
|
37
|
-
| `@bonsae/nrg/tsconfig/test/client/unit.json` | Client unit test tsconfig |
|
|
38
|
-
| `@bonsae/nrg/tsconfig/test/client/component.json` | Client component test tsconfig |
|
|
39
|
-
| `@bonsae/nrg/tsconfig/test/client/e2e.json` | Client E2E test tsconfig |
|
|
40
|
-
|
|
41
16
|
## Quick Start
|
|
42
17
|
|
|
43
18
|
```bash
|
|
44
|
-
# In your Node-RED package project
|
|
45
19
|
pnpm add @bonsae/nrg
|
|
46
|
-
pnpm add -D vite vue
|
|
20
|
+
pnpm add -D vite vue node-red
|
|
47
21
|
```
|
|
48
22
|
|
|
49
23
|
> `vite` and `vue` are dev dependencies because they are only needed at build time. Vue is included as a dependency of nrg and served automatically at runtime.
|
|
50
24
|
|
|
25
|
+
### Node-RED Resolution
|
|
26
|
+
|
|
27
|
+
The vite plugin needs a Node-RED instance for the dev server. It resolves it in this order:
|
|
28
|
+
|
|
29
|
+
1. **`runtime.version`** — if specified in the plugin config, downloads that exact version via `npx` (overrides any locally installed version)
|
|
30
|
+
2. **Local `node_modules`** — if `node-red` is installed as a dependency, it is used directly (fastest)
|
|
31
|
+
3. **Fallback** — downloads the latest `node-red` via `npx`
|
|
32
|
+
|
|
33
|
+
Installing `node-red` as a dev dependency is recommended for fast, reliable dev server startup across all platforms (especially Windows). If you need a specific version (e.g., a beta), set `runtime.version` in the plugin config instead.
|
|
34
|
+
|
|
51
35
|
**vite.config.ts**
|
|
52
36
|
|
|
53
37
|
```typescript
|
|
@@ -153,13 +137,13 @@ pnpm add -D vitest
|
|
|
153
137
|
|
|
154
138
|
Optional peer dependencies:
|
|
155
139
|
|
|
156
|
-
| Package
|
|
157
|
-
|
|
|
158
|
-
| `@vitest/browser-playwright` | Component tests (Playwright browser provider for Vitest)
|
|
159
|
-
| `playwright`
|
|
160
|
-
| `vitest-browser-vue`
|
|
161
|
-
| `@vitest/coverage-v8`
|
|
162
|
-
| `@vitest/coverage-istanbul`
|
|
140
|
+
| Package | When to install |
|
|
141
|
+
| ---------------------------- | ------------------------------------------------------------ |
|
|
142
|
+
| `@vitest/browser-playwright` | Component tests (Playwright browser provider for Vitest) |
|
|
143
|
+
| `playwright` | Component tests or E2E tests (direct `import` in test files) |
|
|
144
|
+
| `vitest-browser-vue` | Component tests (`render` helper for Vue components) |
|
|
145
|
+
| `@vitest/coverage-v8` | Coverage with `--coverage` (V8 provider) |
|
|
146
|
+
| `@vitest/coverage-istanbul` | Coverage with `--coverage` (Istanbul provider) |
|
|
163
147
|
|
|
164
148
|
- `@bonsae/nrg/test/server/unit` — server-side unit tests
|
|
165
149
|
- `@bonsae/nrg/test/client/unit` — client-side unit tests (TypeScript logic)
|
|
@@ -195,11 +179,14 @@ Test client-side TypeScript logic (validation, utilities) with mocked `RED` and
|
|
|
195
179
|
import { defineConfig, mergeConfig } from "vitest/config";
|
|
196
180
|
import { defaultConfig } from "@bonsae/nrg/test/client/unit/config";
|
|
197
181
|
|
|
198
|
-
export default mergeConfig(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
182
|
+
export default mergeConfig(
|
|
183
|
+
defaultConfig,
|
|
184
|
+
defineConfig({
|
|
185
|
+
test: {
|
|
186
|
+
include: ["tests/client/unit/**/*.test.ts"],
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
203
190
|
```
|
|
204
191
|
|
|
205
192
|
```typescript
|
|
@@ -223,11 +210,14 @@ Test your Vue editor components with mocked Node-RED globals. Components that us
|
|
|
223
210
|
import { defineConfig, mergeConfig } from "vitest/config";
|
|
224
211
|
import { defaultConfig } from "@bonsae/nrg/test/client/component/config";
|
|
225
212
|
|
|
226
|
-
export default mergeConfig(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
213
|
+
export default mergeConfig(
|
|
214
|
+
defaultConfig,
|
|
215
|
+
defineConfig({
|
|
216
|
+
test: {
|
|
217
|
+
include: ["tests/client/component/**/*.test.ts"],
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
231
221
|
```
|
|
232
222
|
|
|
233
223
|
```typescript
|
|
@@ -239,7 +229,10 @@ import MyForm from "../src/client/components/my-form.vue";
|
|
|
239
229
|
|
|
240
230
|
describe("MyForm", () => {
|
|
241
231
|
test("renders fields from injected node", async () => {
|
|
242
|
-
const { provide } = createNode({
|
|
232
|
+
const { provide } = createNode({
|
|
233
|
+
name: "test",
|
|
234
|
+
url: "https://example.com",
|
|
235
|
+
});
|
|
243
236
|
const screen = render(MyForm, {
|
|
244
237
|
global: { provide },
|
|
245
238
|
});
|
package/package.json
CHANGED
package/server/index.cjs
CHANGED
|
@@ -280,6 +280,8 @@ var Node = class _Node {
|
|
|
280
280
|
throw error;
|
|
281
281
|
}
|
|
282
282
|
);
|
|
283
|
+
createdPromise.catch(() => {
|
|
284
|
+
});
|
|
283
285
|
node[WIRE_HANDLERS](this, createdPromise);
|
|
284
286
|
},
|
|
285
287
|
{
|
|
@@ -288,7 +290,14 @@ var Node = class _Node {
|
|
|
288
290
|
}
|
|
289
291
|
);
|
|
290
292
|
NodeClass.validateSettings(RED);
|
|
291
|
-
|
|
293
|
+
try {
|
|
294
|
+
await Promise.resolve(NodeClass.registered?.(RED));
|
|
295
|
+
} catch (error) {
|
|
296
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
297
|
+
RED.log.error(
|
|
298
|
+
`Error during registered hook for ${NodeClass.type}: ${message}`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
292
301
|
}
|
|
293
302
|
RED;
|
|
294
303
|
node;
|
|
@@ -428,10 +437,14 @@ var Node = class _Node {
|
|
|
428
437
|
};
|
|
429
438
|
|
|
430
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";
|
|
431
442
|
var IONode = class extends Node {
|
|
432
443
|
static align;
|
|
433
444
|
static color;
|
|
434
445
|
static inputSchema;
|
|
446
|
+
// outputsSchema accepts any schema shape: with returnProperty the raw sent
|
|
447
|
+
// value is validated, and results are frequently non-objects.
|
|
435
448
|
static outputsSchema;
|
|
436
449
|
static validateInput = false;
|
|
437
450
|
static validateOutput = false;
|
|
@@ -459,6 +472,12 @@ var IONode = class extends Node {
|
|
|
459
472
|
return keys.length;
|
|
460
473
|
}
|
|
461
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;
|
|
462
481
|
context;
|
|
463
482
|
constructor(RED, node, config, credentials) {
|
|
464
483
|
super(RED, node, config, credentials);
|
|
@@ -471,6 +490,12 @@ var IONode = class extends Node {
|
|
|
471
490
|
fn.flow = setupContext(context.flow);
|
|
472
491
|
fn.global = setupContext(context.global);
|
|
473
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
|
+
}
|
|
474
499
|
}
|
|
475
500
|
[WIRE_HANDLERS](nodeRedNode, createdPromise) {
|
|
476
501
|
super[WIRE_HANDLERS](nodeRedNode, createdPromise);
|
|
@@ -486,12 +511,14 @@ var IONode = class extends Node {
|
|
|
486
511
|
}
|
|
487
512
|
try {
|
|
488
513
|
nodeRedNode.log("Calling input");
|
|
514
|
+
this.#currentInputMsg = msg;
|
|
489
515
|
await Promise.resolve(this.#input(msg, send));
|
|
490
516
|
this.#sendToPort("complete", {
|
|
491
517
|
...msg,
|
|
492
518
|
complete: {
|
|
493
519
|
source: this.#nodeSource()
|
|
494
|
-
}
|
|
520
|
+
},
|
|
521
|
+
[INPUT_KEY]: msg
|
|
495
522
|
});
|
|
496
523
|
done();
|
|
497
524
|
nodeRedNode.log("Input processed");
|
|
@@ -502,7 +529,8 @@ var IONode = class extends Node {
|
|
|
502
529
|
error: {
|
|
503
530
|
message: errorMsg,
|
|
504
531
|
source: this.#nodeSource()
|
|
505
|
-
}
|
|
532
|
+
},
|
|
533
|
+
[INPUT_KEY]: msg
|
|
506
534
|
});
|
|
507
535
|
if (error instanceof Error) {
|
|
508
536
|
nodeRedNode.error(
|
|
@@ -541,8 +569,9 @@ var IONode = class extends Node {
|
|
|
541
569
|
this.#send = void 0;
|
|
542
570
|
}
|
|
543
571
|
}
|
|
544
|
-
send(msg) {
|
|
572
|
+
send(msg, contextMode = "nest") {
|
|
545
573
|
const NodeClass = this.constructor;
|
|
574
|
+
const sendsValue = this.baseOutputs <= 1;
|
|
546
575
|
const shouldValidateOutput = this.config.validateOutput ?? NodeClass.validateOutput;
|
|
547
576
|
if (shouldValidateOutput && NodeClass.outputsSchema) {
|
|
548
577
|
this.log("Validating output");
|
|
@@ -557,7 +586,7 @@ var IONode = class extends Node {
|
|
|
557
586
|
});
|
|
558
587
|
}
|
|
559
588
|
} else if (isSchemaLike(rawSchema)) {
|
|
560
|
-
if (Array.isArray(msg)) {
|
|
589
|
+
if (Array.isArray(msg) && !sendsValue) {
|
|
561
590
|
const msgs = msg;
|
|
562
591
|
for (let i = 0; i < msgs.length; i++) {
|
|
563
592
|
if (msgs[i] == null) continue;
|
|
@@ -585,13 +614,57 @@ var IONode = class extends Node {
|
|
|
585
614
|
}
|
|
586
615
|
this.log("Output is valid");
|
|
587
616
|
}
|
|
588
|
-
const
|
|
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);
|
|
589
621
|
if (this.#send) {
|
|
590
622
|
this.#send(out);
|
|
591
623
|
} else {
|
|
592
624
|
this.node.send(out);
|
|
593
625
|
}
|
|
594
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
|
+
}
|
|
595
668
|
// --- Built-in port management ---
|
|
596
669
|
get baseOutputs() {
|
|
597
670
|
return this.constructor.outputs ?? 0;
|
|
@@ -614,13 +687,16 @@ var IONode = class extends Node {
|
|
|
614
687
|
* throw an error or call `this.error()` for the error port, and the complete
|
|
615
688
|
* port is sent automatically on successful input processing.
|
|
616
689
|
*/
|
|
617
|
-
sendToPort(port, msg) {
|
|
690
|
+
sendToPort(port, msg, contextMode = "nest") {
|
|
618
691
|
if (port === "error" || port === "complete" || port === "status") {
|
|
619
692
|
throw new NrgError(
|
|
620
693
|
`sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
|
|
621
694
|
);
|
|
622
695
|
}
|
|
623
|
-
this.#sendToPort(
|
|
696
|
+
this.#sendToPort(
|
|
697
|
+
port,
|
|
698
|
+
msg == null ? msg : this.#wrapOutgoing(msg, contextMode)
|
|
699
|
+
);
|
|
624
700
|
}
|
|
625
701
|
#sendToPort(port, msg) {
|
|
626
702
|
let portIndex;
|
|
@@ -678,7 +754,8 @@ var IONode = class extends Node {
|
|
|
678
754
|
error: {
|
|
679
755
|
message,
|
|
680
756
|
source: this.#nodeSource()
|
|
681
|
-
}
|
|
757
|
+
},
|
|
758
|
+
[INPUT_KEY]: msg
|
|
682
759
|
});
|
|
683
760
|
}
|
|
684
761
|
}
|
|
@@ -1086,9 +1163,18 @@ function TypedInput2(options) {
|
|
|
1086
1163
|
[import_typebox2.Kind]: "TypedInput"
|
|
1087
1164
|
};
|
|
1088
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
|
+
}
|
|
1089
1174
|
var SchemaType = Object.assign({}, import_typebox2.Type, {
|
|
1090
1175
|
NodeRef,
|
|
1091
|
-
TypedInput: TypedInput2
|
|
1176
|
+
TypedInput: TypedInput2,
|
|
1177
|
+
ReturnProperty
|
|
1092
1178
|
});
|
|
1093
1179
|
function markNonValidatable(schema) {
|
|
1094
1180
|
const type = schema.type;
|