@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
@@ -1,23 +1,25 @@
1
- import { PluginDefinition } from "@node-red/registry";
2
- import { Node, NodeAPI, NodeDef, NodeAPISettingsWithData} from "node-red";
1
+ import { Node, NodeAPI, NodeAPISettingsWithData} from "node-red";
3
2
  import { INamedType, INodeClass, ITemplateClass } from "./NodeGenerator";
4
3
 
5
4
  import "reflect-metadata";
6
- import jsdom from "jsdom";
7
5
  import fs from 'fs';
8
- import beautify from 'js-beautify';
9
- import markdownit from 'markdown-it'
10
- import Handlebars, { template } from "handlebars";
11
- import { NodeTypeService } from "./tagging/service/NodeTypeService";
12
- import { NodeDescriptionConfig } from "./tagging/NodeDescriptionDecorator";
13
6
 
7
+ import {
8
+ NODEMANAGER_API_VERSION,
9
+ POST_CONSTRUCT_KEY,
10
+ BaseNodeConfig,
11
+ runPostConstructInitializers,
12
+ NodeManager
13
+ } from "./NodeManagerRuntime";
14
14
 
15
-
16
- export interface BaseNodeConfig {
17
- id:string;
18
- name:string;
19
- type:string;
20
- }
15
+ // Re-export so existing downstream code keeps working unchanged.
16
+ export {
17
+ NODEMANAGER_API_VERSION,
18
+ POST_CONSTRUCT_KEY,
19
+ BaseNodeConfig,
20
+ runPostConstructInitializers,
21
+ NodeManager
22
+ };
21
23
 
22
24
  export abstract class BaseNode<Config extends BaseNodeConfig> {
23
25
 
@@ -199,8 +201,6 @@ export abstract class Template{
199
201
 
200
202
  type VoidMethod = (...args: any[]) => void | Promise<void>;
201
203
 
202
- const POST_CONSTRUCT_KEY = Symbol('postConstructInitializers');
203
-
204
204
  // Builder class for post-construct decorators
205
205
  class PostConstructDecoratorBuilder<TMetadata = any> {
206
206
  private decoratorName: string;
@@ -342,150 +342,6 @@ export function createPostConstructDecorator<TMetadata = any>(decoratorName: str
342
342
  return new PostConstructDecoratorBuilder<TMetadata>(decoratorName);
343
343
  }
344
344
 
345
- // Single function to run all post-construct initializers
346
- let runPostConstructInitializers = function(instance: any) {
347
- const initializers = instance[POST_CONSTRUCT_KEY];
348
- if (initializers && initializers.length > 0) {
349
- const className = instance.constructor.name;
350
- //console.log(`Running ${initializers.length} post-construct initializers for ${className}`);
351
-
352
- initializers.forEach((init: Function, index: number) => {
353
- try {
354
- init(instance);
355
- } catch (error) {
356
- console.error(
357
- `Post-construct initialization failed:\n` +
358
- ` Class: ${className}\n` +
359
- ` Initializer: ${index + 1}/${initializers.length}\n` +
360
- ` Error: ${error instanceof Error ? error.message : String(error)}`
361
- );
362
- throw error;
363
- }
364
- });
365
-
366
- // Clear the initializers after running
367
- delete instance[POST_CONSTRUCT_KEY];
368
- }
369
- }
370
-
371
- // ************************************************
372
-
373
- export class NodeManager{
374
- private static PLUGINID = "node-red-contrib-theotherwillembotha/services-plugin";
375
- private static _RED: NodeAPI<NodeAPISettingsWithData>;
376
-
377
- private nodeTypeService:NodeTypeService;
378
- private typeBacklog:INodeClass[] = []
379
-
380
- public constructor(RED : NodeAPI<NodeAPISettingsWithData>){
381
- NodeManager._RED = RED;
382
-
383
- let nodeTypeServiceListener = async (pluginID:String) => {
384
- if(pluginID === "@theotherwillembotha/nodetypeservice"){
385
- // disable the listener- the plugin has been installed.
386
- RED.events.off('plugin.instantiated', nodeTypeServiceListener)
387
-
388
- // get a handle on the NodeTypeService
389
- let plugin = RED.plugins.get("@theotherwillembotha/nodetypeservice") as any;
390
- this.nodeTypeService = (RED.plugins.get("@theotherwillembotha/nodetypeservice") as any).instance as NodeTypeService;
391
-
392
- // process the backlog.
393
- for(let nodeType of this.typeBacklog){
394
- for(let tag of nodeType.getNodeDescriptor().tags()){
395
- this.nodeTypeService.registerNodeType(tag, nodeType);
396
- }
397
- }
398
- }
399
- }
400
-
401
- // check if the reverserproxyttypesewrvice is installed yet.
402
- this.nodeTypeService = (RED.plugins.get("@theotherwillembotha/nodetypeservice") as any).instance as NodeTypeService;
403
- if(this.nodeTypeService) {
404
- console.log("NodeTypeService installed!")
405
- }
406
- else {
407
- // Register a listener to trigger when the RegerseProxyTypService gets installed.
408
- console.log("nodetypeservice not installed yet. waiting for it to be installed");
409
- RED.events.on('plugin.instantiated', nodeTypeServiceListener);
410
- }
411
- }
412
-
413
- public static get RED() {
414
- return NodeManager._RED;
415
- }
416
-
417
- public registerService(type:IServiceClass):NodeManager{
418
- let servicePlugin:any = NodeManager.RED.plugins.get(NodeManager.PLUGINID) as PluginDefinition<any>;
419
- if(servicePlugin){
420
- servicePlugin.instance.install(NodeManager.RED, type);
421
- }
422
- else{
423
- let serviceName = `${type.getServiceDescriptor().sourceFile()}.${type.getServiceDescriptor().name()}`;
424
- console.log(`ERROR: plugin ${NodeManager.PLUGINID} does not seem to be loaded. Cannot start service: ${serviceName}`);
425
- }
426
- return this;
427
- }
428
-
429
- public registerNodeType(typeName: string, type: INodeClass):NodeManager{
430
- if(!type){
431
- console.error(`Type "${typeName}" has no associated type. Make sure that it was exported and included in the index.js file!`)
432
- }
433
-
434
- let nodeConstructor = function(this: any, config: BaseNodeConfig | NodeDef){
435
- //console.log("constructing node", typeName);
436
-
437
- try{
438
- // instantiate the node.
439
- NodeManager.RED.nodes.createNode(this, config as any as NodeDef);
440
- // assign an instance of it to the config.
441
- //(config as any).__node = this;
442
-
443
- // instatiate the actual node.
444
- let node = new (type as any)(this, config);
445
- //this.__node = node;
446
-
447
- // run the constructor initializers.
448
- runPostConstructInitializers(node);
449
-
450
-
451
- // after the node has been instantiated, attempt to invole the onInit method.
452
- if(node.onInit){
453
- node.onInit();
454
- }
455
- }
456
- catch(error){
457
- console.log(error);
458
- console.log("typeName:", typeName)
459
- console.log("type:", type)
460
- console.log("config:", config)
461
- }
462
- }
463
-
464
- try{
465
- console.log("registering node " + typeName);
466
- NodeManager.RED.nodes.registerType(typeName, nodeConstructor, { settings: {}});
467
-
468
- let nodeDescription = type.getNodeDescriptor();
469
-
470
- // registger the node tags.
471
- if(this.nodeTypeService){
472
- for(let tag of nodeDescription.tags()){
473
- this.nodeTypeService.registerNodeType(tag, type);
474
- }
475
- }
476
- else{
477
- this.typeBacklog.push(type);
478
- }
479
-
480
- console.log("registered node " + typeName);
481
- }
482
- catch(error){
483
- console.error("registering node: " + typeName + " failed", error);
484
- }
485
- return this;
486
- }
487
- }
488
-
489
345
  export type FlowDeployment = {
490
346
  rev:string,
491
347
  flows:[FlowElement]
@@ -521,25 +377,27 @@ export abstract class BaseService {
521
377
  }
522
378
  }
523
379
 
524
- const { JSDOM } = jsdom;
525
- const md = markdownit({
526
- html: true,
527
- linkify: true,
528
- typographer: true
529
- })
530
-
531
- // Custom renderer for headings
532
- md.renderer.rules.heading_open = function (tokens, idx, options, env, self) {
533
- const token = tokens[idx];
534
- const level = token.tag.substring(1); // 'h1', 'h2', etc. -> '1', '2'
535
- return `<h${level} class="custom-heading">`;
536
- };
537
-
538
- md.renderer.rules.heading_close = function (tokens, idx, options, env, self) {
539
- const token = tokens[idx];
540
- const level = token.tag.substring(1);
541
- return `</h${level}>`;
542
- };
380
+ // Build-time deps - lazy loaded so they are never required at runtime in production bundles.
381
+ // These are only invoked during `npm run build` (GenerateNodes), never in the deployed container.
382
+ function getJSDOM(): any { return require('jsdom').JSDOM; }
383
+ function getBeautify(): any { return require('js-beautify'); }
384
+ function getHandlebars(): any { return require('handlebars'); }
385
+
386
+ let _md: any = null;
387
+ function getMd(): any {
388
+ if (!_md) {
389
+ _md = require('markdown-it')({ html: true, linkify: true, typographer: true });
390
+ _md.renderer.rules.heading_open = (tokens: any[], idx: number) => {
391
+ const level = tokens[idx].tag.substring(1);
392
+ return `<h${level} class="custom-heading">`;
393
+ };
394
+ _md.renderer.rules.heading_close = (tokens: any[], idx: number) => {
395
+ const level = tokens[idx].tag.substring(1);
396
+ return `</h${level}>`;
397
+ };
398
+ }
399
+ return _md;
400
+ }
543
401
 
544
402
  let beautifyOptions = {
545
403
  "indent_size": 4,
@@ -569,6 +427,7 @@ export type DefaultTemplateType = {
569
427
  minInstances?:number;
570
428
  maxInstances?:number;
571
429
  validate?:Function;
430
+ list?:boolean;
572
431
  }
573
432
 
574
433
  export class NodeBuilder {
@@ -598,12 +457,12 @@ export class NodeBuilder {
598
457
  }
599
458
 
600
459
  public addDefault(name:string, template:DefaultTemplateType):NodeBuilder{
601
- // if the template type ends with squeare brackets, its an array type.
602
- if(template.type && template.type.endsWith("[]")) {
460
+ // if list:true, this is an array of config references - generate shadow defaults for each slot.
461
+ if(template.list === true) {
603
462
  template.maxInstances = template.maxInstances ? template.maxInstances : 10;
604
- this._defaults[name] = {type:template.type, value:"", required:true, maxInstances:template.maxInstances};
463
+ this._defaults[name] = {value:"", required:(template.required) ? template.required : false, maxInstances:template.maxInstances};
605
464
  for(let i = 0; i < template.maxInstances; i++){
606
- this._defaults["_" + name + "_" + i] = {value:"", required:false};
465
+ this._defaults["_" + name + "_" + i] = {value:"", required:false, type: template.type};
607
466
  }
608
467
  }
609
468
  else{
@@ -648,9 +507,10 @@ export class NodeBuilder {
648
507
 
649
508
  public addTemplate(template: { template: ITemplateClass; config: TemplateConfig; }): NodeBuilder {
650
509
  let templateDescriptor = template.template.getTemplateDescriptor();
510
+ let templateConfig = template.config;
651
511
 
652
512
  // load the html source file
653
- let uiDom = new JSDOM(fs.readFileSync(templateDescriptor.templateFile()!));
513
+ let uiDom = new (getJSDOM())(fs.readFileSync(templateDescriptor.templateFile()!));
654
514
 
655
515
  // apply the onCompose section if available.
656
516
  let onCompose = uiDom.window.document.querySelector("script[template-section='onCompose']")?.innerHTML;
@@ -662,7 +522,16 @@ export class NodeBuilder {
662
522
  let sections:string[] = ['IncludeOnce', 'IncludeEditPrepare', 'IncludeEditSave', 'IncludeEditCancel', 'IncludeEditDelete', 'IncludeEditForm', 'IncludeDocumentation'];
663
523
  Object.values(sections).forEach(section => {
664
524
  let sectionData = uiDom.window.document.querySelector(`[template-section='on${section}']`)?.innerHTML;
525
+
665
526
  if(sectionData){
527
+ if(section !== "IncludeOnce" && section !== "IncludeEditForm" && section !== "IncludeDocumentation") {
528
+ // add the packageing around the sectionData
529
+ sectionData = `{
530
+ let node = this;
531
+ let templateConfig = ${JSON.stringify(templateConfig)};
532
+ ${sectionData}
533
+ }`;
534
+ }
666
535
  (this as any)[`add${section}`](templateDescriptor.name(), sectionData);
667
536
  }
668
537
  });
@@ -705,6 +574,69 @@ export class NodeBuilder {
705
574
  return this;
706
575
  }
707
576
 
577
+ public buildOnceHtml(): {source: string, html: string}[] {
578
+ return this._onIncludeOnce.map(entry => ({
579
+ source: entry.source,
580
+ html: this.serializeHTML(entry.source, entry.script)
581
+ }));
582
+ }
583
+
584
+ public buildDeferredType(): string {
585
+ // Apply shadow defaults (same side-effect as buildType)
586
+ let shaddowDefaults = Object.entries(this._defaults).filter(([name, template]) => template.type && template.type.endsWith("[]"));
587
+ if (shaddowDefaults.length > 0) {
588
+ this.addIncludeEditSave(this._name + "_shaddowDefaults",
589
+ `{ let node = this; ` +
590
+ shaddowDefaults
591
+ .map(([name, template]) => `
592
+ for(let i = 0; i < ${template.maxInstances}; i++){
593
+ node["_${name}_" + i] = (node.${name}[i] || {}).proxy || "";
594
+ }`)
595
+ .join("\n\n") +
596
+ `}`
597
+ );
598
+ }
599
+
600
+ const registerTypeBody = `{
601
+ category: '${this._category}',
602
+ ${(this._category !== "config" && this._icon) ? `icon: '${this._icon}',` : ""}
603
+ ${(this._category !== "config" && this._color) ? `color: '${this._color}',` : ""}
604
+ ${(this._category !== "config" && this._labelStyle) ? `labelStyle: '${this._labelStyle}',` : ""}
605
+ ${(this._label) ? `label: ${this._label},` : ""}
606
+ ${this._paletteLabel ? `paletteLabel: '${this._paletteLabel}',` : ""}
607
+ ${(this._category !== "config") ? `inputs:${this._inputs ? "1" : "0"},` : ""}
608
+ ${(this._category !== "config") ? `inputLabels:(i) => '${this._inputs}',` : ""}
609
+ ${(this._category !== "config") ? `outputs:${this._outputs.length},` : ""}
610
+ ${(this._category !== "config") ? `outputLabels:(i) => ${this.serializeProperty(this._outputs)}[i],` : ""}
611
+ defaults: {
612
+ ${Object.entries(this._defaults).map(([key, value]) => this.serializeDefault(key, value)).join("\n")}
613
+ },
614
+ oneditprepare: function() {
615
+ ${this._onIncludeEditPrepare.map(entry => this.serializeJavaScript(entry.source, entry.script)).join("")}
616
+ },
617
+ oneditsave: function() {
618
+ ${this._onIncludeEditSave.map(entry => this.serializeJavaScript(entry.source, entry.script)).join("")}
619
+ },
620
+ oneditcancel: function() {
621
+ ${this._onIncludeEditCancel.map(entry => this.serializeJavaScript(entry.source, entry.script)).join("")}
622
+ },
623
+ oneditdelete: function() {
624
+ ${this._onIncludeEditDelete.map(entry => this.serializeJavaScript(entry.source, entry.script)).join("")}
625
+ },
626
+ }`;
627
+
628
+ return getBeautify().html_beautify(`
629
+ <script type="text/javascript">
630
+ (function () {
631
+ RED.events.on('registry:node-set-added', function (ns) {
632
+ if (ns.types && ns.types.indexOf('${this._name}') !== -1) {
633
+ RED.nodes.registerType('${this._name}', ${registerTypeBody});
634
+ }
635
+ });
636
+ }());
637
+ </script>`, beautifyOptions);
638
+ }
639
+
708
640
  private serializeProperty(value:any):string{
709
641
  switch(typeof value){
710
642
  case "boolean": {
@@ -737,18 +669,18 @@ export class NodeBuilder {
737
669
  }
738
670
 
739
671
  return `<!-- ${source} -->
740
- ${Handlebars.compile(script)(context)}
672
+ ${getHandlebars().compile(script)(context)}
741
673
  `
742
674
  }
743
675
 
744
- private serializeJavaScript(source:string, script:string):string{
676
+ private serializeJavaScript(source:string, script:string):string{
745
677
  let context = {
746
- defaults: (this._category === "config")
678
+ defaults: (this._category === "config")
747
679
  ? Object.fromEntries(Object.keys(this._defaults).map(k => [k, `node-config-input-${k}`]))
748
680
  : Object.fromEntries(Object.keys(this._defaults).map(k => [k, `node-input-${k}`]))
749
681
  }
750
682
 
751
- let serialized = Handlebars.compile(script)(context).trim();
683
+ let serialized = getHandlebars().compile(script)(context).trim();
752
684
  if(!serialized.startsWith("{") && !serialized.endsWith("}")){
753
685
  serialized = "{\n" + serialized + "\n}";
754
686
  }
@@ -758,14 +690,14 @@ export class NodeBuilder {
758
690
  `
759
691
  }
760
692
 
761
- private serializeFormEntry(source:string, script:string):string{
693
+ private serializeFormEntry(source:string, script:string):string{
762
694
  let context = {
763
- defaults: (this._category === "config")
695
+ defaults: (this._category === "config")
764
696
  ? Object.fromEntries(Object.keys(this._defaults).map(k => [k, `node-config-input-${k}`]))
765
697
  : Object.fromEntries(Object.keys(this._defaults).map(k => [k, `node-input-${k}`]))
766
698
  }
767
699
  return `
768
- <div id='section_${source}'>${Handlebars.compile(script)(context)}
700
+ <div id='section_${source}'>${getHandlebars().compile(script)(context)}
769
701
  </div>
770
702
  `
771
703
  }
@@ -794,7 +726,7 @@ export class NodeBuilder {
794
726
  );
795
727
  }
796
728
 
797
- return beautify.html_beautify(`
729
+ return getBeautify().html_beautify(`
798
730
  ${this._onIncludeOnce.map(entry => this.serializeHTML(entry.source, entry.script)).join("")}
799
731
  <script type="text/javascript">
800
732
  RED.nodes.registerType('${this._name}',{
@@ -828,7 +760,7 @@ export class NodeBuilder {
828
760
  };
829
761
 
830
762
  public buildHtml():string{
831
- return beautify.html_beautify(`
763
+ return getBeautify().html_beautify(`
832
764
  <script type="text/html" data-template-name='${this._name}'>
833
765
  ${this._onIncludeEditForm.map(entry => this.serializeFormEntry(entry.source, entry.script)).join("")}
834
766
  </script>
@@ -861,13 +793,13 @@ let evalInContext = (context: any, js: string) => {
861
793
  }
862
794
  };
863
795
 
864
- let buildNode = function(node:INodeClass){
796
+ let buildNodeBuilder = function(node: INodeClass): NodeBuilder {
865
797
  // get the node descriptor and create the builder.
866
798
  let nodeDescriptor = node.getNodeDescriptor();
867
799
  let nodeBuilder = new NodeBuilder(nodeDescriptor.id(), nodeDescriptor.group());
868
800
 
869
801
  // load the html source file
870
- let uiDom = new JSDOM(fs.readFileSync(nodeDescriptor.sourceFile()));
802
+ let uiDom = new (getJSDOM())(fs.readFileSync(nodeDescriptor.sourceFile()));
871
803
 
872
804
  // apply the onCompose section if available.
873
805
  let onCompose = uiDom.window.document.querySelector("script[template-section='onCompose']")?.innerHTML;
@@ -887,10 +819,15 @@ let buildNode = function(node:INodeClass){
887
819
  // add some of the dependencies here.
888
820
  nodeDescriptor.templates().forEach(template => nodeBuilder.addTemplate(template));
889
821
 
890
- return [nodeBuilder.buildType(), nodeBuilder.buildHtml(), nodeBuilder.buildDocumentation()].join("\n\n");
822
+ return nodeBuilder;
823
+ }
891
824
 
825
+ let buildNode = function(node:INodeClass){
826
+ let nodeBuilder = buildNodeBuilder(node);
827
+ return [nodeBuilder.buildType(), nodeBuilder.buildHtml(), nodeBuilder.buildDocumentation()].join("\n\n");
892
828
  }
893
829
 
894
830
  export {
895
- buildNode
831
+ buildNode,
832
+ buildNodeBuilder
896
833
  }
@@ -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
 
@@ -105,31 +106,17 @@ class NodeGenerator {
105
106
  }
106
107
 
107
108
  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")
125
-
126
- fs.writeFileSync(nodesOutputFile + ".html",`
109
+ // STEP 1. Write an empty Nodes.html.
110
+ // Client-side node type definitions live in Plugins.html (generated below)
111
+ // to prevent HTML-scanning conflicts when multiple plugins bundle the same
112
+ // plugincore infrastructure nodes.
113
+ fs.writeFileSync(nodesOutputFile + ".html", `
127
114
  <!--
128
115
  ${NodeGenerator.warning}
116
+ Client-side node type definitions have been moved to Plugins.html to prevent
117
+ duplicate type registration conflicts when multiple plugins share the same
118
+ plugincore infrastructure nodes.
129
119
  -->
130
- ${templateInclude}
131
- ${nodeHTMLOutut}
132
-
133
120
  `);
134
121
 
135
122
  // Step 3. compose the JS structure for each of the nodes.
@@ -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 => {
@@ -195,31 +204,39 @@ module.exports = function (RED) {
195
204
  let plugin = RED.plugins.get(pluginID);
196
205
 
197
206
  if(addedPlugin && plugin.instantiate && !plugin.instance){
198
- console.log("Deploying instance of: " + plugin.id);
199
207
  plugin.instance = new plugin.class();
200
-
208
+
201
209
  // instantiate the plugin.
202
210
  await plugin.instance.init(RED);
203
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
+
204
222
  // publish an event that it has ben deployed.
205
223
  RED.events.emit("plugin.instantiated", pluginID);
206
-
224
+
207
225
  pluginList.splice(pluginList.findIndex(current => current.id() === addedPlugin.id()), 1);
208
226
 
209
227
  // register a flow deployment listener.
210
- let startupDeployment = true;
228
+ // startupDeployment is only needed for a truly fresh Node-RED with no saved flows.
229
+ let startupDeployment = !hasExistingFlows;
211
230
  RED.events.on('runtime-event', async (event) => {
212
- // startup deployment.
231
+ // startup deployment (fresh Node-RED with no pre-existing flows).
213
232
  if ("runtime-deploy" === event?.id && startupDeployment) {
214
- //console.log("STARTUP DEPLOYMENT");
215
233
  startupDeployment = false;
216
234
  const flows = await runtime.flows.getFlows({});
217
235
  await plugin.instance.onDeploy(flows);
218
236
  }
219
-
237
+
220
238
  // stopping flows on redeployment.
221
239
  if ("runtime-state" === event?.id && event?.payload?.state === "stop" && event?.payload?.deploy) {
222
- //console.log("REDEPLOY!");
223
240
  const flows = await runtime.flows.getFlows({});
224
241
  await plugin.instance.onDeploy(flows);
225
242
  }
@@ -240,6 +257,43 @@ module.exports = function (RED) {
240
257
  `
241
258
  fs.writeFileSync(pluginsOutputFile + ".js", output);
242
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 node definitions.
262
+ // All nodes are registered via registry:node-set-added so that:
263
+ // a) registerType is only called after typeToId/nodeSets are populated, and
264
+ // b) data-template-name declarations are absent from Nodes.html, avoiding
265
+ // the HTML-scanning conflict that causes Node-RED to reject an entire
266
+ // plugin when a second package declares the same node type.
267
+ const seenOnce = new Set<string>();
268
+ const onceHtmlParts: string[] = [];
269
+ const nodePluginParts: string[] = [];
270
+
271
+ Object.values(this._nodes).forEach(node => {
272
+ console.log(`Building Plugins.html for: ${node.descriptor.id()}`);
273
+ const nb = buildNodeBuilder(node.nodeClass);
274
+
275
+ nb.buildOnceHtml().forEach(({source, html}) => {
276
+ if (!seenOnce.has(source)) {
277
+ seenOnce.add(source);
278
+ onceHtmlParts.push(html);
279
+ }
280
+ });
281
+
282
+ nodePluginParts.push([
283
+ nb.buildDeferredType(),
284
+ nb.buildHtml(),
285
+ nb.buildDocumentation()
286
+ ].join('\n\n'));
287
+ });
288
+
289
+ fs.writeFileSync(pluginsOutputFile + ".html", `
290
+ <!--
291
+ ${NodeGenerator.warning}
292
+ -->
293
+ ${onceHtmlParts.join('\n')}
294
+ ${nodePluginParts.join('\n\n')}
295
+ `);
296
+ console.log(`Done building Plugins.html\n`);
243
297
  }
244
298
 
245
299
  private static generateHeader(node:NodeDescriptor){