Build Node-RED nodes with Vue 3, TypeScript, and JSON Schema validation.
pnpm add @bonsae/nrg vue
pnpm add -D vite| 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 vue
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 } from "@bonsae/nrg/server";
import { ConfigsSchema } from "../schemas/my-node";
export default defineIONode({
type: "my-node",
color: "#ffffff",
inputs: 1,
outputs: 1,
configSchema: ConfigsSchema,
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 } from "../schemas/my-node";
type Config = Infer<typeof ConfigsSchema>;
export default class MyNode extends IONode<Config> {
static readonly type = "my-node";
static readonly category = "function";
static readonly color: `#${string}` = "#ffffff";
static readonly inputs = 1;
static readonly outputs = 1;
static readonly configSchema: Schema = ConfigsSchema;
async input(msg: any) {
msg.payload = `${this.config.prefix}: ${msg.payload}`;
this.send(msg);
}
} |
| 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.
src/
├── core/ # Runtime framework
│ ├── client/ # Vue 3 editor components
│ │ ├── app.vue # Root form wrapper (validation, toggles)
│ │ ├── components/ # Reusable form inputs
│ │ │ ├── node-red-input.vue
│ │ │ ├── node-red-typed-input.vue
│ │ │ ├── node-red-config-input.vue
│ │ │ ├── node-red-select-input.vue
│ │ │ ├── node-red-editor-input.vue
│ │ │ └── node-red-json-schema-form.vue
│ │ └── index.ts # registerType, defineNode
│ ├── server/ # Node.js server runtime
│ │ ├── nodes/ # Node, IONode, ConfigNode classes
│ │ ├── schemas/ # TypeBox schema system
│ │ ├── types/ # RED, context store types
│ │ └── index.ts # registerTypes, exports
│ ├── constants.ts
│ └── validator.ts # AJV-based validation
├── vite/ # Build tooling
│ ├── plugin.ts # Vite plugin factory
│ ├── plugins/ # Dev server, build orchestration
│ ├── server/ # Server build (CJS/ESM + bridge)
│ ├── client/ # Client build (Vue + auto-wiring)
│ └── index.ts # nodeRed(), defineRuntimeSettings()
└── tsconfig/ # Shared configs for consumers
├── base.json
├── client.json
└── server.json
pnpm install
pnpm build # build all (server CJS, client ESM, vite plugin)
pnpm typecheck # type-check server and client
pnpm lint # eslint
pnpm format # prettierMIT