node-red-builder
v1.4.0
Published
Core runtime and CLI for Node-RED Builder - develop Node-RED nodes with Hot Reload and IDE-friendly JSDoc types
Maintainers
Readme
node-red-builder
CLI and runtime framework for building Node-RED nodes.
Exposes the nrb binary and provides base classes for node
development.
CLI
The nrb binary is available after installing the package.
It is typically invoked via npm scripts (see generated
package.json below) or with bunx nrb.
nrb init [projectDir] [--example]
Scaffolds a new project in the current directory or in
projectDir if specified. Skips files that already exist.
By default, it creates a clean project structure. Use the
--example flag to also generate an example node.
Generates:
package.jsonwith scripts and emptynode-red.nodestsconfig.jsonfor TypeScript and JSDoc type checkingnode-red-builder.config.js.gitignore
With --example, it also generates:
src/nodes/example/—runtime.js,ui.js,template.htmlsrc/locales/en-US/example.jsondocs/en-US/nodes/example.md
The prefix is automatically inferred from the directory
name or the root package.json name if in a monorepo.
Prefixes like node-red-contrib- and node-red- are
stripped.
This command is called internally by create-node-red.
nrb add [name] [--type <t>] [--config-node <n>] [--skip-config]
Adds a new node to an existing project. If type is config and name
is omitted, it defaults to config.
<name>— Kebab-case name of the node (e.g.,my-device).--type—node(default) orconfig.--config-node— Pre-select a config node to link with (skips prompt).--skip-config— Do not link with any config node (skips prompt).
The tool automatically detects existing configuration nodes and offers to
link the new node to one of them. When linked, the generator automatically
adds JSDoc imports and correct @extends parameters to runtime.js for
full type safety in the editor.
Creates:
src/nodes/<name>/runtime.jssrc/nodes/<name>/ui.jssrc/nodes/<name>/template.htmlsrc/locales/en-US/<name>.jsondocs/en-US/nodes/<name>.md(for functional nodes only)
Also adds the entry to package.json:
"node-red": {
"nodes": { "<name>": "dist/nodes/<name>.js" }
}bunx nrb add temperature-sensor
bunx nrb add --type confignrb build
For each directory found in src/nodes/:
- Bundles
runtime.jswith Bun (Node.js target, minified). Packages listed independenciesare marked as external. - Bundles
ui.jsfor the browser and assembles it together withtemplate.htmlinto a singledist/nodes/<n>.html. - Copies icons from
src/nodes/<n>/icons/if the directory exists. - Copies locale files:
src/locales/<lang>/<n>.json→dist/nodes/locales/<lang>/<n>.json - Converts help docs:
docs/<lang>/nodes/<n>.md→dist/nodes/locales/<lang>/<n>.html(H1 is stripped; remaining headings shift down one level)
nrb dev [--port <number>]
Runs nrb build, starts Node-RED, then watches src/,
docs/, src/locales/, and config files for changes.
On any change, rebuilds and restarts Node-RED (debounced
at 300 ms).
The port is read from node-red-builder.config.js or
defaults to 3000. Use the --port flag to override.
nrb start [--port <number>]
Sets up the dev environment and starts Node-RED without building or watching.
The port is read from node-red-builder.config.js or
defaults to 3000. Use the --port flag to override.
On SIGINT / SIGTERM:
- Credentials are saved to
.cred.json - Flows saved in the Node-RED library are moved to
examples/
Dev Environment
Both nrb dev and nrb start use .dev/ as the Node-RED
user directory. On each start:
- Creates
.dev/if absent. - Symlinks
examples/→.dev/lib/flows(if it exists), making example flows available in the Node-RED library. - Copies
.cred.json→.dev/flows_cred.json. - On first run (no
.dev/flows.jsonyet), generates it by merging allexamples/*.jsonfiles, deduplicating tab IDs. - Symlinks the current package into
.dev/node_modules/<name>.
Configuration
node-red-builder.config.js (ESM, default export):
export default {
prefix: 'my',
port: 3000,
srcDir: 'src',
distDir: 'dist',
docsDir: 'docs',
localesDir: 'src/locales',
palette: {
color: '#a6bbcf', // default node color
},
};All paths are resolved relative to process.cwd(). The palette.color
is used as a template variable __COLOR__ when generating new nodes.
The file is optional; defaults are used if absent.
Generated package.json
nrb init generates the following package.json:
{
"name": "node-red-contrib-<prefix>",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node-red-builder start",
"dev": "node-red-builder dev",
"build": "node-red-builder build",
"prepack": "bun run build"
},
"node-red": {
"version": ">=3.0.0",
"nodes": {}
},
"files": ["./dist/", "./examples/"],
"devDependencies": {
"node-red": "latest",
"node-red-builder": "latest"
},
"engines": { "node": ">=18.0.0" }
}Runtime API
Import in runtime.js (Node.js side):
import {
BaseNode,
BaseConfigNode,
registerNode,
} from 'node-red-builder';BaseNode
Base class for functional nodes. The input event is only
registered if onInput is overridden in the subclass.
Errors thrown inside onInput are caught, passed to done(),
and the node status is set to red automatically.
/** @import { NodeMessage } from 'node-red' */
/** @import { NodeProps as BaseProps } from 'node-red-builder' */
/**
* @typedef {BaseProps & {
* action: string,
* actionType: string,
* }} NodeProps
*/
/** @extends {BaseNode<NodeProps>} */
export class MyNode extends BaseNode {
/** @param {NodeMessage} msg */
async onInput(msg, send) {
const action = await this.getProp('action', msg);
msg.payload = action;
send(msg);
this.setStatus('ok', 'green');
this.clearStatus(1_500);
}
async onClose(removed) {}
}
export default (RED) => registerNode(RED, 'my-node', MyNode);Properties
| Property | Type | Description |
|---|---|---|
| RED | NodeAPI | Node-RED API |
| node | Node | The Node-RED node instance |
| props | TProps | Node configuration from the editor |
| config | TConfigNode \| undefined | Linked config node |
| client | inferred | config.getClient() shortcut |
client throws if the config node or its client is
unavailable.
getProp(key, msg)
Resolves props[key] using props[keyType] via
RED.util.evaluateNodeProperty. Supports all standard
Node-RED typed input types (str, num, bool, json,
msg, flow, global, env, jsonata, cred, …).
const action = await this.getProp('action', msg);getProps(keys, msg)
Resolves multiple properties in parallel. Returns
Record<string, any>.
const { action, topic } = await this.getProps(
['action', 'topic'], msg,
);setStatus(text, fill?, shape?)
Sets the node status badge. Text is truncated to 32 chars.
Defaults: fill = 'green', shape = 'dot'.
clearStatus(delay?)
Clears the status badge immediately or after delay ms.
Lifecycle hooks
| Method | Called when |
|---|---|
| onInput(msg, send) | A message arrives |
| onClose(removed) | Node closes or Node-RED restarts |
BaseConfigNode
Base class for configuration nodes.
/** @extends {BaseConfigNode<ConfigNodeDef, Credentials, MyClient>} */
export class MyConfigNode extends BaseConfigNode {
constructor(node, config, RED) {
super(node, config, RED);
this.initClient(MyClient, {
token: node.credentials.token,
});
this.registerRoute('get', '/status', (req, res) => {
res.json({ ok: true });
});
}
async onClose(removed) {}
}
export default (RED) =>
registerNode(RED, 'my-config', MyConfigNode, {
token: { type: 'password' },
});initClient(ClientClass, options)
Creates new ClientClass(options), stores it, returns it.
If the instance emits an 'auth' event, credentials are
saved automatically via RED.nodes.addCredentials.
getClient()
Returns the stored client or null.
registerRoute(method, path, handler, permission?)
Registers a route on RED.httpAdmin at:
/<namespace>/<nodeId><path>permission defaults to 'read'. Auth middleware is applied
automatically. Returns the full path string.
On node close, all registered routes are removed from the Express router automatically.
namespace
Derived from config.type — everything before the last -.
For example, my-config → my.
Lifecycle hook
Override onClose(removed) for teardown. Client event
listeners are removed automatically.
registerNode(RED, typeName, NodeClass, credentials?)
Registers a class-based node with Node-RED. The constructor
receives (node, config, RED). The instance is stored on
node.instance, allowing config nodes to expose their client
to dependent nodes via getClient().
export default (RED) =>
registerNode(RED, 'my-sensor', MySensorNode);
// with credentials:
export default (RED) =>
registerNode(RED, 'my-config', MyConfigNode, {
apiKey: { type: 'password' },
});UI Helpers
Import in ui.js (browser side):
import {
setupTypedInput,
createTypedInputOptions,
} from 'node-red-builder/ui';setupTypedInput(elementId, types?)
Initialises a typedInput widget on #<elementId> and wires
it to the hidden #<elementId>Type field.
types defaults to ['str', 'msg', 'flow', 'global'].
setupTypedInput('node-input-topic');
setupTypedInput('node-input-value', ['str', 'num', 'msg']);createTypedInputOptions(items, getLabel?)
Converts an object or array to the options format used by
typedInput type definitions.
For objects, getLabel(key, value) receives the object key
and value. For arrays, both arguments receive the item value.
const ACTION = { APPLY: 'apply', RESTART: 'restart' };
setupTypedInput('node-input-action', [{
value: 'action',
options: createTypedInputOptions(ACTION, (key) =>
this._(`node.action.${key.toLowerCase()}`),
),
}, 'str', 'msg']);Types
Declaration files are in types/ (built from src/ via
tsc), exposed via exports:
| Import | Types |
|---|---|
| node-red-builder | types/runtime/index.d.ts |
| node-red-builder/config | types/cli/config.d.ts |
| node-red-builder/ui | types/ui/index.d.ts |
| node-red-builder/cli/<cmd> | types/cli/commands/<cmd>.d.ts |
