@theotherwillembotha/node-red-plugincore 0.1.0 → 0.3.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 (167) hide show
  1. package/README.md +616 -220
  2. package/build/GenerateNodes.js +9 -33
  3. package/build/Nodes.html +2 -1505
  4. package/build/Nodes.js +5 -19
  5. package/build/Plugins.html +11 -0
  6. package/build/Plugins.js +17 -12
  7. package/build/core/NodeConstructor.d.ts +12 -16
  8. package/build/core/NodeConstructor.d.ts.map +1 -1
  9. package/build/core/NodeConstructor.js +125 -185
  10. package/build/core/NodeGenerator.d.ts +3 -1
  11. package/build/core/NodeGenerator.d.ts.map +1 -1
  12. package/build/core/NodeGenerator.js +139 -43
  13. package/build/core/NodeManagerRuntime.d.ts +44 -0
  14. package/build/core/NodeManagerRuntime.d.ts.map +1 -0
  15. package/build/core/NodeManagerRuntime.js +140 -0
  16. package/build/core/Utils.d.ts.map +1 -1
  17. package/build/core/logger/service/LoggerService.d.ts +8 -6
  18. package/build/core/logger/service/LoggerService.d.ts.map +1 -1
  19. package/build/core/logger/service/LoggerService.js +101 -23
  20. package/build/core/logger/template/LoggerTemplate.d.ts +1 -2
  21. package/build/core/logger/template/LoggerTemplate.d.ts.map +1 -1
  22. package/build/core/logger/template/LoggerTemplate.js +18 -5
  23. package/build/core/metrics/MetricsDecorator.d.ts +1 -2
  24. package/build/core/metrics/MetricsDecorator.d.ts.map +1 -1
  25. package/build/core/metrics/MetricsDecorator.js +89 -116
  26. package/build/core/metrics/node/CounterMetricConfigNode.d.ts.map +1 -1
  27. package/build/core/metrics/node/CounterMetricConfigNode.js +4 -5
  28. package/build/core/metrics/node/GaugeMetricConfigNode.d.ts.map +1 -1
  29. package/build/core/metrics/node/GaugeMetricConfigNode.js +5 -6
  30. package/build/core/metrics/node/MetricsConfigNode.d.ts +19 -9
  31. package/build/core/metrics/node/MetricsConfigNode.d.ts.map +1 -1
  32. package/build/core/metrics/node/MetricsConfigNode.js +14 -45
  33. package/build/core/metrics/node/TimerMetricConfigNode.d.ts.map +1 -1
  34. package/build/core/metrics/node/TimerMetricConfigNode.js +11 -17
  35. package/build/core/metrics/service/MetricsService.d.ts +110 -114
  36. package/build/core/metrics/service/MetricsService.d.ts.map +1 -1
  37. package/build/core/metrics/service/MetricsService.js +122 -339
  38. package/build/core/metrics/template/CounterMetricTemplate.d.ts +1 -2
  39. package/build/core/metrics/template/CounterMetricTemplate.d.ts.map +1 -1
  40. package/build/core/metrics/template/CounterMetricTemplate.js +16 -5
  41. package/build/core/metrics/template/GaugeMetricTemplate.d.ts +1 -2
  42. package/build/core/metrics/template/GaugeMetricTemplate.d.ts.map +1 -1
  43. package/build/core/metrics/template/GaugeMetricTemplate.js +16 -5
  44. package/build/core/metrics/template/MetricsTemplate.d.ts +1 -2
  45. package/build/core/metrics/template/MetricsTemplate.d.ts.map +1 -1
  46. package/build/core/metrics/template/MetricsTemplate.js +18 -6
  47. package/build/core/metrics/template/TimerMetricTemplate.d.ts +1 -2
  48. package/build/core/metrics/template/TimerMetricTemplate.d.ts.map +1 -1
  49. package/build/core/metrics/template/TimerMetricTemplate.js +16 -5
  50. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts +1 -1
  51. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts.map +1 -1
  52. package/build/core/other/node/DelegatedConfigReferenceNode.js +1 -1
  53. package/build/core/other/service/InputService.d.ts.map +1 -1
  54. package/build/core/other/service/SettingsService.d.ts +1 -2
  55. package/build/core/other/service/SettingsService.d.ts.map +1 -1
  56. package/build/core/other/service/SettingsService.js +19 -5
  57. package/build/core/other/template/BasicTemplate.d.ts +1 -2
  58. package/build/core/other/template/BasicTemplate.d.ts.map +1 -1
  59. package/build/core/other/template/BasicTemplate.js +15 -5
  60. package/build/core/other/template/SettingsTemplate.d.ts +1 -2
  61. package/build/core/other/template/SettingsTemplate.d.ts.map +1 -1
  62. package/build/core/other/template/SettingsTemplate.js +17 -5
  63. package/build/core/state/node/InternalStateConfigNode.d.ts +16 -0
  64. package/build/core/state/node/InternalStateConfigNode.d.ts.map +1 -0
  65. package/build/core/state/node/InternalStateConfigNode.js +89 -0
  66. package/build/core/state/node/StateConfigNode.d.ts +24 -0
  67. package/build/core/state/node/StateConfigNode.d.ts.map +1 -0
  68. package/build/core/state/node/StateConfigNode.js +22 -0
  69. package/build/core/state/service/StateService.d.ts +34 -0
  70. package/build/core/state/service/StateService.d.ts.map +1 -0
  71. package/build/core/state/service/StateService.js +201 -0
  72. package/build/core/state/template/StateTemplate.d.ts +7 -0
  73. package/build/core/state/template/StateTemplate.d.ts.map +1 -0
  74. package/build/core/state/template/StateTemplate.js +25 -0
  75. package/build/core/tagging/ServiceDescriptionDecorator.d.ts +10 -0
  76. package/build/core/tagging/ServiceDescriptionDecorator.d.ts.map +1 -0
  77. package/build/core/tagging/ServiceDescriptionDecorator.js +10 -0
  78. package/build/core/tagging/TemplateDescriptionDecorator.d.ts +8 -0
  79. package/build/core/tagging/TemplateDescriptionDecorator.d.ts.map +1 -0
  80. package/build/core/tagging/TemplateDescriptionDecorator.js +10 -0
  81. package/build/core/tagging/service/NodeTypeService.d.ts +1 -2
  82. package/build/core/tagging/service/NodeTypeService.d.ts.map +1 -1
  83. package/build/core/tagging/service/NodeTypeService.js +19 -5
  84. package/build/core/ui/template/ScriptEditorTemplate.d.ts +1 -2
  85. package/build/core/ui/template/ScriptEditorTemplate.d.ts.map +1 -1
  86. package/build/core/ui/template/ScriptEditorTemplate.js +15 -5
  87. package/build/core/ui/template/UIHelperTemplate.d.ts +1 -2
  88. package/build/core/ui/template/UIHelperTemplate.d.ts.map +1 -1
  89. package/build/core/ui/template/UIHelperTemplate.js +15 -5
  90. package/build/core/webhook/node/WebhookServerConfigNode.d.ts.map +1 -1
  91. package/build/core/webhook/service/ReverseProxyTypeService.d.ts.map +1 -1
  92. package/build/core/webhook/service/WebhookServerService.d.ts +3 -4
  93. package/build/core/webhook/service/WebhookServerService.d.ts.map +1 -1
  94. package/build/core/webhook/service/WebhookServerService.js +48 -18
  95. package/build/core/webhook/template/WebhookTemplate.d.ts +1 -2
  96. package/build/core/webhook/template/WebhookTemplate.d.ts.map +1 -1
  97. package/build/core/webhook/template/WebhookTemplate.js +19 -6
  98. package/build/index.d.ts +6 -2
  99. package/build/index.d.ts.map +1 -1
  100. package/build/index.js +7 -2
  101. package/build/runtime/NodeManagerRuntime.js +140 -0
  102. package/documentation/CounterMetricConfigNode.png +0 -0
  103. package/documentation/GaugeMetricConfigNode.png +0 -0
  104. package/documentation/LoggerTemplate.png +0 -0
  105. package/documentation/MetricsTemplate.png +0 -0
  106. package/documentation/StateTemplate.png +0 -0
  107. package/documentation/TimerMetricConfigNode_histogram.png +0 -0
  108. package/documentation/TimerMetricConfigNode_summary.png +0 -0
  109. package/documentation/WebhookServerConfigNode.png +0 -0
  110. package/documentation/WebhookTemplate.png +0 -0
  111. package/package.json +4 -11
  112. package/src/GenerateNodes.ts +10 -40
  113. package/src/core/NodeConstructor.ts +150 -228
  114. package/src/core/NodeGenerator.ts +156 -47
  115. package/src/core/NodeManagerRuntime.ts +179 -0
  116. package/src/core/logger/service/LoggerService.ts +60 -32
  117. package/src/core/logger/template/LoggerTemplate.html +11 -6
  118. package/src/core/logger/template/LoggerTemplate.ts +9 -11
  119. package/src/core/metrics/MetricsDecorator.ts +140 -168
  120. package/src/core/metrics/node/CounterMetricConfigNode.ts +20 -20
  121. package/src/core/metrics/node/GaugeMetricConfigNode.ts +24 -25
  122. package/src/core/metrics/node/MetricsConfigNode.ts +29 -48
  123. package/src/core/metrics/node/TimerMetricConfigNode.ts +50 -59
  124. package/src/core/metrics/service/MetricsService.ts +214 -489
  125. package/src/core/metrics/template/CounterMetricTemplate.html +0 -1
  126. package/src/core/metrics/template/CounterMetricTemplate.ts +8 -11
  127. package/src/core/metrics/template/GaugeMetricTemplate.html +1 -1
  128. package/src/core/metrics/template/GaugeMetricTemplate.ts +8 -11
  129. package/src/core/metrics/template/MetricsTemplate.html +132 -13
  130. package/src/core/metrics/template/MetricsTemplate.ts +9 -10
  131. package/src/core/metrics/template/TimerMetricTemplate.ts +8 -10
  132. package/src/core/other/node/DelegatedConfigReferenceNode.ts +1 -1
  133. package/src/core/other/service/SettingsService.ts +6 -10
  134. package/src/core/other/template/BasicTemplate.html +1 -1
  135. package/src/core/other/template/BasicTemplate.ts +6 -11
  136. package/src/core/other/template/SettingsTemplate.ts +8 -13
  137. package/src/core/state/node/InternalStateConfigNode.html +77 -0
  138. package/src/core/state/node/InternalStateConfigNode.ts +102 -0
  139. package/src/core/state/node/StateConfigNode.ts +29 -0
  140. package/src/core/state/service/StateService.ts +180 -0
  141. package/src/core/state/template/StateTemplate.html +244 -0
  142. package/src/core/state/template/StateTemplate.ts +18 -0
  143. package/src/core/tagging/ServiceDescriptionDecorator.ts +23 -0
  144. package/src/core/tagging/TemplateDescriptionDecorator.ts +19 -0
  145. package/src/core/tagging/service/NodeTypeService.ts +6 -11
  146. package/src/core/ui/template/ScriptEditorTemplate.ts +6 -10
  147. package/src/core/ui/template/UIHelperTemplate.html +41 -26
  148. package/src/core/ui/template/UIHelperTemplate.ts +6 -10
  149. package/src/core/webhook/service/WebhookServerService.ts +39 -23
  150. package/src/core/webhook/template/WebhookTemplate.html +28 -35
  151. package/src/core/webhook/template/WebhookTemplate.ts +10 -12
  152. package/src/index.ts +8 -2
  153. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts +0 -20
  154. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts.map +0 -1
  155. package/build/core/logger/node/ConsoleLoggerConfigNode.js +0 -62
  156. package/build/core/logger/node/RestLoggerConfigNode.d.ts +0 -40
  157. package/build/core/logger/node/RestLoggerConfigNode.d.ts.map +0 -1
  158. package/build/core/logger/node/RestLoggerConfigNode.js +0 -128
  159. package/documentation/ConsoleLoggerConfigNode.png +0 -0
  160. package/documentation/MetricsConfigNode.png +0 -0
  161. package/documentation/RestLoggerConfigNode.png +0 -0
  162. package/documentation/TimerMetricConfigNode.png +0 -0
  163. package/documentation/WebhookNode.png +0 -0
  164. package/src/core/logger/node/ConsoleLoggerConfigNode.html +0 -90
  165. package/src/core/logger/node/ConsoleLoggerConfigNode.ts +0 -71
  166. package/src/core/logger/node/RestLoggerConfigNode.html +0 -183
  167. package/src/core/logger/node/RestLoggerConfigNode.ts +0 -170
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs';
2
+ import * as path from 'path';
2
3
 
3
- import { buildNode, Template, NodeDescriptor, BaseService, ServiceDescriptor, BaseNode, DependencyType, TemplateDescriptor } from "./NodeConstructor"
4
+ import { buildNode, buildNodeBuilder, Template, NodeDescriptor, BaseService, ServiceDescriptor, BaseNode, DependencyType, TemplateDescriptor } from "./NodeConstructor"
4
5
 
5
6
  const templatePropertyKeys = ["onIncludeOnce", "onIncludeDefaults", "onIncludeEditPrepare", "onIncludeEditPrepare", "onIncludeEditSave", "onIncludeEditForm"];
6
7
 
@@ -57,12 +58,10 @@ class NodeGenerator {
57
58
 
58
59
  public registerTemplate(template: ITemplateClass): NodeGenerator {
59
60
  try{
60
- // verify that we dont already have this template.
61
61
  let descriptor = template.getTemplateDescriptor();
62
62
  if(!this._templates.map(t => t.name()).includes(descriptor.name())){
63
63
  this._templates.push(descriptor);
64
- // FIXME: skip recursive dependency resolution for now.
65
- // descriptor.dependencies().forEach(dependency => this.addDependency(dependency));
64
+ descriptor.dependencies().forEach(dependency => this.addDependency(dependency));
66
65
  }
67
66
  }
68
67
  catch(error){
@@ -74,14 +73,14 @@ class NodeGenerator {
74
73
 
75
74
  public registerNode(node:INodeClass):NodeGenerator{
76
75
  try{
77
- // verify that we have dont already have this node.
78
76
  let descriptor = node.getNodeDescriptor();
79
77
  if(!this._nodes[descriptor.id()]){
80
78
  this._nodes[descriptor.id()] = {nodeClass:node, descriptor:descriptor};
81
- // skip the recursive dependencies for now.
82
- //descriptor.dependencies().forEach(dependency => this.addDependency(dependency));
79
+ // resolve templates declared in @NodeDescription
80
+ descriptor.templates().forEach(t => this.registerTemplate(t.template));
81
+ // resolve explicit dependencies (services, nodes, templates)
82
+ descriptor.dependencies().forEach(dependency => this.addDependency(dependency));
83
83
  }
84
-
85
84
  }
86
85
  catch(error){
87
86
  console.log("ERROR:", error);
@@ -89,13 +88,12 @@ class NodeGenerator {
89
88
  return this;
90
89
  }
91
90
 
92
- public registerService(service: IServiceClass):NodeGenerator {
91
+ public registerService(service: IServiceClass):NodeGenerator {
93
92
  try{
94
93
  let descriptor = service.getServiceDescriptor();
95
94
  if(!this._services.map(s => s.name()).includes(descriptor.name())){
96
95
  this._services.push(descriptor);
97
- // FIXME: skip recursive dependency resolution for now.
98
- //descriptor.dependencies().forEach(dependency => this.addDependency(dependency));
96
+ descriptor.dependencies().forEach(dependency => this.addDependency(dependency));
99
97
  }
100
98
  }
101
99
  catch(error){
@@ -104,33 +102,22 @@ class NodeGenerator {
104
102
  return this;
105
103
  }
106
104
 
107
- public generate(nodesOutputFile:string, pluginsOutputFile:string){
108
- // STEP 1. Compile the templates.
109
- // let templateInclude = this._templates
110
- // .map(template => new (template.clazz()))
111
- // .map(template => {
112
- // return template.onIncludeOnce();
113
- // })
114
- // .join("\n");
115
- let templateInclude = ""
116
-
117
- // STEP 2. compose the HTMLStructure of each of the nodes.
118
- let nodeHTMLOutut = Object.values(this._nodes)
119
- .map(node => {
120
- console.log(`Processing node: ${node.descriptor.id()}`);
121
-
122
- return buildNode(node.nodeClass);
123
- })
124
- .join("\n")
105
+ public generate(nodesOutputFile:string, pluginsOutputFile:string, packageName:string){
106
+ // Partition nodes into owned (belongs to this package) vs shared (from dependencies).
107
+ // Owned nodes get standard registerType + data-template-name in Nodes.html so that
108
+ // Node-RED's registry maps them to this module (enabling palette icons, flow library
109
+ // detection, etc.). Shared nodes keep the deferred pattern in Plugins.html to avoid
110
+ // HTML-scanning conflicts when multiple plugins bundle the same infrastructure nodes.
111
+ const allNodes = Object.values(this._nodes);
112
+ const ownedNodes = allNodes.filter(node => node.descriptor.package() === packageName);
113
+ const sharedNodes = allNodes.filter(node => node.descriptor.package() !== packageName);
125
114
 
126
- fs.writeFileSync(nodesOutputFile + ".html",`
127
- <!--
128
- ${NodeGenerator.warning}
129
- -->
130
- ${templateInclude}
131
- ${nodeHTMLOutut}
115
+ console.log(`\nPackage: ${packageName}`);
116
+ console.log(` Owned nodes (→ Nodes.html): ${ownedNodes.map(n => n.descriptor.id()).join(', ') || '(none)'}`);
117
+ console.log(` Shared nodes (→ Plugins.html): ${sharedNodes.map(n => n.descriptor.id()).join(', ') || '(none)'}\n`);
132
118
 
133
- `);
119
+ // STEP 1. Generate Nodes.html with standard client-side definitions for owned nodes.
120
+ this.generateNodesHtml(nodesOutputFile + ".html", ownedNodes);
134
121
 
135
122
  // Step 3. compose the JS structure for each of the nodes.
136
123
  // 1. get a list of the files to import.
@@ -153,15 +140,37 @@ ${nodeHTMLOutut}
153
140
  ${NodeGenerator.warning}
154
141
  */
155
142
  "use strict";
156
- const NodeManager = require("@theotherwillembotha/node-red-plugincore").NodeManager;
143
+ const { NodeManager } = require("./runtime/NodeManagerRuntime");
157
144
  ${importList}
158
145
  module.exports = (RED) => {
159
146
  let manager = new NodeManager(RED);
160
147
  ${moduleExports}
161
- }
162
-
148
+ }
149
+
163
150
  `);
164
151
 
152
+ // Copy NodeManagerRuntime.js into the plugin's build/runtime/ folder so that
153
+ // the generated Nodes.js require("./runtime/NodeManagerRuntime") resolves locally
154
+ // with no dependency on plugincore being installed in the user's environment.
155
+ const outputDir = path.dirname(nodesOutputFile);
156
+ const runtimeDestDir = path.join(outputDir, 'runtime');
157
+ const runtimeDestPath = path.join(runtimeDestDir, 'NodeManagerRuntime.js');
158
+
159
+ let runtimeSrcPath: string;
160
+ try {
161
+ // Resolves when this is a consumer plugin - plugincore is in node_modules.
162
+ runtimeSrcPath = require.resolve('@theotherwillembotha/node-red-plugincore/build/runtime/NodeManagerRuntime');
163
+ } catch {
164
+ // We ARE plugincore - NodeManagerRuntime.js is compiled alongside us in build/core/.
165
+ runtimeSrcPath = path.join(__dirname, 'NodeManagerRuntime.js');
166
+ }
167
+
168
+ if (path.resolve(runtimeSrcPath) !== path.resolve(runtimeDestPath)) {
169
+ fs.mkdirSync(runtimeDestDir, { recursive: true });
170
+ fs.copyFileSync(runtimeSrcPath, runtimeDestPath);
171
+ }
172
+ console.log(`NodeManagerRuntime.js → ${runtimeDestPath}`);
173
+
165
174
  console.log(`Done building ${Object.keys(this._nodes).length} nodes\n${Object.values(this._nodes).map(node => " - " + node.descriptor.id() + "\n").join("")}\n`);
166
175
 
167
176
  let serviceExports = this._services.map(service => {
@@ -196,29 +205,38 @@ module.exports = function (RED) {
196
205
 
197
206
  if(addedPlugin && plugin.instantiate && !plugin.instance){
198
207
  plugin.instance = new plugin.class();
199
-
208
+
200
209
  // instantiate the plugin.
201
210
  await plugin.instance.init(RED);
202
211
 
212
+ // Eagerly call onDeploy with any pre-existing saved flows so that
213
+ // service registries (MetricsService, WebhookServerService, etc.) are
214
+ // populated before config nodes are constructed during flow startup.
215
+ // Without this, MetricsConfigNode._metrics is undefined on first boot.
216
+ const existingFlows = await runtime.flows.getFlows({});
217
+ const hasExistingFlows = existingFlows && existingFlows.flows && existingFlows.flows.length > 0;
218
+ if (hasExistingFlows) {
219
+ await plugin.instance.onDeploy(existingFlows);
220
+ }
221
+
203
222
  // publish an event that it has ben deployed.
204
223
  RED.events.emit("plugin.instantiated", pluginID);
205
-
224
+
206
225
  pluginList.splice(pluginList.findIndex(current => current.id() === addedPlugin.id()), 1);
207
226
 
208
227
  // register a flow deployment listener.
209
- let startupDeployment = true;
228
+ // startupDeployment is only needed for a truly fresh Node-RED with no saved flows.
229
+ let startupDeployment = !hasExistingFlows;
210
230
  RED.events.on('runtime-event', async (event) => {
211
- // startup deployment.
231
+ // startup deployment (fresh Node-RED with no pre-existing flows).
212
232
  if ("runtime-deploy" === event?.id && startupDeployment) {
213
- //console.log("STARTUP DEPLOYMENT");
214
233
  startupDeployment = false;
215
234
  const flows = await runtime.flows.getFlows({});
216
235
  await plugin.instance.onDeploy(flows);
217
236
  }
218
-
237
+
219
238
  // stopping flows on redeployment.
220
239
  if ("runtime-state" === event?.id && event?.payload?.state === "stop" && event?.payload?.deploy) {
221
- //console.log("REDEPLOY!");
222
240
  const flows = await runtime.flows.getFlows({});
223
241
  await plugin.instance.onDeploy(flows);
224
242
  }
@@ -239,6 +257,97 @@ module.exports = function (RED) {
239
257
  `
240
258
  fs.writeFileSync(pluginsOutputFile + ".js", output);
241
259
  console.log(`Done building ${this._services.length} services\n${this._services.map(service => " - " + service.name() + "\n").join("")}\n`);
260
+
261
+ // STEP 4. Generate Plugins.html with deferred client-side definitions for shared nodes.
262
+ this.generatePluginsHtml(pluginsOutputFile + ".html", sharedNodes);
263
+ }
264
+
265
+ private generateNodesHtml(filePath: string, nodes: {nodeClass: INodeClass, descriptor: NodeDescriptor}[]): void {
266
+ if (nodes.length === 0) {
267
+ fs.writeFileSync(filePath, `
268
+ <!--
269
+ ${NodeGenerator.warning}
270
+ No owned nodes in this package. Shared nodes are registered via Plugins.html.
271
+ -->
272
+ `);
273
+ console.log(`Nodes.html: no owned nodes\n`);
274
+ return;
275
+ }
276
+
277
+ const seenOnce = new Set<string>();
278
+ const onceHtmlParts: string[] = [];
279
+ const nodeParts: string[] = [];
280
+
281
+ nodes.forEach(node => {
282
+ console.log(`Building Nodes.html for: ${node.descriptor.id()}`);
283
+ const nb = buildNodeBuilder(node.nodeClass);
284
+
285
+ nb.buildOnceHtml().forEach(({source, html}) => {
286
+ if (!seenOnce.has(source)) {
287
+ seenOnce.add(source);
288
+ onceHtmlParts.push(html);
289
+ }
290
+ });
291
+
292
+ nodeParts.push([
293
+ nb.buildStandardType(),
294
+ nb.buildHtml(),
295
+ nb.buildDocumentation()
296
+ ].join('\n\n'));
297
+ });
298
+
299
+ fs.writeFileSync(filePath, `
300
+ <!--
301
+ ${NodeGenerator.warning}
302
+ -->
303
+ ${onceHtmlParts.join('\n')}
304
+ ${nodeParts.join('\n\n')}
305
+ `);
306
+ console.log(`Done building Nodes.html (${nodes.length} owned nodes)\n`);
307
+ }
308
+
309
+ private generatePluginsHtml(filePath: string, nodes: {nodeClass: INodeClass, descriptor: NodeDescriptor}[]): void {
310
+ if (nodes.length === 0) {
311
+ fs.writeFileSync(filePath, `
312
+ <!--
313
+ ${NodeGenerator.warning}
314
+ No shared nodes in this package. All nodes are registered via Nodes.html.
315
+ -->
316
+ `);
317
+ console.log(`Plugins.html: no shared nodes\n`);
318
+ return;
319
+ }
320
+
321
+ const seenOnce = new Set<string>();
322
+ const onceHtmlParts: string[] = [];
323
+ const nodePluginParts: string[] = [];
324
+
325
+ nodes.forEach(node => {
326
+ console.log(`Building Plugins.html for: ${node.descriptor.id()}`);
327
+ const nb = buildNodeBuilder(node.nodeClass);
328
+
329
+ nb.buildOnceHtml().forEach(({source, html}) => {
330
+ if (!seenOnce.has(source)) {
331
+ seenOnce.add(source);
332
+ onceHtmlParts.push(html);
333
+ }
334
+ });
335
+
336
+ nodePluginParts.push([
337
+ nb.buildDeferredType(),
338
+ nb.buildHtml(),
339
+ nb.buildDocumentation()
340
+ ].join('\n\n'));
341
+ });
342
+
343
+ fs.writeFileSync(filePath, `
344
+ <!--
345
+ ${NodeGenerator.warning}
346
+ -->
347
+ ${onceHtmlParts.join('\n')}
348
+ ${nodePluginParts.join('\n\n')}
349
+ `);
350
+ console.log(`Done building Plugins.html (${nodes.length} shared nodes)\n`);
242
351
  }
243
352
 
244
353
  private static generateHeader(node:NodeDescriptor){
@@ -265,7 +374,7 @@ class TypeUtility {
265
374
  private constructor(){}
266
375
 
267
376
  private static getPrototypes(t:INamedType):string[] {
268
- if(!t.name){
377
+ if(!t || !t.name){
269
378
  return [];
270
379
  }
271
380
  else{
@@ -0,0 +1,179 @@
1
+ /**
2
+ * NodeManagerRuntime.ts
3
+ *
4
+ * STANDALONE - zero imports from the rest of plugincore.
5
+ *
6
+ * This file is compiled and copied into every plugin's build output so that
7
+ * plugins are self-contained and do not require plugincore to be separately
8
+ * installed in the user's Node-RED environment.
9
+ *
10
+ * When multiple plugins are deployed, each ships its own copy of this file.
11
+ * The first plugin to load registers its NodeManager and nodes; subsequent
12
+ * copies operate independently on their own node types with no conflict.
13
+ *
14
+ * POST_CONSTRUCT_KEY is intentionally a plain string (not a Symbol) so that
15
+ * the decorators (from plugincore's NodeConstructor, loaded via the plugin's
16
+ * dependency on plugincore) and this runtime file (a separate module instance)
17
+ * resolve to the same property key on node instances.
18
+ */
19
+
20
+ import type { NodeAPI, NodeAPISettingsWithData } from "node-red";
21
+
22
+ // ─── API version ────────────────────────────────────────────────────────────
23
+ // Semver of the NodeManager API contract - independent of plugincore's package
24
+ // version. Bump when the contract changes; plugins can read this to warn users
25
+ // that they were built against a different NodeManager version.
26
+ export const NODEMANAGER_API_VERSION = "1.0.0";
27
+
28
+ // ─── Shared key for post-construct initializer lists ────────────────────────
29
+ // Must be a plain string so it resolves identically across module instances.
30
+ export const POST_CONSTRUCT_KEY = '__plugincore_postConstructInitializers__';
31
+
32
+ // ─── Minimal types ───────────────────────────────────────────────────────────
33
+
34
+ export interface BaseNodeConfig {
35
+ id: string;
36
+ name: string;
37
+ type: string;
38
+ }
39
+
40
+ interface INodeDescriptor {
41
+ id(): string;
42
+ tags(): string[];
43
+ }
44
+
45
+ interface INodeClass {
46
+ new(node: any, config: any): any;
47
+ getNodeDescriptor(): INodeDescriptor;
48
+ }
49
+
50
+ // ─── Post-construct initializer runner ───────────────────────────────────────
51
+
52
+ export function runPostConstructInitializers(instance: any): void {
53
+ const initializers: Function[] = instance[POST_CONSTRUCT_KEY];
54
+ if (!initializers || initializers.length === 0) return;
55
+
56
+ const className = instance.constructor.name;
57
+ initializers.forEach((init, index) => {
58
+ try {
59
+ init(instance);
60
+ } catch (error) {
61
+ console.error(
62
+ `Post-construct initialization failed:\n` +
63
+ ` Class: ${className}\n` +
64
+ ` Initializer: ${index + 1}/${initializers.length}\n` +
65
+ ` Error: ${error instanceof Error ? error.message : String(error)}`
66
+ );
67
+ throw error;
68
+ }
69
+ });
70
+
71
+ delete instance[POST_CONSTRUCT_KEY];
72
+ }
73
+
74
+ // ─── NodeManager ─────────────────────────────────────────────────────────────
75
+ // RED is stored on `global` rather than as a static class field so that all
76
+ // module instances of NodeManagerRuntime (the local copy bundled into Nodes.js
77
+ // and the copy bundled via plugincore's NodeConstructor) share the same value.
78
+ // Without this, esbuild's two separate inline copies would have independent
79
+ // static fields and the one used by decorators would never see RED being set.
80
+ const _GLOBAL_RED_KEY = '__plugincore_NodeManager_RED__';
81
+
82
+ // ─── Global node-type registration guard ─────────────────────────────────────
83
+ // When multiple self-contained plugins are installed they each bundle plugincore
84
+ // inline and each try to call RED.nodes.registerType for the same shared
85
+ // infrastructure node types (ConsoleLoggerConfigNode, RestLoggerConfigNode, etc.).
86
+ // Node-RED rejects duplicate registrations with a warning and the second
87
+ // plugin's Nodes.js fails to load. This global set tracks which types have
88
+ // already been registered so subsequent plugins silently skip them.
89
+ const _GLOBAL_REGISTERED_NODES_KEY = '__plugincore_registered_nodes__';
90
+
91
+ function getRegisteredNodes(): Set<string> {
92
+ if (!(global as any)[_GLOBAL_REGISTERED_NODES_KEY]) {
93
+ (global as any)[_GLOBAL_REGISTERED_NODES_KEY] = new Set<string>();
94
+ }
95
+ return (global as any)[_GLOBAL_REGISTERED_NODES_KEY];
96
+ }
97
+
98
+ export class NodeManager {
99
+
100
+ // nodeTypeService is resolved dynamically via RED.plugins - no static import needed.
101
+ private nodeTypeService: any;
102
+ private typeBacklog: INodeClass[] = [];
103
+
104
+ public constructor(RED: NodeAPI<NodeAPISettingsWithData>) {
105
+ (global as any)[_GLOBAL_RED_KEY] = RED;
106
+
107
+ const nodeTypeServiceId = "@theotherwillembotha/nodetypeservice";
108
+
109
+ const nodeTypeServiceListener = (pluginID: string) => {
110
+ if (pluginID === nodeTypeServiceId) {
111
+ RED.events.off('plugin.instantiated', nodeTypeServiceListener);
112
+ this.nodeTypeService = (RED.plugins.get(nodeTypeServiceId) as any).instance;
113
+
114
+ for (const nodeType of this.typeBacklog) {
115
+ for (const tag of nodeType.getNodeDescriptor().tags()) {
116
+ this.nodeTypeService.registerNodeType(tag, nodeType);
117
+ }
118
+ }
119
+ this.typeBacklog = [];
120
+ }
121
+ };
122
+
123
+ this.nodeTypeService = (RED.plugins.get(nodeTypeServiceId) as any)?.instance;
124
+ if (!this.nodeTypeService) {
125
+ RED.events.on('plugin.instantiated', nodeTypeServiceListener);
126
+ }
127
+ }
128
+
129
+ public static get RED(): NodeAPI<NodeAPISettingsWithData> {
130
+ return (global as any)[_GLOBAL_RED_KEY];
131
+ }
132
+
133
+ public registerNodeType(typeName: string, type: INodeClass): NodeManager {
134
+ if (!type) {
135
+ console.error(
136
+ `[NodeManager v${NODEMANAGER_API_VERSION}] Type "${typeName}" is undefined. ` +
137
+ `Ensure it is exported and included in GenerateNodes.`
138
+ );
139
+ return this;
140
+ }
141
+
142
+ const registeredNodes = getRegisteredNodes();
143
+ if (registeredNodes.has(typeName)) {
144
+ // Already registered by another plugin - skip to avoid Node-RED duplicate-registration error.
145
+ return this;
146
+ }
147
+ registeredNodes.add(typeName);
148
+
149
+ const nodeConstructor = function(this: any, config: any) {
150
+ try {
151
+ NodeManager.RED.nodes.createNode(this, config);
152
+ const node = new (type as any)(this, config);
153
+ runPostConstructInitializers(node);
154
+ if (node.onInit) {
155
+ node.onInit();
156
+ }
157
+ } catch (error) {
158
+ console.error(`[NodeManager v${NODEMANAGER_API_VERSION}] Failed to construct node "${typeName}":`, error);
159
+ }
160
+ };
161
+
162
+ try {
163
+ NodeManager.RED.nodes.registerType(typeName, nodeConstructor, { settings: {} });
164
+
165
+ const nodeDescription = type.getNodeDescriptor();
166
+ if (this.nodeTypeService) {
167
+ for (const tag of nodeDescription.tags()) {
168
+ this.nodeTypeService.registerNodeType(tag, type);
169
+ }
170
+ } else {
171
+ this.typeBacklog.push(type);
172
+ }
173
+ } catch (error) {
174
+ console.error(`[NodeManager v${NODEMANAGER_API_VERSION}] Failed to register node type "${typeName}":`, error);
175
+ }
176
+
177
+ return this;
178
+ }
179
+ }
@@ -1,15 +1,35 @@
1
1
 
2
- import {BaseService, ServiceDescriptor, ConfigNodeConfig, ConfigNode } from "../../NodeConstructor";
2
+ import * as os from 'os';
3
+ import {BaseService, ConfigNodeConfig, ConfigNode } from "../../NodeConstructor";
4
+ import { ServiceDescription } from "../../tagging/ServiceDescriptionDecorator";
3
5
  import { NodeAPI, NodeAPISettingsWithData, Node } from "node-red";
4
- import Handlebars from "handlebars";
5
6
  import { LoggerTemplate, LoggerTemplateConfig } from "../template/LoggerTemplate";
6
- var network = require('network');
7
7
 
8
- // helper for serializing json objects inside handlebars tags.
9
- // you can then convert it like this: {{{json myobject}}}
10
- Handlebars.registerHelper('json', function(context) {
11
- return JSON.stringify(context);
12
- });
8
+ // Build-time / optional runtime deps - lazy so they are never required at bundle load time.
9
+ function getHandlebars(): any { return require('handlebars'); }
10
+
11
+ function getLocalIpAddress(): string {
12
+ const interfaces = os.networkInterfaces();
13
+ for (const name of Object.keys(interfaces)) {
14
+ for (const iface of interfaces[name] ?? []) {
15
+ if (iface.family === 'IPv4' && !iface.internal) {
16
+ return iface.address;
17
+ }
18
+ }
19
+ }
20
+ return '127.0.0.1';
21
+ }
22
+
23
+ let _helpersRegistered = false;
24
+ function ensureHelpersRegistered(): void {
25
+ if (_helpersRegistered) return;
26
+ _helpersRegistered = true;
27
+ // helper for serializing json objects inside handlebars tags.
28
+ // you can then convert it like this: {{{json myobject}}}
29
+ getHandlebars().registerHelper('json', function(context: any) {
30
+ return JSON.stringify(context);
31
+ });
32
+ }
13
33
 
14
34
  export enum Level{
15
35
  DEBUG = "DEBUG",
@@ -40,12 +60,13 @@ export type TagMap = {
40
60
 
41
61
  export abstract class Log {
42
62
  private _config: LoggerTemplateConfig;
43
- private template: HandlebarsTemplateDelegate<any>;
63
+ private _templateStr: string;
64
+ private _compiledTemplate: any = null;
44
65
  private tags:TagMap;
45
66
 
46
67
  protected constructor(config:LoggerTemplateConfig){
47
68
  this._config = config;
48
- this.template = Handlebars.compile(config.template);
69
+ this._templateStr = config.template;
49
70
  this.tags = {
50
71
  id: config.id,
51
72
  node: config.name,
@@ -55,6 +76,14 @@ export abstract class Log {
55
76
  }
56
77
  }
57
78
 
79
+ private getTemplate(): any {
80
+ if (!this._compiledTemplate) {
81
+ ensureHelpersRegistered();
82
+ this._compiledTemplate = getHandlebars().compile(this._templateStr);
83
+ }
84
+ return this._compiledTemplate;
85
+ }
86
+
58
87
  protected config():LoggerTemplateConfig{
59
88
  return this._config;
60
89
  }
@@ -62,7 +91,7 @@ export abstract class Log {
62
91
  public log(payload:{[key:string]:any}|string):void{
63
92
  try{
64
93
  // step 1. serialize the payload using the template engine.
65
- let message = (payload instanceof String) ? payload as string : this.template({msg:payload});
94
+ let message = (payload instanceof String) ? payload as string : this.getTemplate()({msg:payload});
66
95
 
67
96
  // step 2. write to the log appender.
68
97
  this.writeToLog(this.config().level, message, this.tags);
@@ -71,7 +100,7 @@ export abstract class Log {
71
100
  console.log(e);
72
101
  }
73
102
  }
74
-
103
+
75
104
  protected abstract writeToLog(level:string, message:string, tags:{[key:string]:string|boolean|number}):void;
76
105
  }
77
106
 
@@ -110,9 +139,22 @@ export abstract class AbstractLogger<BaseLoggerConfig>{
110
139
  protected abstract createLogger(config: LoggerTemplateConfig):Log;
111
140
  }
112
141
 
142
+ @ServiceDescription({
143
+ id: "@theotherwillembotha/loggerservice",
144
+ sourceFile: "@theotherwillembotha/node-red-plugincore",
145
+ dependencies: [LoggerTemplate]
146
+ })
113
147
  export class LoggerService extends BaseService {
114
148
 
115
- public static instanceID:string;
149
+ // Backed by a global so the value is shared across separately-bundled copies
150
+ // of plugincore (Nodes.js and Plugins.js each inline their own copy).
151
+ public static get instanceID(): string {
152
+ return (global as any).__plugincore_instance_id__ ?? '';
153
+ }
154
+ public static set instanceID(value: string) {
155
+ (global as any).__plugincore_instance_id__ = value;
156
+ }
157
+
116
158
  private red!: NodeAPI<NodeAPISettingsWithData>;
117
159
 
118
160
  constructor(){
@@ -121,26 +163,11 @@ export class LoggerService extends BaseService {
121
163
 
122
164
  public init(red: NodeAPI<NodeAPISettingsWithData>): void | Promise<void> {
123
165
  this.red = red;
124
-
125
- return new Promise<void>((resolve) => {
126
- network.get_active_interface((err:any, obj:any) => {
127
- LoggerService.instanceID = obj.ip_address;
128
- });
129
- });
166
+ LoggerService.instanceID = getLocalIpAddress();
130
167
  }
131
168
 
132
169
  public deinit(red: NodeAPI<NodeAPISettingsWithData>): void | Promise<void> {}
133
170
 
134
- static override getServiceDescriptor():ServiceDescriptor {
135
- return new ServiceDescriptor(
136
- "@theotherwillembotha/loggerservice",
137
- "LoggerService",
138
- "services-plugin",
139
- "@theotherwillembotha/node-red-plugincore",
140
- LoggerService,
141
- [LoggerTemplate]
142
- );
143
- }
144
171
  }
145
172
 
146
173
  export type LoggerConfigNodeConfig = ConfigNodeConfig & BaseLoggerConfig & {}
@@ -160,10 +187,11 @@ export abstract class LoggerConfigNode<CNC extends LoggerConfigNodeConfig, Logge
160
187
 
161
188
  export class DoNothingAppender extends Log{
162
189
 
163
- private static instance:DoNothingAppender = new DoNothingAppender();
190
+ private static _instance: DoNothingAppender | null = null;
164
191
 
165
- static get(): any {
166
- return DoNothingAppender.instance;
192
+ static get(): DoNothingAppender {
193
+ if (!DoNothingAppender._instance) DoNothingAppender._instance = new DoNothingAppender();
194
+ return DoNothingAppender._instance;
167
195
  }
168
196
 
169
197
  private constructor(){
@@ -108,13 +108,13 @@
108
108
  </script>
109
109
 
110
110
  <script type="text/javascript" template-section="onIncludeEditPrepare">
111
- let node = this;
112
-
113
111
  // controls.
114
112
  let logEnabled = $("#{{defaults.logEnabled}}");
115
113
  let logTemplateEnabled = $("#{{defaults.logTemplateOverrideEnabled}}");
116
114
  let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
117
115
  let logEnabledSection = $("#section_logEnabled");
116
+ let loggerSection = $("#section_loggerconfig");
117
+ loggerSection.hide();
118
118
 
119
119
  // functions.
120
120
  let getLoggerTypes = async function(){
@@ -262,10 +262,15 @@
262
262
  }
263
263
  });
264
264
 
265
- // lastly, make sure that we set the correct value on the logger selector.
266
- updateLoggerSelectorList().then(() => {
267
- loggerSelector.val(node.logger);
268
- loggerSelector.trigger('change');
265
+ // Show the logger section only if at least one logger type is registered.
266
+ // If nodered_logging (or any other logger provider) is not installed, the section stays hidden.
267
+ getLoggerTypes().then(loggerTypes => {
268
+ if (loggerTypes.length === 0) return;
269
+ loggerSection.show();
270
+ updateLoggerSelectorList().then(() => {
271
+ loggerSelector.val(node.logger);
272
+ loggerSelector.trigger('change');
273
+ });
269
274
  });
270
275
 
271
276
  </script>