Nodes dependent on other nodes

I'm trying to restructure a monolith library of nodes (50+ different node types) into something that might be a bit more manageable and more focused on use-cases.

Basically I have three modules: Core, Foo and Bar. Core contains Configuration Nodes used by Foo and Bar. It is built as an NPM package and pushed to a repo. It deploys just fine and I can see that the ConfigNodes from Core are registered in the NodeRed registry of the running NodeRed instance via the admin cli.

Module Foo and Bar imports the Core Module NPM package (in order to reuse some of the functionality provided by it), however, when attempting to deploy it, the logs show the error:

[@example/foo/nodes] 'MyServiceConfigNode' already registered by module @example/core

By the looks of it, it tries to deploy the Core Module nodes even though they are not explicitly being deployed by Foo Module.

  1. Is there a way around this? At the moment it seems like the only viable option is to split the Core Module into two packages: one for the functionality and one for the nodes and only import the Core Functionality package in modules Foo and Bar.

  2. Is there a way to determine which nodes are deployed at node-deployment-time? My alternative solution to the above problem would be to replace the Core Module with the Core Module Source when packaging modules Foo and Bar and just have them check if a particular Node has been deployed before deploying it.

Apologies if the answer is something really obvious, but searching for resolving node hierarchies has not provided much in terms of answers or insight.

The import of MyServiceConfigNode by foo is causing the function that has the registerType to be executed again. This happens because the server definition of the node is coupled with its registration. You have to ensure registerType of MyServiceConfigNode isn't called inside the exported node definition in your core.

If you want to write nodes using modern js, take a look at this framework Im putting together. Even if you decide not to use it, take a look how I was able to separate the registerType call from the node definition in the server side

I have the same use case with my nodes. Linked below is my config node. You'll find links to the two modules containing the nodes in the readme.

Roughly speaking: Foo and Bar have Core as direct dependency. When NR starts, NR will retrieve the nodes from Foo, Bar, and Core, which allows Core to exist. Foo and Bar must not have Core's nodes defined in their package.json.

This implementation has the following major drawbacks:

  • it requires a restart of NR (to load the config node).
  • although it's rare (and obscure...), NPM can install Core in the Foo or Bar directory, and in this case, NR won't load the Core nodes.

Take a look at my code, and I'll try to answer your questions :slightly_smiling_face:

This is kind of what I do with UIBUILDER which is, as you can imagine, fairly complex these days. Though UIBUILDER doesn't use config nodes (don't ask, that was an early design decision that I might do differently if starting now), it does use many library modules, some of which are consumed by multiple nodes. So a slightly different but possibly related use-case.

The trick that I use extensively is to make the library modules classes that self-declare as singletons. That way, they can never execute more than once in any Node-RED instance and they become shared as single instances to every node that needs them. In addition, the first node that is loaded that uses a library triggers the singleton so it never matters what order nodes are loaded in.

I threw a bit more time behind this problem and tried to follow the general advice as much as I could and I think I've managed to find the culprit.

Part of my build step loads an external file that specifies which nodes are assembled into which modules (like I said... there are a lot of them) and produces a "nodes.js" and "nodes.html" file for each.

It turns out that the error is not thrown by NR from attempting to register a duplicate node type via RED.nodes.registerType function in the nodes.js file, but by RED.nodes.registerType in the nodes.html file. The reason for the same node types appearing in multiple modules was an attempt to make the modules self-contained; eg, Foo Module would also contain the nodes from Core Module, in case they havent been installed yet.

A partial solution might be to add the RED.nodes.registerType function inside a

if(!RED.nodes.registry.getNodeTypes().includes("MyServiceConfigNode")){
    RED.nodes.registerType('MyServiceConfigNode',{
    ........
    ........
    ........
    });
}

block, that doesn't solve the issue of the ui block

<script type="text/html" data-template-name="MyServiceConfigNode">
    ........
    ........
    ........
</script>

Alternatively I'll have to just concede, and add every node only to a single module and figure out a different mechanism for managing the module dependencies.

You could always use an Editor plugin to register common config nodes.

My nodes no longer include any js in the html file, instead the js code is loaded as a resource. Here is an example:
image

The help panel is defined in a locales sub-folder:

This makes the editor code a heck of a lot easier to manage.

So the editor plugin that uibuilder uses is just this:

<link type="text/css" rel="stylesheet" href="./resources/node-red-contrib-uibuilder/editor-common.css" media="all">
<script type="module" async src="./resources/node-red-contrib-uibuilder/editor-common.js"></script>

It simply loads a common module in my case. All of the nodes reference that module. Because it is in a separate plugin, it is only ever executed once, when the editor loads. So you can safely register your common config nodes there (I only need to register the plugin itself).


The onadd method of the plugin, runs the code in this case. It registers a number of event handlers, some utility functions, and creates a global uibuilder object that any of my nodes can consume.

Prior to using this method, every node included the common module. Obviously, that meant that the code got run once for every node and so I had to wrap the code in something that only allowed a single execution. Use of the plugin simplifies that greatly.

Try to decouple the "node definition" from the registerType call. Then export the node definition in a module alone. Then in your foo/bar modules import "only" the node definition from the core package.

The node defintion is the object you pass to the registerType method. Take a look in the template repo I shared with you.

If the nodes are registered via a plugin, I don't believe any of that complexity is required. Also, plugins are loaded first I believe and so you should be able to guarantee that the config nodes are loaded before any actual nodes (would need testing as I've not actually done this).

My ui templates and are generated automatically based on a set of meta templates (handlebars) that provide common ui components that are shared across nodes (eg, a checkbox that enables / disables metrics or logging on a specific node). Stacking checks before registering nodes is trivial.

I dug a bit through the NR code and found that the data-template-name section is pretty much fixed to be loaded from a file that matches the name of the .js file.

Putting together the various recommendations, here is what I am thinking...

  • Scrub the idea of having a single massive nodes.html and nodes.js file and break each into its own html and js file. (we'll do the decoupling of the node definition later)
  • Create a plugin that will act as the loader for the nodes, but instead of loading them as nodes, load them as individual modules.
  • before "Loading a node as a module", check if it has been loaded.

What would be the simplest way to get a handle on the loader / registry in NR during the plugin initialization?

I have already explained this.

ok. thanks