The goal of this framework is to modernize the creation of nodes, while also improving their maintainability and lifespan. It will be part of the nrg-cli v3. THE AGE OF NON MINIFIED SINGLE FILE NODES AND JQUERY IS IN THE PAST!
How to define the client-side of your node. As you can see, you dont need to define defaults or credentials again because the source of truth for those is defined in the server-side of your node, using a json schema.
import { defineNode } from "../../../core/client";
import component from "../components/your-node-form.vue";
export default defineNode({
category: "function",
color: "#FFFFFF",
inputs: 1,
outputs: 1,
icon: "vue.png",
form: {
component,
disableSaveButtonOnError: true,
},
});
The server side part of a node can be created by extending the base classes called IONode and ConfigNode, as shown below. The definition of defaults and credentials happen in the server, using JSON Schemas and it is then sent to the client-side of the node via an http request done in the client right before the node is registered. Defaults and credentials are extracted from the schema and used in RED.nodes.registerType
alongside the other properties you defined for the client part of your node.
IO Node
import { Static } from "@sinclair/typebox";
import {
CloseDoneFunction,
InputDoneFunction,
IONode,
IONodeValidations,
SendFunction,
} from "../../../core/server/nodes";
import RemoteServerConfigNode from "./remote-server";
import {
ConfigsSchema,
CredentialsSchema,
InputMessageSchema,
OutputMessageSchema,
} from "../../schemas/your-node";
export type YourNodeConfigs = Static<typeof ConfigsSchema>;
export type YourNodeCredentials = Static<typeof CredentialsSchema>;
export type YourNodeInputMessage = Static<typeof InputMessageSchema>;
export type YourNodeOutputMessage = Static<typeof OutputMessageSchema>;
export default class YourNode extends IONode<
YourNodeConfigs,
YourNodeCredentials,
YourNodeInputMessage,
YourNodeOutputMessage
> {
static override validations: IONodeValidations = {
configs: ConfigsSchema,
credentials: CredentialsSchema,
input: InputMessageSchema,
outputs: OutputMessageSchema,
};
static override async init() {
console.log("testing your-node node init");
try {
const response = await fetch("https://dog.ceo/api/breeds/image/random");
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const json = await response.json();
console.log(json);
} catch (error) {
if (error instanceof Error) {
console.error("Error while fetching dogs: ", error.message);
} else {
console.error("Unknown error occurred: ", error);
}
}
}
override async onInput(
msg: {
payload?: string | undefined;
topic?: string | undefined;
_msgid?: string | undefined;
myVariable?: string | undefined;
},
send: SendFunction<{
payload?: string | undefined;
topic?: string | undefined;
_msgid?: string | undefined;
originalType: "string" | "number";
processedTime: number;
}>,
done: InputDoneFunction,
): Promise<void> {
console.log(this);
console.log(msg);
const server = IONode.getNode<RemoteServerConfigNode>(
this.configs.remoteServer,
);
console.log(server?.users);
const outputMsg: YourNodeOutputMessage = {
originalType: "number",
processedTime: 1,
};
send(outputMsg);
done();
}
override async onClose(
removed: boolean,
done: CloseDoneFunction,
): Promise<void> {
console.log("removing node");
console.log(removed);
done();
}
}
Config node
import { Static } from "@sinclair/typebox";
import { ConfigNode, ConfigNodeValidations } from "../../../core/server/nodes";
import { ConfigsSchema } from "../../schemas/remote-server";
export type RemoteServerConfigs = Static<typeof ConfigsSchema>;
export default class RemoteServerConfigNode extends ConfigNode<RemoteServerConfigs> {
static override validations: ConfigNodeValidations = {
configs: ConfigsSchema,
};
// NOTE: run only once when node type is registered
static override async init() {
console.log("testing remote-server node init");
try {
const response = await fetch("https://dog.ceo/api/breeds/image/random");
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const json = await response.json();
console.log(json);
} catch (error) {
if (error instanceof Error) {
console.error("Error while fetching dogs: ", error.message);
} else {
console.error("Unknown error occurred: ", error);
}
}
}
}
It is really easy to build your node library with this template
pnpm install
pnpm build
After building your node library, a dist
will appear in your local directory
You can install the build to your local Node-RED instance running
cd ~/.node-red
npm install ${PATH_CLONED_REPO}/dist
The index.html
file is generated automatically.
The files under dist
can be published to npm or other package manager of your choice. It contains everything consumers will need, like source maps and type declarations.
The server part of the node, as well as its dependencies, are bundled as common js using esbuild. Client dependencies are bundled using vite, chuncked and minified for fast loading. There is an example in /src/server/vite.config.ts
. Tree shaking and minification are performed in both builds.
During development locales are grouped by node to ease maintenance
once the project is built, labels and docs are grouped the way Node-RED is expecting
using i18n is now easier and natural than ever. A $i18n
global function is available to all your components. Labels are automatically scoped by the node's type.
There are a ton of other enhancements when using Vue for authoring your Node config forms. For example, typed and editor inputs are created using NodeRedTypedInput
and NodeRedEditorInput
in your component template. You no longer need to write any additional jquery function, or declare 2 props to store the value and the type of your typed input separately, like myPropValue and myPropType. Instead, you can have a single prop called myProp and pass it to your NodeRedTypedInput component. In the server myProp will have its value and type stored in myProp.value and myProp.type respectively. Additionaly, when exported, myProp will be serialized as an object, which is way easier to read.
Custom jquery widget and RED type augumentations will be replaced by the future official types that @GogoVega is writting. The ones in the repo are 99.99% incomplete and horrible, and were created just for testing intellisense in vscode's ts lang server.
The core folder and its dependencies is going to be packaged under @allanoricil/nrg-core (I wish I could own the @nrg scope but github ignored my requests). If node-red core team accepts my framework model I can put the nrg packages under the @node-red scope.
If you have ideas just open a PR or leave a comment here. Thanks.