@theotherwillembotha/node-red-plugincore 0.0.55 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +616 -217
  2. package/build/GenerateNodes.js +3 -6
  3. package/build/Nodes.html +3 -1502
  4. package/build/Nodes.js +5 -15
  5. package/build/Plugins.html +227 -0
  6. package/build/Plugins.js +18 -9
  7. package/build/core/NodeConstructor.d.ts +10 -16
  8. package/build/core/NodeConstructor.d.ts.map +1 -1
  9. package/build/core/NodeConstructor.js +112 -156
  10. package/build/core/NodeGenerator.d.ts.map +1 -1
  11. package/build/core/NodeGenerator.js +79 -29
  12. package/build/core/NodeManagerRuntime.d.ts +44 -0
  13. package/build/core/NodeManagerRuntime.d.ts.map +1 -0
  14. package/build/core/NodeManagerRuntime.js +140 -0
  15. package/build/core/Utils.d.ts.map +1 -1
  16. package/build/core/logger/service/LoggerService.d.ts +6 -5
  17. package/build/core/logger/service/LoggerService.d.ts.map +1 -1
  18. package/build/core/logger/service/LoggerService.js +51 -20
  19. package/build/core/logger/template/LoggerTemplate.d.ts.map +1 -1
  20. package/build/core/metrics/MetricsDecorator.d.ts +1 -2
  21. package/build/core/metrics/MetricsDecorator.d.ts.map +1 -1
  22. package/build/core/metrics/MetricsDecorator.js +89 -116
  23. package/build/core/metrics/node/CounterMetricConfigNode.d.ts.map +1 -1
  24. package/build/core/metrics/node/CounterMetricConfigNode.js +4 -5
  25. package/build/core/metrics/node/GaugeMetricConfigNode.d.ts.map +1 -1
  26. package/build/core/metrics/node/GaugeMetricConfigNode.js +5 -6
  27. package/build/core/metrics/node/MetricsConfigNode.d.ts +19 -9
  28. package/build/core/metrics/node/MetricsConfigNode.d.ts.map +1 -1
  29. package/build/core/metrics/node/MetricsConfigNode.js +14 -45
  30. package/build/core/metrics/node/TimerMetricConfigNode.d.ts.map +1 -1
  31. package/build/core/metrics/node/TimerMetricConfigNode.js +11 -17
  32. package/build/core/metrics/service/MetricsService.d.ts +110 -114
  33. package/build/core/metrics/service/MetricsService.d.ts.map +1 -1
  34. package/build/core/metrics/service/MetricsService.js +122 -339
  35. package/build/core/metrics/template/CounterMetricTemplate.d.ts.map +1 -1
  36. package/build/core/metrics/template/GaugeMetricTemplate.d.ts.map +1 -1
  37. package/build/core/metrics/template/MetricsTemplate.d.ts.map +1 -1
  38. package/build/core/metrics/template/MetricsTemplate.js +1 -2
  39. package/build/core/metrics/template/TimerMetricTemplate.d.ts.map +1 -1
  40. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts +1 -1
  41. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts.map +1 -1
  42. package/build/core/other/node/DelegatedConfigReferenceNode.js +1 -1
  43. package/build/core/other/service/InputService.d.ts.map +1 -1
  44. package/build/core/other/service/SettingsService.d.ts +1 -2
  45. package/build/core/other/service/SettingsService.d.ts.map +1 -1
  46. package/build/core/other/service/SettingsService.js +19 -5
  47. package/build/core/other/template/BasicTemplate.d.ts.map +1 -1
  48. package/build/core/other/template/SettingsTemplate.d.ts.map +1 -1
  49. package/build/core/state/node/InternalStateConfigNode.d.ts +16 -0
  50. package/build/core/state/node/InternalStateConfigNode.d.ts.map +1 -0
  51. package/build/core/state/node/InternalStateConfigNode.js +89 -0
  52. package/build/core/state/node/StateConfigNode.d.ts +24 -0
  53. package/build/core/state/node/StateConfigNode.d.ts.map +1 -0
  54. package/build/core/state/node/StateConfigNode.js +22 -0
  55. package/build/core/state/service/StateService.d.ts +34 -0
  56. package/build/core/state/service/StateService.d.ts.map +1 -0
  57. package/build/core/state/service/StateService.js +201 -0
  58. package/build/core/state/template/StateTemplate.d.ts +8 -0
  59. package/build/core/state/template/StateTemplate.d.ts.map +1 -0
  60. package/build/core/state/template/StateTemplate.js +11 -0
  61. package/build/core/tagging/ServiceDescriptionDecorator.d.ts +10 -0
  62. package/build/core/tagging/ServiceDescriptionDecorator.d.ts.map +1 -0
  63. package/build/core/tagging/ServiceDescriptionDecorator.js +10 -0
  64. package/build/core/tagging/service/NodeTypeService.d.ts +1 -2
  65. package/build/core/tagging/service/NodeTypeService.d.ts.map +1 -1
  66. package/build/core/tagging/service/NodeTypeService.js +19 -5
  67. package/build/core/ui/template/ScriptEditorTemplate.d.ts.map +1 -1
  68. package/build/core/ui/template/UIHelperTemplate.d.ts.map +1 -1
  69. package/build/core/webhook/WebhookDecorator.d.ts.map +1 -1
  70. package/build/core/webhook/WebhookDecorator.js +43 -6
  71. package/build/core/webhook/node/WebhookServerConfigNode.d.ts.map +1 -1
  72. package/build/core/webhook/service/ReverseProxyTypeService.d.ts +1 -0
  73. package/build/core/webhook/service/ReverseProxyTypeService.d.ts.map +1 -1
  74. package/build/core/webhook/service/WebhookServerService.d.ts +3 -4
  75. package/build/core/webhook/service/WebhookServerService.d.ts.map +1 -1
  76. package/build/core/webhook/service/WebhookServerService.js +48 -23
  77. package/build/core/webhook/template/WebhookTemplate.d.ts.map +1 -1
  78. package/build/index.d.ts +5 -2
  79. package/build/index.d.ts.map +1 -1
  80. package/build/index.js +6 -2
  81. package/build/runtime/NodeManagerRuntime.js +140 -0
  82. package/documentation/CounterMetricConfigNode.png +0 -0
  83. package/documentation/GaugeMetricConfigNode.png +0 -0
  84. package/documentation/LoggerTemplate.png +0 -0
  85. package/documentation/MetricsTemplate.png +0 -0
  86. package/documentation/StateTemplate.png +0 -0
  87. package/documentation/TimerMetricConfigNode_histogram.png +0 -0
  88. package/documentation/TimerMetricConfigNode_summary.png +0 -0
  89. package/documentation/WebhookServerConfigNode.png +0 -0
  90. package/documentation/WebhookTemplate.png +0 -0
  91. package/package.json +5 -12
  92. package/src/GenerateNodes.ts +7 -11
  93. package/src/core/NodeConstructor.ts +134 -197
  94. package/src/core/NodeGenerator.ts +88 -34
  95. package/src/core/NodeManagerRuntime.ts +179 -0
  96. package/src/core/logger/service/LoggerService.ts +39 -26
  97. package/src/core/logger/template/LoggerTemplate.html +13 -8
  98. package/src/core/metrics/MetricsDecorator.ts +140 -168
  99. package/src/core/metrics/node/CounterMetricConfigNode.ts +20 -20
  100. package/src/core/metrics/node/GaugeMetricConfigNode.ts +24 -25
  101. package/src/core/metrics/node/MetricsConfigNode.ts +29 -48
  102. package/src/core/metrics/node/TimerMetricConfigNode.html +2 -2
  103. package/src/core/metrics/node/TimerMetricConfigNode.ts +50 -59
  104. package/src/core/metrics/service/MetricsService.ts +214 -489
  105. package/src/core/metrics/template/CounterMetricTemplate.html +0 -1
  106. package/src/core/metrics/template/GaugeMetricTemplate.html +1 -1
  107. package/src/core/metrics/template/MetricsTemplate.html +132 -13
  108. package/src/core/metrics/template/MetricsTemplate.ts +3 -4
  109. package/src/core/other/node/DelegatedConfigReferenceNode.ts +1 -1
  110. package/src/core/other/service/SettingsService.ts +6 -10
  111. package/src/core/other/template/BasicTemplate.html +5 -2
  112. package/src/core/state/node/InternalStateConfigNode.html +77 -0
  113. package/src/core/state/node/InternalStateConfigNode.ts +102 -0
  114. package/src/core/state/node/StateConfigNode.ts +29 -0
  115. package/src/core/state/service/StateService.ts +180 -0
  116. package/src/core/state/template/StateTemplate.html +244 -0
  117. package/src/core/state/template/StateTemplate.ts +17 -0
  118. package/src/core/tagging/ServiceDescriptionDecorator.ts +23 -0
  119. package/src/core/tagging/service/NodeTypeService.ts +6 -11
  120. package/src/core/ui/template/UIHelperTemplate.html +41 -26
  121. package/src/core/webhook/WebhookDecorator.ts +53 -10
  122. package/src/core/webhook/service/ReverseProxyTypeService.ts +1 -0
  123. package/src/core/webhook/service/WebhookServerService.ts +39 -28
  124. package/src/core/webhook/template/WebhookTemplate.html +29 -36
  125. package/src/index.ts +7 -2
  126. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts +0 -20
  127. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts.map +0 -1
  128. package/build/core/logger/node/ConsoleLoggerConfigNode.html +0 -90
  129. package/build/core/logger/node/ConsoleLoggerConfigNode.js +0 -62
  130. package/build/core/logger/node/RestLoggerConfigNode.d.ts +0 -40
  131. package/build/core/logger/node/RestLoggerConfigNode.d.ts.map +0 -1
  132. package/build/core/logger/node/RestLoggerConfigNode.html +0 -183
  133. package/build/core/logger/node/RestLoggerConfigNode.js +0 -128
  134. package/build/core/logger/template/LoggerTemplate.html +0 -331
  135. package/build/core/metrics/node/CounterMetricConfigNode.html +0 -48
  136. package/build/core/metrics/node/GaugeMetricConfigNode.html +0 -48
  137. package/build/core/metrics/node/MetricsConfigNode.html +0 -21
  138. package/build/core/metrics/node/TimerMetricConfigNode.html +0 -190
  139. package/build/core/metrics/template/CounterMetricTemplate.html +0 -40
  140. package/build/core/metrics/template/GaugeMetricTemplate.html +0 -39
  141. package/build/core/metrics/template/MetricsTemplate.html +0 -54
  142. package/build/core/metrics/template/TimerMetricTemplate.html +0 -39
  143. package/build/core/other/node/DelegatedConfigReferenceNode.html +0 -1
  144. package/build/core/other/template/BasicTemplate.html +0 -76
  145. package/build/core/other/template/SettingsTemplate.html +0 -128
  146. package/build/core/ui/template/ScriptEditorTemplate.html +0 -50
  147. package/build/core/ui/template/UIHelperTemplate.html +0 -229
  148. package/build/core/webhook/node/WebhookServerConfigNode.html +0 -61
  149. package/build/core/webhook/template/WebhookTemplate.html +0 -442
  150. package/documentation/ConsoleLoggerConfigNode.png +0 -0
  151. package/documentation/MetricsConfigNode.png +0 -0
  152. package/documentation/RestLoggerConfigNode.png +0 -0
  153. package/documentation/TimerMetricConfigNode.png +0 -0
  154. package/documentation/WebhookNode.png +0 -0
  155. package/src/core/logger/node/ConsoleLoggerConfigNode.html +0 -90
  156. package/src/core/logger/node/ConsoleLoggerConfigNode.ts +0 -71
  157. package/src/core/logger/node/RestLoggerConfigNode.html +0 -183
  158. package/src/core/logger/node/RestLoggerConfigNode.ts +0 -170
package/README.md CHANGED
@@ -1,28 +1,13 @@
1
1
  # @theotherwillembotha/node-red-plugincore
2
2
 
3
- A TypeScript framework for building production-grade Node-RED plugins with built-in support for structured logging, Prometheus metrics, webhook servers, and reusable UI components.
3
+ A TypeScript framework for building production-grade Node-RED plugins. Rather than scaffolding blank nodes from scratch, plugincore provides a collection of reusable helper components - base classes, decorator-driven injection, composable UI templates, and an assembly framework - that eliminate boilerplate and allow cross-plugin functionality like logging, metrics, and state management to be shared and reused without duplication.
4
4
 
5
- This package has two roles:
5
+ This package serves two roles:
6
6
 
7
- 1. **Config nodes** a set of shared configuration nodes (loggers, metric collectors, webhook server) that are installed into Node-RED and referenced by other nodes in a flow.
8
- 2. **Developer framework** a TypeScript base library that plugin authors extend to build their own Node-RED nodes, with decorators and templates that wire in logging, metrics, webhooks, and editor UI automatically.
7
+ 1. **Developer framework** - TypeScript base classes, decorators, and a build-time node generator that plugin authors extend to create their own Node-RED nodes. Cross-cutting concerns (logging, metrics, state, webhooks) are injected by annotation rather than manually wired.
8
+ 2. **Shared runtime infrastructure** - a set of services and a Webhook Server config node that are installed into Node-RED and shared automatically across all plugins built on this framework.
9
9
 
10
- ---
11
-
12
- ## Plugin ecosystem
13
-
14
- This framework is the foundation for a growing set of Node-RED plugins. The following plugins are currently available:
15
-
16
- | Plugin | Description |
17
- |--------|-------------|
18
- | [@theotherwillembotha/node-red-telemetry](https://github.com/theotherwillembotha/nodered_telemetry) | Ready-to-use flow nodes for structured logging and Prometheus metrics — Logger, Counter, Gauge, and Timer nodes that attach to the config nodes provided by this package. |
19
- | [@theotherwillembotha/node-red-loki](https://github.com/theotherwillembotha/nodered_loki) | Grafana Loki logger config node. Extends the logging infrastructure provided by this package with a Loki transport, enabling log delivery directly to a Loki instance from any node built with the `@Logger` decorator. |
20
- | [@theotherwillembotha/node-red-nginxproxymanager](https://github.com/theotherwillembotha/nodered_nginxproxymanager) | Node-RED nodes for managing Nginx Proxy Manager hosts directly from your flows. Includes a config node that registers as a reverse proxy provider, an Update Host node for creating and updating proxy entries, and a Get Hosts node for retrieving the current host list. |
21
- | [@theotherwillembotha/node-red-circuitbreaker](https://github.com/theotherwillembotha/nodered_circuitbreaker) | Circuit Breaker nodes for building resilient flows. Detects faults in external integrations using configurable fault and trip functions, routes messages based on breaker state, and supports event-driven recovery flows. |
22
- | [@theotherwillembotha/node-red-temporal](https://github.com/theotherwillembotha/nodered_temporal) | Date/time transformation nodes powered by the TC39 Temporal API. Parse, convert, adjust, and format date/time values across timezones using named presets or Moment.js-style custom format strings. |
23
- | [@theotherwillembotha/node-red-zookeeper](https://github.com/theotherwillembotha/nodered_zookeeper) | Apache ZooKeeper integration nodes. Subscribe to ZooKeeper node change events, read node values on demand, and write data to ZooKeeper nodes from your flows. |
24
-
25
- Additional plugins will be listed here as they are published.
10
+ Logger providers (Console, REST, Loki) and metrics providers (Prometheus) ship as separate optional plugins that plug into this infrastructure. See the [plugin ecosystem](#plugin-ecosystem) table at the end of this document.
26
11
 
27
12
  ---
28
13
 
@@ -36,65 +21,44 @@ Either use the **Manage Palette** option in the Node-RED editor, or run the foll
36
21
  npm install @theotherwillembotha/node-red-plugincore
37
22
  ```
38
23
 
39
- This installs the config nodes below and makes them available in your palette. Typically you would also install a plugin such as [node-red-telemetry](https://github.com/theotherwillembotha/nodered_telemetry) to get the actual flow nodes that reference them.
24
+ This installs the shared infrastructure and the Webhook Server config node. To use logging or metrics in your flows, also install the relevant provider plugin (e.g. `node-red-logging`, `node-red-prometheus`).
40
25
 
41
26
  ### Config nodes
42
27
 
43
- Config nodes are shared resources configured once and referenced across your flow. They appear under their own groups in the palette sidebar.
44
-
45
- #### Logging
46
-
47
- Two logger backends are built in. Additional backends are available as separate plugins (see the [plugin ecosystem](#plugin-ecosystem) table above). All expose the same interface and are interchangeable — any node built with the `@Logger` decorator can use any of them.
48
-
49
- **Console Logger** — writes structured log output to stdout via Winston. Ideal for development and containerised deployments that forward stdout to a log aggregator.
50
-
51
- ![Console Logger Config](documentation/ConsoleLoggerConfigNode.png)
52
-
53
- **REST Logger** — ships log entries to a remote HTTP/HTTPS endpoint. Supports Basic and API Key authentication.
54
-
55
- ![REST Logger Config](documentation/RestLoggerConfigNode.png)
28
+ #### Webhook Server
56
29
 
57
- All loggers share a **Level** selector (debug, info, warn, error) and a **Template** field a Handlebars template that controls the shape of each log entry. The default `message:{{msg}}` passes the raw message through; you can customise it to include only the fields you care about.
30
+ The Webhook Server config node runs an Express v5 HTTP server on a configurable local port. It serves as the shared HTTP listener for any node that uses the `@Webhook` decorator or `WebhookTemplate`. Supports optional reverse proxy configuration so that registered webhook paths know their publicly-visible address.
58
31
 
59
- #### Metrics
32
+ ![Webhook Server Config Node](documentation/WebhookServerConfigNode.png)
60
33
 
61
- Prometheus-compatible metric collectors. A scrape endpoint (`/metrics`) is provided automatically once any metric node is deployed.
34
+ *The Webhook Server config node. **Name** is a human-readable label shown in the Server dropdown of any node using `WebhookTemplate`. **Port** is the local TCP port the Express server listens on — each server instance must use a unique port. The **External Host** and **External Port** fields (not shown) are used when the server sits behind a reverse proxy, allowing registered webhook nodes to know their publicly-visible address.*
62
35
 
63
- | Node | Description |
64
- |------|-------------|
65
- | **Metrics Config** | Top-level Prometheus registry. One per deployment. |
66
- | **Counter Metric** | An ever-increasing counter (e.g. messages processed, errors). |
67
- | **Gauge Metric** | A value that goes up and down (e.g. queue depth, active connections). |
68
- | **Timer Metric** | A histogram or summary for measuring durations (e.g. processing time per message). |
36
+ #### Logger providers
69
37
 
70
- ![Metrics Config](documentation/MetricsConfigNode.png)
38
+ Logger config nodes are provided by separate plugins:
71
39
 
72
- **Counter** and **Gauge** share the same layout — a name, a description, and an optional reset-on-deploy toggle:
40
+ - **Console Logger**, **REST Logger** - [@theotherwillembotha/node-red-logging](https://github.com/theotherwillembotha/nodered_logging)
41
+ - **Loki Logger** - [@theotherwillembotha/node-red-loki](https://github.com/theotherwillembotha/nodered_loki)
73
42
 
74
- ![Counter Metric Config](documentation/CounterMetricConfigNode.png)
75
- ![Gauge Metric Config](documentation/GaugeMetricConfigNode.png)
43
+ Once a logger plugin is installed, a **Logging** section appears automatically in the editor of any node built with this framework. It is hidden when no logger plugins are present.
76
44
 
77
- The **Timer** config adds a metric type selector (Histogram or Summary) with configurable bucket or percentile strategies:
45
+ #### Metrics providers
78
46
 
79
- ![Timer Metric Config](documentation/TimerMetricConfigNode.png)
47
+ Metrics config nodes are provided by separate plugins:
80
48
 
81
- #### Webhook Server
49
+ - **Prometheus** (Counter, Gauge, Histogram, `/metrics` endpoint) - [@theotherwillembotha/node-red-prometheus](https://github.com/theotherwillembotha/nodered_prometheus)
82
50
 
83
- | Node | Description |
84
- |------|-------------|
85
- | **Webhook Server** | Runs an Express v5 HTTP server on a configurable local port. Supports optional reverse proxy configuration so that registered webhook paths know their publicly-visible address. |
86
-
87
- ![Webhook Server Config](documentation/WebhookNode.png)
51
+ The **Metrics** section appears automatically in any node editor that supports metrics. It is hidden when no metrics plugins are installed.
88
52
 
89
53
  ---
90
54
 
91
- ## Development Building plugins with this framework
55
+ ## Development - Building plugins with this framework
92
56
 
93
57
  ### Prerequisites
94
58
 
95
59
  - Node.js 18+
96
60
  - Node-RED 4+
97
- - TypeScript 5+ with `experimentalDecorators` and `emitDecoratorMetadata` enabled
61
+ - TypeScript 5+
98
62
 
99
63
  ### Installation
100
64
 
@@ -102,98 +66,574 @@ The **Timer** config adds a metric type selector (Histogram or Summary) with con
102
66
  npm install @theotherwillembotha/node-red-plugincore
103
67
  ```
104
68
 
105
- Your `tsconfig.json` must include:
69
+ ### Required tsconfig.json
106
70
 
107
71
  ```json
108
72
  {
109
73
  "compilerOptions": {
110
- "experimentalDecorators": true,
111
- "emitDecoratorMetadata": true
74
+ "target": "es2022",
75
+ "module": "commonjs",
76
+ "rootDir": "./src",
77
+ "outDir": "./build",
78
+ "declaration": true,
79
+ "experimentalDecorators": true,
80
+ "emitDecoratorMetadata": true,
81
+ "useDefineForClassFields": false
112
82
  }
113
83
  }
114
84
  ```
115
85
 
116
- ### Defining a node
86
+ > **`useDefineForClassFields: false` is non-negotiable.** With `target: "es2022"`, TypeScript defaults this to `true`, which emits native class field initializers that run *after* `__decorate()`. This silently overwrites every `@Logger`, `@Metrics`, and other decorator-injected property with `undefined` at runtime - with no compile error and no startup warning.
87
+
88
+ > **`declaration: true`** is required if any downstream TypeScript package imports types from your plugin. Without it, consumers get `Could not find a declaration file for module '...'`.
89
+
90
+ ---
91
+
92
+ ## Building a flow node
93
+
94
+ A flow node is a node that appears on the Node-RED palette and processes messages passing through a flow. It extends `BaseNode`.
95
+
96
+ ### Minimal example
97
+
98
+ The simplest possible flow node - no logging, no metrics, just message handling.
99
+
100
+ **`src/myplugin/node/EchoNode.ts`**
101
+
102
+ ```typescript
103
+ import {
104
+ BaseNode, // base class for all flow nodes
105
+ BaseNodeConfig, // base interface for node config - all config interfaces extend this
106
+ NodeDescription, // decorator that registers the node type with the build system
107
+ SourceUtility, // resolves HTML file path correctly in both build and dev contexts
108
+ onInput, // decorator that wires a method to node.on("input")
109
+ Message // Node-RED message type
110
+ } from "@theotherwillembotha/node-red-plugincore";
111
+ import { Node } from "node-red";
112
+
113
+ // Extend BaseNodeConfig to declare this node's own configuration fields.
114
+ // Each field maps to a form input with id="node-input-<fieldName>".
115
+ interface EchoNodeConfig extends BaseNodeConfig {
116
+ name: string;
117
+ prefix: string;
118
+ }
119
+
120
+ @NodeDescription({
121
+ id: "EchoNode", // unique node type identifier across all plugins
122
+ name: "Echo", // display name in the Node-RED editor
123
+ group: "my-plugin", // palette group heading
124
+ sourceFile: SourceUtility.getSourcePath("/build/", "/src/") + "EchoNode.html",
125
+ package: "@myscope/node-red-myplugin", // must match the npm package name exactly
126
+ })
127
+ class EchoNode extends BaseNode<EchoNodeConfig> {
128
+
129
+ constructor(node: Node, config: EchoNodeConfig) {
130
+ super(node, config);
131
+ // this.config is available here and throughout the class
132
+ }
133
+
134
+ // @onInput wires this method to node.on("input") automatically.
135
+ // No need to call node.on("input", ...) in the constructor.
136
+ @onInput()
137
+ protected onMessageReceived(message: Message): void {
138
+ (message as any).payload = `${this.config.prefix}: ${(message as any).payload}`;
139
+ this.node().send(message as any);
140
+ }
141
+ }
142
+
143
+ export { EchoNode };
144
+ ```
145
+
146
+ **`src/myplugin/node/EchoNode.html`**
117
147
 
118
- Extend `BaseNode` (or `ConfigNode` for config nodes) and annotate the class with `@NodeDescription`. The decorator registers the node type, its editor HTML file, the palette group it appears in, and any shared templates it composes in.
148
+ ```html
149
+ <!-- onCompose runs at build time. "node" is a NodeBuilder instance.
150
+ Register defaults and configure the palette appearance here.
151
+ This section is NOT shipped to the browser. -->
152
+ <script type="text/javascript" template-section="onCompose">
153
+ node.addDefault("name", { value: "" });
154
+ node.addDefault("prefix", { value: "Echo", required: true });
155
+ node.setLabel(function() { return this.name || "Echo"; });
156
+ node.setColor("#87CEEB");
157
+ node.setIcon("font-awesome/fa-reply");
158
+ node.setInput("in");
159
+ node.addOutputs(["out"]);
160
+ </script>
161
+
162
+ <!-- onIncludeEditForm is the HTML rendered inside the editor dialog.
163
+ Use id="node-input-<fieldName>" - Node-RED auto-saves and restores these. -->
164
+ <script type="text/html" template-section="onIncludeEditForm">
165
+ <div class="form-row">
166
+ <label for="node-input-name">Name</label>
167
+ <input type="text" id="node-input-name" placeholder="Echo">
168
+ </div>
169
+ <div class="form-row">
170
+ <label for="node-input-prefix">Prefix</label>
171
+ <input type="text" id="node-input-prefix" placeholder="Echo">
172
+ </div>
173
+ </script>
174
+
175
+ <!-- onIncludeDocumentation is rendered in the Node-RED sidebar Info panel. -->
176
+ <script type="text/markdown" template-section="onIncludeDocumentation">
177
+ ## Echo
178
+
179
+ Prepends a configurable prefix to `msg.payload`.
180
+
181
+ ### Properties
182
+
183
+ - **Name** - Display name in the editor.
184
+ - **Prefix** - Text prepended to the incoming payload.
185
+
186
+ ### Output
187
+
188
+ The modified message is passed to the single output.
189
+ </script>
190
+ ```
191
+
192
+ ---
193
+
194
+ ### Full-featured example
195
+
196
+ This version adds structured logging, a metrics counter, and a webhook route. Compare each addition to the minimal example above.
197
+
198
+ **`src/myplugin/node/WebhookListenerNode.ts`**
119
199
 
120
200
  ```typescript
121
201
  import {
122
202
  BaseNode, BaseNodeConfig,
123
203
  NodeDescription, SourceUtility,
124
- LoggerTemplate, LoggerTemplateConfig,
125
- Log, Logger
204
+ LoggerTemplate, LoggerTemplateConfig, // + adds the logging UI section and its config fields
205
+ MetricsTemplate, MetricsTemplateConfig, // + adds the metrics UI section and its config fields
206
+ WebhookTemplate, // + adds the webhook server selector and path field
207
+ Log, Logger, // @Logger injects a Log instance at runtime
208
+ CounterMetric, Metrics, MetricType, // @Metrics injects a metric collector at runtime
209
+ Webhook, // @Webhook registers HTTP routes with the webhook server
210
+ onInput, Message
126
211
  } from "@theotherwillembotha/node-red-plugincore";
127
212
  import { Node } from "node-red";
128
-
129
- interface MyNodeConfig extends BaseNodeConfig, LoggerTemplateConfig {
130
- name: string;
213
+ import { Request, Response } from "express";
214
+
215
+ // Extend all three mixin config interfaces to include the fields
216
+ // that LoggerTemplate, MetricsTemplate, and WebhookTemplate manage.
217
+ interface WebhookListenerNodeConfig
218
+ extends BaseNodeConfig, LoggerTemplateConfig, MetricsTemplateConfig {
219
+ name: string;
220
+ webhookServer: string; // id of the WebhookServerConfigNode
221
+ path: string; // HTTP path registered with the webhook server
131
222
  }
132
223
 
133
224
  @NodeDescription({
134
- id: "my-node",
135
- name: "My Node",
136
- group: "my-plugin",
137
- sourceFile: SourceUtility.getSourcePath("/build/", "/src/") + "MyNode.html",
138
- package: "@myscope/my-nodered-plugin",
225
+ id: "WebhookListenerNode",
226
+ name: "Webhook Listener",
227
+ group: "my-plugin",
228
+ sourceFile: SourceUtility.getSourcePath("/build/", "/src/") + "WebhookListenerNode.html",
229
+ package: "@myscope/node-red-myplugin",
139
230
  templates: [
140
- { template: LoggerTemplate, config: {} }
231
+ // Each template injects its own form rows, edit lifecycle hooks, and
232
+ // onCompose defaults. Order here controls the order they appear in the editor.
233
+ { template: LoggerTemplate, config: {} },
234
+ { template: MetricsTemplate, config: {} },
235
+ { template: WebhookTemplate, config: {} },
141
236
  ]
142
237
  })
143
- class MyNode extends BaseNode<MyNodeConfig> {
238
+ class WebhookListenerNode extends BaseNode<WebhookListenerNodeConfig> {
144
239
 
240
+ // Injected by the framework at runtime. Declare with ! - TypeScript cannot
241
+ // see the injection mechanism at compile time.
145
242
  @Logger()
146
243
  private log!: Log;
147
244
 
148
- constructor(node: Node, config: MyNodeConfig) {
245
+ // Injected by the framework. The concrete implementation depends on which
246
+ // metrics provider plugin is installed (e.g. Prometheus). Falls back to a
247
+ // silent no-op if no provider is configured.
248
+ @Metrics({ name: "webhook_requests_total", help: "Total inbound webhook requests", type: MetricType.Counter })
249
+ private requestCount!: CounterMetric;
250
+
251
+ // @Webhook registers the methods below with the WebhookServerConfigNode
252
+ // referenced by this.config.webhookServer. Routes are registered on deploy
253
+ // and removed on close.
254
+ @Webhook()
255
+ private webhook: any;
256
+
257
+ constructor(node: Node, config: WebhookListenerNodeConfig) {
149
258
  super(node, config);
150
259
  }
151
260
 
152
- protected onInit() {
153
- this.log.log("MyNode initialised");
261
+ // HTTP route registered via @Webhook. Method name determines the HTTP verb.
262
+ // The path is read from this.config.path at runtime.
263
+ public post(req: Request, res: Response): void {
264
+ this.requestCount.inc();
265
+ this.log.log({ event: "webhook.received", body: req.body });
266
+ this.node().send({ payload: req.body } as any);
267
+ res.status(200).json({ ok: true });
268
+ }
269
+
270
+ @onInput()
271
+ protected onMessageReceived(message: Message): void {
272
+ // Example: forward messages onward unchanged
273
+ this.node().send(message as any);
154
274
  }
155
275
  }
276
+
277
+ export { WebhookListenerNode };
278
+ ```
279
+
280
+ **`src/myplugin/node/WebhookListenerNode.html`**
281
+
282
+ The node's HTML file only contains its *own* fields. `LoggerTemplate`, `MetricsTemplate`, and `WebhookTemplate` inject their own form rows and edit lifecycle sections automatically - those sections are merged with this file's sections at build time.
283
+
284
+ ```html
285
+ <script type="text/javascript" template-section="onCompose">
286
+ // Only register this node's own fields. Logger, Metrics, and Webhook
287
+ // fields are added automatically by their respective templates.
288
+ node.addDefault("name", { value: "" });
289
+ node.addDefault("webhookServer", { value: "", required: true });
290
+ node.addDefault("path", { value: "/webhook" });
291
+ node.setLabel(function() { return this.name || "Webhook Listener"; });
292
+ node.setColor("#F0A500");
293
+ node.setIcon("font-awesome/fa-sign-in");
294
+ node.setInput("in");
295
+ node.addOutputs(["out"]);
296
+ </script>
297
+
298
+ <!-- This section runs when the editor dialog opens (oneditprepare).
299
+ Initialize any custom widgets here. Logger/Metrics/Webhook templates
300
+ have their own onIncludeEditPrepare that runs alongside this one. -->
301
+ <script type="text/javascript" template-section="onIncludeEditPrepare">
302
+ // Custom widget initialization goes here
303
+ </script>
304
+
305
+ <!-- The editor form. Logger, Metrics, and Webhook sections are appended
306
+ automatically below these rows by their respective templates. -->
307
+ <script type="text/html" template-section="onIncludeEditForm">
308
+ <div class="form-row">
309
+ <label for="node-input-name">Name</label>
310
+ <input type="text" id="node-input-name" placeholder="Webhook Listener">
311
+ </div>
312
+ <!-- Webhook server + path fields are injected by WebhookTemplate -->
313
+ </script>
314
+
315
+ <script type="text/markdown" template-section="onIncludeDocumentation">
316
+ ## Webhook Listener
317
+
318
+ Registers an HTTP endpoint with a Webhook Server config node and emits a
319
+ message for each incoming request.
320
+
321
+ ### Properties
322
+
323
+ - **Name** - Display name in the editor.
324
+ - **Logging** - Select a logger provider and optionally override the log template.
325
+ - **Metrics** - Select a metrics provider to track request counts.
326
+ - **Webhook Server** - The Webhook Server config node providing the HTTP listener.
327
+ - **Path** - The HTTP path to register (e.g. `/webhook`).
328
+
329
+ ### Output
330
+
331
+ `msg.payload` contains the parsed request body.
332
+ </script>
333
+ ```
334
+
335
+ > **Key differences from the minimal example:**
336
+ > - `templates` array in `@NodeDescription` - each entry auto-composes a UI section and its lifecycle hooks.
337
+ > - Config interface mixins (`LoggerTemplateConfig`, `MetricsTemplateConfig`) - provide TypeScript types for the fields those templates manage.
338
+ > - `@Logger()`, `@Metrics()`, `@Webhook()` on class properties - injected at runtime; always declare with `!`.
339
+ > - The node's HTML only handles its own fields; template-owned fields are never duplicated.
340
+
341
+ ---
342
+
343
+ ## Building a config node
344
+
345
+ A config node is a shared configuration resource referenced by multiple flow nodes. It extends `ConfigNode`. Config nodes do not appear on the palette; they are created from the editor's config node panel.
346
+
347
+ Config nodes follow the same pattern as flow nodes with two differences:
348
+ - Extend `ConfigNode<TConfig>` instead of `BaseNode<TConfig>`
349
+ - Form field IDs use `node-config-input-<fieldName>` instead of `node-input-<fieldName>`
350
+
351
+ ```typescript
352
+ @NodeDescription({ id: "MyConfigNode", name: "My Config", group: "my-plugin", ... })
353
+ class MyConfigNode extends ConfigNode<MyConfigNodeConfig> {
354
+ constructor(node: Node, config: MyConfigNodeConfig) {
355
+ super(node, config);
356
+ // register with a service, open a connection, etc.
357
+ }
358
+ }
359
+ ```
360
+
361
+ ```html
362
+ <script type="text/html" template-section="onIncludeEditForm">
363
+ <div class="form-row">
364
+ <!-- config nodes use node-config-input-*, not node-input-* -->
365
+ <label for="node-config-input-name">Name</label>
366
+ <input type="text" id="node-config-input-name">
367
+ </div>
368
+ </script>
369
+ ```
370
+
371
+ ---
372
+
373
+ ## Cross-cutting infrastructure
374
+
375
+ These subsystems are the building blocks behind every shared UI section. Understanding how the pieces fit together makes it easier to build plugins that integrate cleanly with the rest of the ecosystem.
376
+
377
+ ### Logger infrastructure
378
+
379
+ The logging subsystem has four parts:
380
+
381
+ | Part | Role |
382
+ |------|------|
383
+ | `LoggerService` | Background service that maintains logger instances. Must be registered in `GenerateNodes.ts`. |
384
+ | `@Logger()` | Property decorator. Injects a `Log` instance wired to whichever logger provider the user selected. |
385
+ | `LoggerTemplate` | UI template. Adds a **Logging** section to any node editor - an enable toggle, a logger provider selector, and an optional message template override. Hidden when no logger provider plugins are installed. |
386
+ | `LoggerConfigNode` | Abstract base class that logger provider plugins extend. Not registered in plugincore itself. |
387
+
388
+ **Using logging in a node:**
389
+
390
+ ```typescript
391
+ // 1. Extend LoggerTemplateConfig in the node's config interface
392
+ interface MyNodeConfig extends BaseNodeConfig, LoggerTemplateConfig { ... }
393
+
394
+ // 2. Include LoggerTemplate in @NodeDescription templates
395
+ @NodeDescription({ ..., templates: [{ template: LoggerTemplate, config: {} }] })
396
+ class MyNode extends BaseNode<MyNodeConfig> {
397
+
398
+ // 3. Declare the injected property
399
+ @Logger()
400
+ private log!: Log;
401
+
402
+ @onInput()
403
+ protected handle(msg: Message): void {
404
+ this.log.log({ event: "received", payload: (msg as any).payload });
405
+ }
406
+ }
407
+ ```
408
+
409
+ **Creating a logger provider:**
410
+
411
+ Extend `LoggerConfigNode` and tag it so the `NodeTypeService` can discover it:
412
+
413
+ ```typescript
414
+ import { LoggerConfigNode, ConfigNodeConfig, NodeDescription, SourceUtility }
415
+ from "@theotherwillembotha/node-red-plugincore";
416
+
417
+ interface ConsoleLoggerConfig extends ConfigNodeConfig {
418
+ name: string;
419
+ level: string;
420
+ template: string;
421
+ }
422
+
423
+ @NodeDescription({
424
+ id: "ConsoleLoggerConfigNode",
425
+ name: "Console Logger",
426
+ group: "logging",
427
+ sourceFile: SourceUtility.getSourcePath("/build/", "/src/") + "ConsoleLoggerConfigNode.html",
428
+ package: "@myscope/node-red-logging",
429
+ tags: ["LoggerType"], // <-- required: makes this discoverable as a logger provider
430
+ })
431
+ class ConsoleLoggerConfigNode extends LoggerConfigNode<ConsoleLoggerConfig> { ... }
432
+ ```
433
+
434
+ The `"LoggerType"` tag is what causes this node to appear in the **Logger** dropdown of any node using `LoggerTemplate`.
435
+
436
+ **Screenshot - LoggerTemplate section** (as it appears inside a consumer node's editor):
437
+
438
+ ![Logger Template](documentation/LoggerTemplate.png)
439
+
440
+ *The Logging section injected by `LoggerTemplate`. The enable toggle controls whether log calls are forwarded to the provider. The Logger dropdown lists all installed logger providers, with edit (pencil) and create (plus) buttons. Override Template allows per-node customisation of the log format.*
441
+
442
+ ---
443
+
444
+ ### Metrics infrastructure
445
+
446
+ The metrics subsystem mirrors the logging subsystem exactly:
447
+
448
+ | Part | Role |
449
+ |------|------|
450
+ | `MetricsService` | Background service that maintains metric collector registrations. Must be registered in `GenerateNodes.ts`. |
451
+ | `@Metrics({...})` | Property decorator. Injects the appropriate metric type (Counter, Gauge, Histogram) from the configured provider. Falls back to a silent no-op when no provider is configured. |
452
+ | `MetricsTemplate` | UI template. Adds a **Metrics** section with a provider selector. Hidden when no metrics plugins are installed. |
453
+ | `MetricsConfigNode` | Abstract base class that metrics provider plugins extend. |
454
+
455
+ **Metric types:**
456
+
457
+ | `MetricType` | Property type | Description |
458
+ |--------------|--------------|-------------|
459
+ | `Counter` | `CounterMetric` | Monotonically increasing count. `.inc(amount?)` |
460
+ | `Gauge` | `GaugeMetric` | Value that can go up or down. `.set(value)`, `.inc()`, `.dec()` |
461
+ | `Histogram` | `HistogramMetric` | Distribution of observed values. `.observe(value)` |
462
+ | `Summary` | `SummaryMetric` | Quantile distribution. `.observe(value)` |
463
+
464
+ ```typescript
465
+ @Metrics({ name: "messages_total", help: "Messages processed", type: MetricType.Counter })
466
+ private messageCount!: CounterMetric;
467
+
468
+ @Metrics({ name: "queue_depth", help: "Current queue depth", type: MetricType.Gauge })
469
+ private queueDepth!: GaugeMetric;
470
+
471
+ @Metrics({ name: "process_seconds", help: "Processing latency", type: MetricType.Histogram })
472
+ private latency!: HistogramMetric;
156
473
  ```
157
474
 
158
- The `LoggerTemplate` fragment is automatically composed into the node's editor panel, giving the user a logger selector and optional message template override with no additional HTML required.
475
+ **Screenshot - MetricsTemplate section:**
159
476
 
160
- ### Available decorators
477
+ ![Metrics Template](documentation/MetricsTemplate.png)
161
478
 
162
- | Decorator | Property type | What it injects |
163
- |-----------|--------------|-----------------|
164
- | `@Logger()` | `Log` | Winston logger wired to a user-selected logger config node |
165
- | `@Metrics({...})` | `CounterMetric` / `GaugeMetric` / `HistogramMetric` | Prometheus metric collector |
166
- | `@Webhook()` | — | Registers the node's routes with the webhook server |
167
- | `@onInput()` | method | Wires the method as the Node-RED `input` message handler |
479
+ *The Metrics section injected by `MetricsTemplate`. Enable Metrics controls whether metric calls are forwarded. The Metric Provider dropdown lists all installed metrics providers.*
168
480
 
169
- ### Templates
481
+ **Metric config nodes** (provided by `node-red-prometheus`):
170
482
 
171
- Templates bundle reusable UI fragments that compose into any node's editor panel. Include them in the `templates` array of `@NodeDescription`.
483
+ Plugin authors can create specialised metric config nodes to let users define named, reusable metric collectors that are referenced by flow nodes.
172
484
 
173
- | Template | Adds to editor |
174
- |----------|---------------|
175
- | `LoggerTemplate` | Logger backend selector and optional message template override |
176
- | `MetricsTemplate` | Metrics enable toggle and collector reference |
177
- | `CounterMetricTemplate` | Counter config node reference |
178
- | `GaugeMetricTemplate` | Gauge config node reference |
179
- | `TimerMetricTemplate` | Timer config node reference |
180
- | `WebhookTemplate` | Webhook server reference, path, auth, and reverse proxy config |
181
- | `UIHelperTemplate` | Global `PluginCore.dialog()` and `PluginCore.table()` UI factories (see below) |
182
- | `ScriptEditorTemplate` | Global `PluginCore.createScriptEditor()` factory for Monaco-based script editors (see below) |
183
- | `SettingsTemplate` | General settings section |
184
- | `BasicTemplate` | Base styles shared by all nodes |
485
+ ![Counter Metric Config Node](documentation/CounterMetricConfigNode.png)
185
486
 
186
- ### UI helpers
487
+ *Counter Metric Config Node - tracks a monotonically increasing count. **Name** and **Description** label the counter in the Prometheus `/metrics` output. **Reset on Deploy** zeroes the counter each time flows are re-deployed. **Metric Provider** selects the Prometheus instance to register with.*
187
488
 
188
- Including `UIHelperTemplate` in a node's `templates` list injects two client-side factory functions into the Node-RED editor page. Both are available globally as `PluginCore.dialog(...)` and `PluginCore.table(...)` and are styled to match Node-RED's own editor aesthetic.
489
+ ![Gauge Metric Config Node](documentation/GaugeMetricConfigNode.png)
189
490
 
190
- #### `PluginCore.dialog(options)`
491
+ *Gauge Metric Config Node - tracks a value that can increase or decrease freely. Configuration is identical to Counter, except the underlying metric type allows `.inc()`, `.dec()`, and `.set(value)` calls.*
191
492
 
192
- Opens a modal overlay with a title bar and one or more tabs. Closes on the close button, an overlay click, or Escape.
493
+ Timer Metric Config Nodes can operate in two modes selected by the **Type** dropdown:
494
+
495
+ ![Timer Metric Config Node - Summary](documentation/TimerMetricConfigNode_summary.png)
496
+
497
+ *Summary mode. **Percentile Type** controls whether percentile boundaries are computed automatically or specified manually. The **Percentiles** field accepts a comma-separated list of quantiles (e.g. `0.01, 0.1, 0.9, 0.99`).*
498
+
499
+ ![Timer Metric Config Node - Histogram](documentation/TimerMetricConfigNode_histogram.png)
500
+
501
+ *Histogram mode. Observations are sorted into fixed buckets defined by **Bucket Type**, **Start**, **Interval**, and **Count**. Linear bucketing (shown) produces evenly spaced boundaries starting at Start and stepping by Interval.*
502
+
503
+ ---
504
+
505
+ ### State infrastructure
506
+
507
+ The state subsystem provides a common interface for externalising node state - allowing state to be stored and coordinated outside of Node-RED (e.g. in ZooKeeper). Nodes that need persistent or distributed state can use this subsystem without being coupled to a specific storage backend.
508
+
509
+ | Part | Role |
510
+ |------|------|
511
+ | `StateConfigNode` | Abstract base class. State provider plugins extend this. |
512
+ | `StateTemplate` | UI template. Adds a **State** section with a provider selector to any node editor. Auto-creates an `InternalStateConfigNode` on first open if no provider is configured. |
513
+
514
+ **Creating a state provider:**
515
+
516
+ Extend `StateConfigNode` and tag it `"StateProvider"`:
517
+
518
+ ```typescript
519
+ @NodeDescription({ ..., tags: ["StateProvider"] })
520
+ class MyStateConfigNode extends StateConfigNode<MyStateConfig> { ... }
521
+ ```
522
+
523
+ **Screenshot - StateTemplate section:**
524
+
525
+ ![State Template](documentation/StateTemplate.png)
526
+
527
+ *The State section injected by `StateTemplate`. The Provider dropdown lists all installed state provider nodes. The pencil button opens the selected provider's editor. If no external state provider is installed, an Internal State Config Node is created automatically and selected.*
528
+
529
+ ---
530
+
531
+ ### Webhook infrastructure
532
+
533
+ The webhook subsystem provides a shared Express v5 HTTP server that multiple nodes can register routes on.
534
+
535
+ | Part | Role |
536
+ |------|------|
537
+ | `WebhookServerConfigNode` | Config node (provided by plugincore itself). Runs the HTTP server on a configured port. |
538
+ | `@Webhook()` | Property decorator. Registers public methods (`get`, `post`, `put`, `delete`) as HTTP routes with the referenced `WebhookServerConfigNode`. |
539
+ | `WebhookTemplate` | UI template. Adds a webhook server selector and path field to the node editor. |
540
+
541
+ ![Webhook Template](documentation/WebhookTemplate.png)
542
+
543
+ *The Webhook section injected by `WebhookTemplate`. **Server** is a dropdown that lists all `WebhookServerConfigNode` instances, with edit and add buttons. **Path** is the HTTP route registered on that server (e.g. `/example`). **Authentication** selects the credential strategy — None, Basic Auth, or API Key — and reveals the relevant credential fields when a strategy other than None is selected. **Reverse Proxy** is hidden unless a reverse proxy provider plugin (e.g. `node-red-nginxproxymanager`) is installed; when shown, it lets you map the webhook path to a public-facing domain.*
544
+
545
+ ---
546
+
547
+ ## NodeTypeService and DelegatedConfigReferenceNode
548
+
549
+ These two components are the plumbing behind the provider selector dropdowns. They are required in every plugin's `GenerateNodes.ts`.
550
+
551
+ ### NodeTypeService
552
+
553
+ `NodeTypeService` exposes a `GET /nodetypeservice/find?tag=<tag>` endpoint that returns all registered node types carrying a given tag. This is how `LoggerTemplate`, `MetricsTemplate`, and `StateTemplate` discover available providers at runtime without any hardcoded knowledge of which plugins are installed.
554
+
555
+ When you annotate a node with `tags: ["LoggerType"]`, the `NodeTypeService` indexes it under that tag. Multiple tags are supported:
556
+
557
+ ```typescript
558
+ @NodeDescription({
559
+ id: "PrometheusMetricsConfigNode",
560
+ tags: ["MetricsProvider"], // discoverable by MetricsTemplate
561
+ ...
562
+ })
563
+ ```
564
+
565
+ The selector templates query this endpoint in `onIncludeEditPrepare` and hide the entire section if the response is empty - so nodes that use `LoggerTemplate` or `MetricsTemplate` remain clean in setups where those provider plugins are not installed.
566
+
567
+ ### DelegatedConfigReferenceNode
568
+
569
+ `DelegatedConfigReferenceNode` is a thin shim config node that acts as the property type for provider selector fields. It provides reference counting - Node-RED tracks how many nodes reference a given config node and prevents deletion while it is in use - without the normal behaviour where Node-RED resets a config selector to blank when the editor saves.
570
+
571
+ This is necessary because provider selectors (Logger, Metrics, State) are managed manually by the template's JavaScript rather than by Node-RED's auto-bind mechanism. Using `DelegatedConfigReferenceNode` as the declared type preserves reference counting while letting the template control the actual value.
572
+
573
+ `DelegatedConfigReferenceNode` must be registered in every plugin's `GenerateNodes.ts`:
574
+
575
+ ```typescript
576
+ new NodeGenerator(...)
577
+ .registerNode(DelegatedConfigReferenceNode)
578
+ ...
579
+ ```
580
+
581
+ ---
582
+
583
+ ## Node HTML file - template sections
584
+
585
+ Each node's `.html` file is divided into named sections using the `template-section` attribute. The build pipeline reads these sections and assembles them into the correct slots in the generated Node-RED registration call. Sections from multiple templates (`LoggerTemplate`, `MetricsTemplate`, etc.) are merged in the order they were registered.
586
+
587
+ ```html
588
+ <script type="text/javascript" template-section="onCompose"> ... </script>
589
+ <div template-section="onIncludeOnce"> ... </div>
590
+ <script type="text/javascript" template-section="onIncludeEditPrepare"> ... </script>
591
+ <script type="text/html" template-section="onIncludeEditForm"> ... </script>
592
+ <script type="text/javascript" template-section="onIncludeEditSave"> ... </script>
593
+ <script type="text/javascript" template-section="onIncludeEditCancel"> ... </script>
594
+ <script type="text/javascript" template-section="onIncludeEditDelete"> ... </script>
595
+ <script type="text/markdown" template-section="onIncludeDocumentation"> ... </script>
596
+ ```
597
+
598
+ ### Section reference
599
+
600
+ | Section | When it runs | Typical use |
601
+ |---------|-------------|-------------|
602
+ | `onCompose` | At **build time** inside a `NodeBuilder` context | Call `node.addDefault(...)`, `node.setLabel(...)`, `node.setColor(...)`, `node.setIcon(...)`, `node.setInput(...)`, `node.addOutputs(...)`. Not shipped to the browser. |
603
+ | `onIncludeOnce` | Injected into the browser **once per page load** | Global styles (`<style>`), shared helper functions, and cached resource fetches. Wrap scripts in `<script>` inside a `<div>`. Shared across all instances. |
604
+ | `onIncludeEditPrepare` | Runs when the **node editor opens** (`oneditprepare`) | Initialise `typedInput` widgets, bind event handlers, fetch async data, restore saved state. Assign `this` to a variable before async code. |
605
+ | `onIncludeEditForm` | The **HTML form** inside the editor dialog | `<div class="form-row">` blocks. Use `id="node-input-<field>"` for flow nodes; `id="node-config-input-<field>"` for config nodes. |
606
+ | `onIncludeEditSave` | Runs when the user clicks **Done** (`oneditsave`) | Read widget values back into the node object. Standard `node-input-*` fields save automatically; use this only for non-standard fields. |
607
+ | `onIncludeEditCancel` | Runs when the user clicks **Cancel** (`oneditcancel`) | Clean up resources - always call `.dispose()` on `ScriptEditorTemplate` editors here to avoid Monaco memory leaks. |
608
+ | `onIncludeEditDelete` | Runs when the node is **deleted** | Release any persistent resources tied to this node instance. Rarely needed. |
609
+ | `onIncludeDocumentation` | Rendered in the **help panel** sidebar | Markdown describing the node's behaviour, properties, inputs, and outputs. |
610
+
611
+ ### `onCompose` - NodeBuilder API
612
+
613
+ | Method | Description |
614
+ |--------|-------------|
615
+ | `node.addDefault(name, options)` | Register a config field. `options`: `{ value, required?, validate? }` |
616
+ | `node.setLabel(fn)` | Function returning the node's label at runtime (`this` = node instance) |
617
+ | `node.setPaletteLabel(label)` | Fixed palette label string |
618
+ | `node.setColor(color)` | Palette colour (hex string) |
619
+ | `node.setIcon(icon)` | Palette icon filename (relative to `icons/`, or `font-awesome/fa-*`) |
620
+ | `node.setLabelStyle(style)` | CSS class for the label (e.g. `node_label_white`) |
621
+ | `node.setInput(label)` | Add an input port |
622
+ | `node.addOutputs(labels)` | Add one or more output ports; pass a string array for labelled outputs |
623
+
624
+ ---
625
+
626
+ ## UI helpers
627
+
628
+ Including `UIHelperTemplate` in a node's `templates` array injects two client-side factory functions into the Node-RED editor page. Both are globally available as `PluginCore.dialog(...)` and `PluginCore.table(...)` and are styled to match Node-RED's editor aesthetic.
629
+
630
+ ### `PluginCore.dialog(options)`
631
+
632
+ Opens a modal overlay with a title bar and one or more tabs.
193
633
 
194
634
  ```javascript
195
635
  PluginCore.dialog({
196
- title: "My Plugin Status",
636
+ title: "My Plugin - Status",
197
637
  tabs: [
198
638
  {
199
639
  label: "Proxy Hosts",
@@ -201,14 +641,14 @@ PluginCore.dialog({
201
641
  $container.append(
202
642
  PluginCore.table({
203
643
  columns: [
204
- { key: "id", label: "ID" },
205
- { key: "name", label: "Name" },
644
+ { key: "id", label: "ID" },
645
+ { key: "name", label: "Name" },
206
646
  { key: "enabled", label: "Enabled",
207
647
  render: function(v) {
208
648
  return $("<span>")
209
649
  .addClass(v ? "plugincore-status-enabled"
210
650
  : "plugincore-status-disabled")
211
- .text(v ? "Enabled" : "Disabled");
651
+ .text(v ? "Enabled" : "Disabled");
212
652
  }}
213
653
  ],
214
654
  rows: data
@@ -220,22 +660,18 @@ PluginCore.dialog({
220
660
  });
221
661
  ```
222
662
 
223
- **Options:**
224
-
225
- | Field | Type | Description |
226
- |-------|------|-------------|
227
- | `title` | `string` | Heading shown in the dialog title bar |
663
+ | Option | Type | Description |
664
+ |--------|------|-------------|
665
+ | `title` | `string` | Heading in the dialog title bar |
228
666
  | `tabs` | `array` | One or more tab definitions |
229
667
  | `tabs[].label` | `string` | Tab heading |
230
- | `tabs[].render` | `function($container)` | Called with a jQuery element; append content into it |
668
+ | `tabs[].render` | `function($container)` | Called with a jQuery element - append content into it |
231
669
 
232
- **Returns:** `{ close() }` call `close()` to dismiss the dialog programmatically.
233
-
234
- ---
670
+ **Returns:** `{ close() }` - call `close()` to dismiss programmatically.
235
671
 
236
- #### `PluginCore.table(config)`
672
+ ### `PluginCore.table(config)`
237
673
 
238
- Returns a styled jQuery `<table>` element ready to append into any container.
674
+ Returns a styled jQuery `<table>` ready to append into any container.
239
675
 
240
676
  ```javascript
241
677
  var $table = PluginCore.table({
@@ -249,154 +685,97 @@ var $table = PluginCore.table({
249
685
  $container.append($table);
250
686
  ```
251
687
 
252
- **Config:**
253
-
254
688
  | Field | Type | Description |
255
689
  |-------|------|-------------|
256
- | `columns` | `array` | Column definitions |
257
690
  | `columns[].key` | `string` | Property name on each row object |
258
691
  | `columns[].label` | `string` | Column header text |
259
- | `columns[].render` | `function(value, row)` | Optional. Return a string or jQuery element for custom cell rendering |
692
+ | `columns[].render` | `function(value, row)` | Optional - return a string or jQuery element for custom cells |
260
693
  | `rows` | `object[]` | Data rows |
261
694
 
262
- **CSS classes available for cell content:**
695
+ **CSS classes for cell content:**
263
696
 
264
- | Class | Colour | Intended use |
265
- |-------|--------|-------------|
266
- | `plugincore-status-enabled` | Green | Enabled / active state |
267
- | `plugincore-status-disabled` | Red | Disabled / inactive state |
697
+ | Class | Colour | Use |
698
+ |-------|--------|-----|
699
+ | `plugincore-status-enabled` | Green | Active / enabled state |
700
+ | `plugincore-status-disabled` | Red | Inactive / disabled state |
268
701
 
269
- #### `PluginCore.createScriptEditor(elementId, template, initialValue)`
702
+ ### `PluginCore.createScriptEditor(elementId, template, initialValue)`
270
703
 
271
- Including `ScriptEditorTemplate` in a node's `templates` list injects a Monaco-based script editor factory into the Node-RED editor page. It wraps the async Monaco initialisation boilerplate into a single call and returns a `{ getValue(), dispose() }` handle.
704
+ Including `ScriptEditorTemplate` in a node's `templates` array injects a Monaco-based script editor factory. Wraps the async Monaco initialisation into a single call.
272
705
 
273
706
  ```javascript
274
707
  // In onIncludeEditPrepare:
275
- let scriptTemplate = `
276
- interface Message { [key: string]: any; }
277
- async function(msg: Message) {
278
- \${script}
279
- }
280
- `;
281
-
708
+ let node = this;
282
709
  node.scriptEditor = PluginCore.createScriptEditor(
283
- 'node-input-script-editor', // DOM id of the container element
284
- scriptTemplate, // TypeScript context template
285
- node.script || 'return true;' // initial value
710
+ 'node-input-script-editor', // DOM id of the container element
711
+ `async function(msg) { \${script} }`, // TypeScript context template
712
+ node.script || 'return true;' // initial value
286
713
  );
287
714
 
288
715
  // In onIncludeEditSave:
289
716
  node.script = node.scriptEditor.getValue();
290
717
  delete node.scriptEditor;
291
718
 
292
- // In IncludeEditCancel:
719
+ // In onIncludeEditCancel - always call dispose() to avoid Monaco memory leaks:
293
720
  node.scriptEditor.dispose();
294
721
  delete node.scriptEditor;
295
722
  ```
296
723
 
297
- The `template` string provides the TypeScript context that the Monaco language service uses for diagnostics, completions, and hover info. Use `\${script}` as the placeholder for the user's code. The user only sees their code — the surrounding context is invisible to them but informs type checking.
298
-
299
724
  **Returns:** `{ getValue(): string, dispose(): void }`
300
725
 
726
+ The `template` string provides TypeScript context that the Monaco language service uses for type checking and completions. Use `${script}` as the placeholder for the user's code - they see only their code, not the surrounding context.
727
+
301
728
  ---
302
729
 
303
- > **Note — Handlebars escaping in node HTML files**
730
+ > **Handlebars escaping in node HTML files**
304
731
  >
305
- > Node HTML files (`.html` template files) are processed by Handlebars during the build. This means any `{{ }}` syntax in the HTML including in JavaScript comments or JSDoc will be interpreted as a Handlebars expression and produce unexpected output or an error.
732
+ > Node HTML files are processed by Handlebars during the build step. Any `{{ }}` anywhere in the file - including `<script>` blocks, comments, and markdown sections - will be interpreted as a Handlebars expression.
306
733
  >
307
- > Escape curly braces with a backslash wherever they appear literally in the file:
734
+ > Escape curly braces with a backslash wherever they appear literally:
308
735
  >
309
736
  > ```javascript
310
- > // Wrong Handlebars will try to evaluate this:
311
- > // @returns {{ getValue(): string }}
312
- >
313
- > // Correct — escaped so Handlebars passes it through:
314
- > // @returns \{{ getValue(): string \}}
737
+ > // Wrong: @returns {{ getValue(): string }}
738
+ > // Correct: @returns \{{ getValue(): string \}}
315
739
  > ```
316
- >
317
- > This applies anywhere in the HTML file: `<script>` blocks, inline styles, markdown documentation sections, and comments.
318
740
 
319
741
  ---
320
742
 
321
- ### Node HTML file — template sections
743
+ ## Registering nodes for generation
322
744
 
323
- Each node's `.html` file is divided into named sections using the `template-section` attribute. The build pipeline reads these sections and assembles them into the correct slots in the generated Node-RED registration call. Sections from multiple templates (e.g. `LoggerTemplate`, `MetricsTemplate`) are merged automatically in the order they were registered.
324
-
325
- ```html
326
- <script type="text/javascript" template-section="onCompose"> ... </script>
327
- <div template-section="onIncludeOnce"> ... </div>
328
- <script type="text/javascript" template-section="onIncludeEditPrepare"> ... </script>
329
- <script type="text/html" template-section="onIncludeEditForm"> ... </script>
330
- <script type="text/javascript" template-section="onIncludeEditSave"> ... </script>
331
- <script type="text/javascript" template-section="onIncludeEditCancel"> ... </script>
332
- <script type="text/javascript" template-section="onIncludeEditDelete"> ... </script>
333
- <script type="text/markdown" template-section="onIncludeDocumentation"> ... </script>
334
- ```
335
-
336
- #### Section reference
337
-
338
- | Section | When it runs | Typical use |
339
- |---------|-------------|-------------|
340
- | `onCompose` | At **build time**, inside a `NodeBuilder` context | Call `node.addDefault(...)`, `node.setLabel(...)`, `node.setColor(...)`, `node.setIcon(...)`, `node.addOutputs(...)`, etc. to configure the node definition that will be written into the generated `Nodes.js`. This section is **not** shipped to the browser. |
341
- | `onIncludeOnce` | Injected into the browser **once** per page load | Global styles (`<style>`), shared helper functions, and cached resource fetches (e.g. timezone or format lists). Everything here is shared across all node instances of this type. Wrap scripts in `<script>` tags inside a `<div>`. |
342
- | `onIncludeEditPrepare` | Runs when the **node editor dialog opens** (Node-RED `oneditprepare`) | Initialise `typedInput` widgets, bind event listeners, fetch async data, restore saved state. `this` refers to the node being edited — assign it to a local variable (e.g. `let node = this`) before any async code. |
343
- | `onIncludeEditForm` | The **HTML form** rendered inside the editor dialog | `<div class="form-row">` blocks containing `<label>` and `<input>` elements. Use `id="node-input-<fieldName>"` for regular nodes or `id="node-config-input-<fieldName>"` for config nodes. Include hidden `<input type="hidden">` fields for `typedInput` type tracking. |
344
- | `onIncludeEditSave` | Runs when the user clicks **Done** (Node-RED `oneditsave`) | Read widget values back into the node object before it is serialised. Most `typedInput` widgets save automatically via the `node-input-*` naming convention; use this section for anything that does not. |
345
- | `onIncludeEditCancel` | Runs when the user clicks **Cancel** (Node-RED `oneditcancel`) | Clean up resources that were created in `onIncludeEditPrepare` — e.g. call `.dispose()` on Monaco editor instances to avoid memory leaks. |
346
- | `onIncludeEditDelete` | Runs when the node is **deleted** from the canvas | Release any persistent resources tied to this node instance. Rarely needed for most nodes. |
347
- | `onIncludeDocumentation` | Rendered in the Node-RED **help panel** (sidebar Info tab) | Markdown content describing the node's behaviour, fields, and examples. Supports standard GitHub-flavoured markdown including tables, code blocks, and blockquotes. |
348
-
349
- #### `onCompose` — NodeBuilder API
350
-
351
- The `onCompose` script runs at build time with `node` bound to a `NodeBuilder` instance. The following methods are available:
352
-
353
- | Method | Description |
354
- |--------|-------------|
355
- | `node.addDefault(name, options)` | Register a config field. `options`: `{ value, required, validate? }`. The optional `validate` function is serialised as-is into the generated `defaults` block and runs in the browser editor. |
356
- | `node.setLabel(fn)` | Set a function that returns the node's palette label at runtime. |
357
- | `node.setPaletteLabel(label)` | Set the fixed palette label. |
358
- | `node.setColor(color)` | Set the node's palette colour (hex string). |
359
- | `node.setIcon(icon)` | Set the node's palette icon filename (relative to the plugin's `icons/` directory). |
360
- | `node.setLabelStyle(style)` | Set the CSS class for the palette label (e.g. `node_label_white`). |
361
- | `node.setInput(label)` | Add an input port with the given label. |
362
- | `node.addOutputs(labels)` | Add one or more output ports. Pass a string array for multiple labelled outputs. |
363
-
364
- #### Data flow through the edit lifecycle
365
-
366
- ```
367
- oneditprepare → [user edits] → oneditsave (Done clicked)
368
- → oneditcancel (Cancel clicked)
369
- → oneditdelete (node deleted)
370
- ```
371
-
372
- Values flow through `node-input-<field>` (or `node-config-input-<field>`) named inputs. Node-RED automatically saves and restores these between sessions. Fields not following this convention must be manually read in `onIncludeEditSave` and written in `onIncludeEditPrepare`.
373
-
374
- ---
375
-
376
- ### Registering nodes for generation
377
-
378
- Create a `GenerateNodes.ts` at the root of your `src/` directory. This is the composition root — register every service, template, and node, then call `.generate()` to emit the two Node-RED entry files (`Nodes.js` and `Plugins.js`).
745
+ Create a `GenerateNodes.ts` at the root of your `src/` directory. This is the composition root - register every service, template, and node, then call `.generate()` to emit the two Node-RED entry files.
379
746
 
380
747
  ```typescript
381
748
  import { NodeGenerator } from "@theotherwillembotha/node-red-plugincore";
382
749
  import {
383
- LoggerService, LoggerTemplate,
384
- ConsoleLoggerConfigNode, RestLoggerConfigNode
750
+ LoggerService, MetricsService, NodeTypeService, SettingsService,
751
+ DelegatedConfigReferenceNode,
752
+ BasicTemplate, LoggerTemplate, MetricsTemplate,
385
753
  } from "@theotherwillembotha/node-red-plugincore";
386
- import { MyNode } from "./nodes/MyNode";
387
754
 
388
- new NodeGenerator("./src/")
755
+ import { MyService } from "./myplugin/service/MyService";
756
+ import { MyConfigNode } from "./myplugin/node/MyConfigNode";
757
+ import { MyNode } from "./myplugin/node/MyNode";
758
+
759
+ new NodeGenerator("./src/myplugin/")
760
+ // ── Infrastructure ── required by every plugin; deduplication guards make this safe to register from multiple plugins
389
761
  .registerService(LoggerService)
762
+ .registerService(MetricsService)
763
+ .registerService(NodeTypeService)
764
+ .registerService(SettingsService)
765
+ .registerTemplate(BasicTemplate)
390
766
  .registerTemplate(LoggerTemplate)
391
- .registerNode(ConsoleLoggerConfigNode)
392
- .registerNode(RestLoggerConfigNode)
767
+ .registerTemplate(MetricsTemplate)
768
+ .registerNode(DelegatedConfigReferenceNode)
769
+ // ── Plugin-specific ──────────────────────────────────────────────────────
770
+ .registerService(MyService)
771
+ .registerNode(MyConfigNode)
393
772
  .registerNode(MyNode)
394
773
  .generate("./build/Nodes", "./build/Plugins");
395
774
 
396
775
  process.exit(0);
397
776
  ```
398
777
 
399
- Only register the services and config nodes your plugin actually depends on. You do not need to re-register nodes from this package if your plugin does not expose them directly.
778
+ Logger and metrics provider nodes (`ConsoleLoggerConfigNode`, `PrometheusMetricsConfigNode`, etc.) are **not** registered here. They are registered by their own dedicated plugins and discovered at runtime through the `NodeTypeService` tag system.
400
779
 
401
780
  ### Wiring up package.json
402
781
 
@@ -413,12 +792,32 @@ Only register the services and config nodes your plugin actually depends on. You
413
792
  ### Build
414
793
 
415
794
  ```bash
416
- npm run build # clean → tsc → generate node files → copy icons
795
+ npm run build # clean → tsc → generate node files → bundle (esbuild) → copy icons
417
796
  npm run clean # remove build/
418
797
  ```
419
798
 
420
799
  ---
421
800
 
801
+ ## Plugin ecosystem
802
+
803
+ These plugins are built on this framework and available from npm. Each plugin is independently installable - only install what your flows need.
804
+
805
+ | Plugin | Uses from plugincore | Provides |
806
+ |--------|---------------------|----------|
807
+ | [@theotherwillembotha/node-red-logging](https://github.com/theotherwillembotha/nodered_logging) | `LoggerService` `LoggerConfigNode` | **Console Logger Config Node** (config) - writes log entries to stdout with configurable level and Handlebars message template<br>**REST Logger Config Node** (config) - POSTs structured log entries to an HTTP endpoint |
808
+ | [@theotherwillembotha/node-red-loki](https://github.com/theotherwillembotha/nodered_loki) | `LoggerService` `LoggerConfigNode` `LoggerTemplate` | **Loki Config Node** (config) - shared connection reference for a Grafana Loki backend<br>**Loki Logger Config Node** (config) - streams structured log entries to Loki<br>**Loki Query Node** (flow) - executes LogQL queries against a Loki backend and emits results |
809
+ | [@theotherwillembotha/node-red-telemetry](https://github.com/theotherwillembotha/nodered_telemetry) | `LoggerService` `LoggerTemplate` | **Logger Node** (flow) - attaches to any installed logger provider; routes `msg` through the selected logger on each message |
810
+ | [@theotherwillembotha/node-red-prometheus](https://github.com/theotherwillembotha/nodered_prometheus) | `MetricsService` `MetricsConfigNode` `WebhookTemplate` | **Prometheus Metrics Config Node** (config) - hosts a `/metrics` scrape endpoint via a Webhook Server<br>**Counter Metric Config Node** (config) - named monotonic counter<br>**Gauge Metric Config Node** (config) - named up/down value gauge<br>**Timer Metric Config Node** (config) - named latency timer (Histogram or Summary mode) |
811
+ | [@theotherwillembotha/node-red-zookeeper](https://github.com/theotherwillembotha/nodered_zookeeper) | `BaseNode` `ConfigNode` `LoggerTemplate` `StateConfigNode` | **ZooKeeper Server Config Node** (config) - shared ZooKeeper client connection<br>**ZooKeeper State Config Node** (config) - maps flow state names to ZooKeeper node values<br>**ZooKeeper Read Node** (flow) - reads a ZooKeeper node value on demand<br>**ZooKeeper Write Node** (flow) - writes a value to a ZooKeeper node<br>**ZooKeeper Subscribe Node** (flow) - emits a message each time a ZooKeeper node changes |
812
+ | [@theotherwillembotha/node-red-circuitbreaker](https://github.com/theotherwillembotha/nodered_circuitbreaker) | `BaseNode` `ConfigNode` `LoggerTemplate` `MetricsTemplate` `StateTemplate` `ScriptEditorTemplate` | **Circuit Breaker Config Node** (config) - manages open/closed/half-open state with configurable fault detection and trip logic<br>**Fault Detector Config Node** (config) - user-defined TypeScript function that evaluates messages for faults<br>**Circuit Breaker Node** (flow) - routes messages based on current breaker state and emits state-change events |
813
+ | [@theotherwillembotha/node-red-temporal](https://github.com/theotherwillembotha/nodered_temporal) | `BaseNode` `LoggerTemplate` | **Temporal Transform Node** (flow) - parses, adjusts, and formats date/time values across timezones using the TC39 Temporal API<br>**Temporal Duration Node** (flow) - computes the signed duration between two date/time values |
814
+ | [@theotherwillembotha/node-red-whatsapp](https://github.com/theotherwillembotha/nodered_whatsapp) | `BaseNode` `ConfigNode` `LoggerTemplate` `WebhookTemplate` | **WhatsApp Config Node** (config) - manages a Baileys WhatsApp session (no cloud API or subscription required)<br>**WhatsApp Send Node** (flow) - sends a WhatsApp message from a flow<br>**WhatsApp Receive Node** (flow) - emits a message for each incoming WhatsApp message |
815
+ | [@theotherwillembotha/node-red-nginxproxymanager](https://github.com/theotherwillembotha/nodered_nginxproxymanager) | `BaseNode` `ConfigNode` `LoggerTemplate` `UIHelperTemplate` | **Nginx Proxy Manager Config Node** (config) - shared connection to an Nginx Proxy Manager instance; registers as a reverse proxy provider for Webhook Server nodes<br>**Update Host Node** (flow) - creates or updates a proxy host entry<br>**Get Hosts Node** (flow) - retrieves the current list of proxy hosts, displayable via `PluginCore.dialog()` |
816
+
817
+ Additional plugins will be listed here as they are published.
818
+
819
+ ---
820
+
422
821
  ## Repository
423
822
 
424
823
  - Source: [github.com/theotherwillembotha/nodered_plugincore](https://github.com/theotherwillembotha/nodered_plugincore)