Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

782 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nrg-icon

npm package build status codecov Socket Badge

Warning

While NRG is at v0, breaking changes can land in any release and will not bump the major version. Pin an exact version and review the release notes before upgrading.

NRG

Build Node-RED nodes with Vue 3, TypeScript, JSON Schemas, Vite and Vitest.

Quick Start

Scaffold a new project with everything wired up:

pnpm create @bonsae/nrg

A node is a class (the logic) and a config schema (drives the editor form + validation). Here's a small greeting node — one typed input, one output, and a few config fields:

src/server/nodes/greeting.ts

import { Node, IONode, type Infer, type Input, type Outputs, type Port } from "@bonsae/nrg/server";
import { ConfigsSchema } from "@/schemas/greeting";

type GreetingConfig = Infer<typeof ConfigsSchema>;
type GreetingInput = Input<Port<{ name: string }>>;
type GreetingOutputs = Outputs<{ greeting: Port<{ text: string }> }>;

@Node({ type: "greeting", config: ConfigsSchema })
export default class Greeting extends IONode<{
  config: GreetingConfig;
  input: GreetingInput;
  outputs: GreetingOutputs;
}> {
  override async input(msg: GreetingInput) {
    const { greeting, style, repeat } = this.config;
    const suffix = style === "excited" ? "!" : style === "friendly" ? " :)" : "";
    const text = Array.from({ length: repeat }, () => `${greeting}, ${msg.name}${suffix}`).join(" ");
    this.send("greeting", { text });
  }
}

The class is pure typed logic — config, the input msg, and the send payload are all statically typed. Those config types are inferred from a schema, and that same schema is the single source of truth for the fields' defaults, validation, and the editor form:

src/shared/schemas/greeting.ts

import { SchemaType, defineSchema } from "@bonsae/nrg/schema";

export const ConfigsSchema = defineSchema(
  {
    greeting: SchemaType.String({ default: "Hello", description: "The greeting word placed before the name.", "x-nrg-form": { icon: "comment" } }),
    style: SchemaType.Union(
      [SchemaType.Literal("plain"), SchemaType.Literal("excited"), SchemaType.Literal("friendly")],
      { default: "plain", description: "Tone of the greeting.", "x-nrg-form": { icon: "paint-brush" } },
    ),
    repeat: SchemaType.Number({ default: 1, description: "How many times to repeat the greeting.", "x-nrg-form": { icon: "repeat" } }),
    note: SchemaType.Optional(
      SchemaType.String({ default: "", description: "Optional note shown under the node.", "x-nrg-form": { icon: "pencil" } }),
    ),
  },
  { $id: "GreetingConfigsSchema" },
);

You wrote no editor HTML, no jQuery, no oneditprepare — nrg generates the entire edit dialog from the schema and types above: the style union becomes a dropdown, strings and numbers become validated inputs, every non-Optional field is marked required with a * (so greeting, style, and repeat get one; note, wrapped in Optional, doesn't), and the input/output ports and lifecycle wiring come for free.

The greeting node's editor form, generated by nrg from the schema

A classic Node-RED node hand-writes hundreds of lines of HTML for its edit form and keeps it in sync with the runtime by hand. Here it's derived from your schema and types — so it can't drift, and every field is validated for free.

Flows Are Type-Checked

In stock Node-RED any output can wire into any input: a node that emits { payload: "hello" } connects happily to one expecting a number, the deploy succeeds, and the mismatch surfaces at runtime. Because nrg node ports are typed (Port<T>), nrg compiles your whole flow on every deploy and asks the TypeScript compiler whether each connection actually type-checks — then lists the bad ones in a Type errors tab and paints them red on the canvas, before the flow ever runs.

A Node-RED flow with a red-dashed failing connection and a yellow-dashed unchecked one, beside the Type errors sidebar tab listing both with their TypeScript messages

The middle wire reds because invoice reads a customer field nothing upstream adds — with tsc's own message. The bottom wire is yellow: it's still valid, but one endpoint is untyped, so the check says so rather than pretending.

The check is an opt-in dev dependency — npm install --save-dev @bonsae/node-red-type-check-plugin and nrg dev picks it up; without it nothing changes. See The Wire Check for how it works and Reading the Verdicts for every verdict, including how a port's Data Schema can narrow its type per instance.

See the documentation for the full walkthrough — schemas, the generated editor form, testing, and building — or the node-red-salesforce repo for a real-world reference.

License

MIT

Releases

Contributors

Languages