Anticipating the pushback: some folk will argue this detracts from Visual Flow based Programming - and to a degree, that's a valid point. But there's already a common pattern, often suggested, of storing functions in context to write once, use many times (DRY). This prototype is an attempt to formalise that existing practice while bringing it as close to expected IDE behaviour as possible (within the existing confines of the Node-RED editor).
What...
What really bothers me about Node-RED is the cognitive load of working on larger projects where common utility functions are needed to reduce repeated code / keep things DRY. The usual pattern (stash a function in global/flow context) works, but has severe limitations and is just a pain in the you know what:
- No function signature or parameter hints when you call it elsewhere
- No "go to definition" - you have to close the editor you're in, hunt down the node that defined the function, open that, and then find your way back
- Nothing tells you what a context-stored value even looks like until you go read the source
I've been prototyping something and want some feedback.
1. A "Module" mode for the Function node. Flip a function node to Module mode and it stops processing messages (no inputs/outputs) - instead it just defines and exports plain JS via module.exports = {...}. Other Function nodes pull it in with a new node.import(idOrName) - synchronous, a live reference (not a message round-trip).
2. Real editor tooling for it - this is the part that addresses the cognitive load:
- Real autocomplete (with JSDoc) for whatever a library node exports, driven by actual TypeScript inference against its live source — not a hand-rolled scrape, so it stays accurate as the code changes
- Ctrl+click (Cmd+click on macOS) on
node.import("mathlib")opens that node's own editor, stacked on top of the one you're in (closing returns you back to where you were) - Alt+F12 / Peek Definition shows a read-only preview of the library's source without leaving your current editor
Why this pattern
I went through a few iterations before this one clicked:
RED.function.register(name, fn) inside existing code - the obvious "just add an API" move. The problem: the function code still lives buried inside whatever node's code happens to call it - there's no independent, addressable "home" for it. The editor would have to somehow parse arbitrary user code looking for register() calls and extract just the referenced function's text to do anything useful with it. And since it's not a node, there's no id, no name, no natural place for "go to definition" to land you - i'd have to invent all of that from scratch just to get back to feature parity with what nodes already give you for free.
A brand new "Function Library" node type - cleaner than the above, but it means learning a whole new node, a new palette entry, a new mental model, for something that's conceptually 95% identical to a Function node (JS sandbox, Setup/On Start tabs, Monaco editor, deploy lifecycle). Every one of those pieces already exists and already works; a new node type would mean re-solving problems the Function node solved years ago.
What made "just add a Mode to the Function node" click was realising almost everything I needed was already sitting there:
- Identity is free. A Module-mode node is still a real node - it already has an id, a name, an editor, a place on the canvas. Ctrl+click-to-navigate isn't a new feature so much as it is
RED.editor.edit(RED.nodes.node(id)), a call Node-RED already makes internally whenever you click through to a referenced config node. - The addressing model already existed. Link Call node already solves "reference another node by id or name, scoped to the current flow, refuse if ambiguous" for calling a link-in subroutine.
node.import()just mirrors that exact resolution order for a synchronous, non-message-passing case. I didn't have to invent a new mental model - users who already understand Link Call already understand this. - The code stays in a normal node property, not in a runtime value. This is the one that made the tooling possible at all. If a reusable function lives in
global.set(...), its shape only exists once the flow is running - the editor has zero visibility into it at design time. Keep the function's source as a node's own editable text (exactly like a Function node's body always has been) and the editor can reason about it statically, before any deploy has ever happened - which is the whole reason real TypeScript inference and ctrl+click became possible in the first place. module.exportsis a standard pattern. Exporting viamodule.exports = {...}is the same CommonJS convention JS/Node developers already know (no bespoke Node-RED API to learn). It's also why the real TypeScript inference is possible at all: TypeScript has understood CommonJSmodule.exportsshape-inference for years, so I get accurate, JSDoc-aware types for free rather than having to invent a custom type-extraction scheme.






