Build Node-RED nodes with Vue 3, TypeScript, and JSON Schema validation.
pnpm add @bonsae/nrg
pnpm add -D vite
viteis a dev dependency because it is only needed at build time. Vue is included as a dependency of nrg and served automatically at runtime.
| Export | Description |
|---|---|
@bonsae/nrg |
Root entry — defineRuntimeSettings |
@bonsae/nrg/server |
Server node classes, schema utilities, validation (IONode, ConfigNode, defineIONode, defineConfigNode, defineModule, SchemaType, defineSchema, Infer) |
@bonsae/nrg/client |
Client-side registration (registerTypes, defineNode) |
@bonsae/nrg/vite |
Vite plugin for building and developing Node-RED packages |
@bonsae/nrg/tsconfig/* |
Shared TypeScript configurations for consumers |
# In your Node-RED package project
pnpm add @bonsae/nrg
pnpm add -D vitevite.config.ts
import { defineConfig } from "vite";
import { nodeRed } from "@bonsae/nrg/vite";
export default defineConfig({
plugins: [nodeRed()],
});src/server/schemas/my-node.ts
import { defineSchema, SchemaType } from "@bonsae/nrg/server";
export const ConfigsSchema = defineSchema(
{
name: SchemaType.String({ default: "" }),
prefix: SchemaType.String({ default: "hello" }),
},
{ $id: "my-node:configs" }
);src/server/nodes/my-node.ts
NRG supports two ways to define nodes:
| Functional API | Class API |
|---|---|
import { defineIONode, SchemaType } from "@bonsae/nrg/server";
import { ConfigsSchema, InputSchema, OutputSchema } from "../schemas/my-node";
export default defineIONode({
type: "my-node",
color: "#ffffff",
configSchema: ConfigsSchema,
inputSchema: InputSchema,
outputsSchema: OutputSchema,
async input(msg) {
msg.payload = `${this.config.prefix}: ${msg.payload}`;
this.send(msg);
},
}); |
import { IONode, type Schema, type Infer } from "@bonsae/nrg/server";
import { ConfigsSchema, InputSchema, OutputSchema } from "../schemas/my-node";
type Config = Infer<typeof ConfigsSchema>;
type Input = Infer<typeof InputSchema>;
type Output = Infer<typeof OutputSchema>;
export default class MyNode extends IONode<Config, any, Input, Output> {
static readonly type = "my-node";
static readonly category = "function";
static readonly color: `#${string}` = "#ffffff";
static readonly configSchema: Schema = ConfigsSchema;
static readonly inputSchema: Schema = InputSchema;
static readonly outputsSchema: Schema = OutputSchema;
async input(msg: Input) {
this.send({ payload: `${this.config.prefix}: ${msg.payload}` });
}
} |
| Automatic type inference, less boilerplate | Custom methods, inheritance, mixins |
src/server/index.ts
import { defineModule } from "@bonsae/nrg/server";
import MyNode from "./nodes/my-node";
export default defineModule({
nodes: [MyNode],
});See the consumer template for a complete example.
Test your nodes' server-side logic with @bonsae/nrg/test:
pnpm add -D vitest// tests/my-node.test.ts
import { describe, it, expect } from "vitest";
import { createNode } from "@bonsae/nrg/test";
import MyNode from "../src/server/nodes/my-node";
describe("my-node", () => {
it("should process messages", async () => {
const { node } = await createNode(MyNode, {
config: { greeting: "hello" },
});
await node.receive({ payload: "world" });
expect(node.sent(0)).toEqual([{ payload: "hello world" }]);
});
});npx vitest runpnpm install
pnpm build # build all (server CJS, client ESM, vite plugin)
pnpm typecheck # type-check server and client
pnpm lint # eslint
pnpm format # prettierMIT