@theotherwillembotha/node-red-plugincore 0.1.0 → 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 (134) hide show
  1. package/README.md +616 -220
  2. package/build/GenerateNodes.js +3 -6
  3. package/build/Nodes.html +3 -1504
  4. package/build/Nodes.js +5 -15
  5. package/build/Plugins.html +227 -0
  6. package/build/Plugins.js +18 -8
  7. package/build/core/NodeConstructor.d.ts +9 -16
  8. package/build/core/NodeConstructor.d.ts.map +1 -1
  9. package/build/core/NodeConstructor.js +109 -146
  10. package/build/core/NodeGenerator.d.ts.map +1 -1
  11. package/build/core/NodeGenerator.js +79 -28
  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/node/WebhookServerConfigNode.d.ts.map +1 -1
  70. package/build/core/webhook/service/ReverseProxyTypeService.d.ts.map +1 -1
  71. package/build/core/webhook/service/WebhookServerService.d.ts +3 -4
  72. package/build/core/webhook/service/WebhookServerService.d.ts.map +1 -1
  73. package/build/core/webhook/service/WebhookServerService.js +48 -18
  74. package/build/core/webhook/template/WebhookTemplate.d.ts.map +1 -1
  75. package/build/index.d.ts +5 -2
  76. package/build/index.d.ts.map +1 -1
  77. package/build/index.js +6 -2
  78. package/build/runtime/NodeManagerRuntime.js +140 -0
  79. package/documentation/CounterMetricConfigNode.png +0 -0
  80. package/documentation/GaugeMetricConfigNode.png +0 -0
  81. package/documentation/LoggerTemplate.png +0 -0
  82. package/documentation/MetricsTemplate.png +0 -0
  83. package/documentation/StateTemplate.png +0 -0
  84. package/documentation/TimerMetricConfigNode_histogram.png +0 -0
  85. package/documentation/TimerMetricConfigNode_summary.png +0 -0
  86. package/documentation/WebhookServerConfigNode.png +0 -0
  87. package/documentation/WebhookTemplate.png +0 -0
  88. package/package.json +4 -10
  89. package/src/GenerateNodes.ts +7 -11
  90. package/src/core/NodeConstructor.ts +130 -186
  91. package/src/core/NodeGenerator.ts +88 -33
  92. package/src/core/NodeManagerRuntime.ts +179 -0
  93. package/src/core/logger/service/LoggerService.ts +39 -26
  94. package/src/core/logger/template/LoggerTemplate.html +11 -6
  95. package/src/core/metrics/MetricsDecorator.ts +140 -168
  96. package/src/core/metrics/node/CounterMetricConfigNode.ts +20 -20
  97. package/src/core/metrics/node/GaugeMetricConfigNode.ts +24 -25
  98. package/src/core/metrics/node/MetricsConfigNode.ts +29 -48
  99. package/src/core/metrics/node/TimerMetricConfigNode.ts +50 -59
  100. package/src/core/metrics/service/MetricsService.ts +214 -489
  101. package/src/core/metrics/template/CounterMetricTemplate.html +0 -1
  102. package/src/core/metrics/template/GaugeMetricTemplate.html +1 -1
  103. package/src/core/metrics/template/MetricsTemplate.html +132 -13
  104. package/src/core/metrics/template/MetricsTemplate.ts +3 -4
  105. package/src/core/other/node/DelegatedConfigReferenceNode.ts +1 -1
  106. package/src/core/other/service/SettingsService.ts +6 -10
  107. package/src/core/other/template/BasicTemplate.html +1 -1
  108. package/src/core/state/node/InternalStateConfigNode.html +77 -0
  109. package/src/core/state/node/InternalStateConfigNode.ts +102 -0
  110. package/src/core/state/node/StateConfigNode.ts +29 -0
  111. package/src/core/state/service/StateService.ts +180 -0
  112. package/src/core/state/template/StateTemplate.html +244 -0
  113. package/src/core/state/template/StateTemplate.ts +17 -0
  114. package/src/core/tagging/ServiceDescriptionDecorator.ts +23 -0
  115. package/src/core/tagging/service/NodeTypeService.ts +6 -11
  116. package/src/core/ui/template/UIHelperTemplate.html +41 -26
  117. package/src/core/webhook/service/WebhookServerService.ts +39 -23
  118. package/src/core/webhook/template/WebhookTemplate.html +28 -35
  119. package/src/index.ts +7 -2
  120. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts +0 -20
  121. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts.map +0 -1
  122. package/build/core/logger/node/ConsoleLoggerConfigNode.js +0 -62
  123. package/build/core/logger/node/RestLoggerConfigNode.d.ts +0 -40
  124. package/build/core/logger/node/RestLoggerConfigNode.d.ts.map +0 -1
  125. package/build/core/logger/node/RestLoggerConfigNode.js +0 -128
  126. package/documentation/ConsoleLoggerConfigNode.png +0 -0
  127. package/documentation/MetricsConfigNode.png +0 -0
  128. package/documentation/RestLoggerConfigNode.png +0 -0
  129. package/documentation/TimerMetricConfigNode.png +0 -0
  130. package/documentation/WebhookNode.png +0 -0
  131. package/src/core/logger/node/ConsoleLoggerConfigNode.html +0 -90
  132. package/src/core/logger/node/ConsoleLoggerConfigNode.ts +0 -71
  133. package/src/core/logger/node/RestLoggerConfigNode.html +0 -183
  134. package/src/core/logger/node/RestLoggerConfigNode.ts +0 -170
@@ -1,22 +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
6
 
7
+ import {
8
+ NODEMANAGER_API_VERSION,
9
+ POST_CONSTRUCT_KEY,
10
+ BaseNodeConfig,
11
+ runPostConstructInitializers,
12
+ NodeManager
13
+ } from "./NodeManagerRuntime";
13
14
 
14
-
15
- export interface BaseNodeConfig {
16
- id:string;
17
- name:string;
18
- type:string;
19
- }
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
+ };
20
23
 
21
24
  export abstract class BaseNode<Config extends BaseNodeConfig> {
22
25
 
@@ -198,8 +201,6 @@ export abstract class Template{
198
201
 
199
202
  type VoidMethod = (...args: any[]) => void | Promise<void>;
200
203
 
201
- const POST_CONSTRUCT_KEY = Symbol('postConstructInitializers');
202
-
203
204
  // Builder class for post-construct decorators
204
205
  class PostConstructDecoratorBuilder<TMetadata = any> {
205
206
  private decoratorName: string;
@@ -341,143 +342,6 @@ export function createPostConstructDecorator<TMetadata = any>(decoratorName: str
341
342
  return new PostConstructDecoratorBuilder<TMetadata>(decoratorName);
342
343
  }
343
344
 
344
- // Single function to run all post-construct initializers
345
- let runPostConstructInitializers = function(instance: any) {
346
- const initializers = instance[POST_CONSTRUCT_KEY];
347
- if (initializers && initializers.length > 0) {
348
- const className = instance.constructor.name;
349
- //console.log(`Running ${initializers.length} post-construct initializers for ${className}`);
350
-
351
- initializers.forEach((init: Function, index: number) => {
352
- try {
353
- init(instance);
354
- } catch (error) {
355
- console.error(
356
- `Post-construct initialization failed:\n` +
357
- ` Class: ${className}\n` +
358
- ` Initializer: ${index + 1}/${initializers.length}\n` +
359
- ` Error: ${error instanceof Error ? error.message : String(error)}`
360
- );
361
- throw error;
362
- }
363
- });
364
-
365
- // Clear the initializers after running
366
- delete instance[POST_CONSTRUCT_KEY];
367
- }
368
- }
369
-
370
- // ************************************************
371
-
372
- export class NodeManager{
373
- private static PLUGINID = "node-red-contrib-theotherwillembotha/services-plugin";
374
- private static _RED: NodeAPI<NodeAPISettingsWithData>;
375
-
376
- private nodeTypeService:NodeTypeService;
377
- private typeBacklog:INodeClass[] = []
378
-
379
- public constructor(RED : NodeAPI<NodeAPISettingsWithData>){
380
- NodeManager._RED = RED;
381
-
382
- let nodeTypeServiceListener = async (pluginID:String) => {
383
- if(pluginID === "@theotherwillembotha/nodetypeservice"){
384
- // disable the listener- the plugin has been installed.
385
- RED.events.off('plugin.instantiated', nodeTypeServiceListener)
386
-
387
- // get a handle on the NodeTypeService
388
- let plugin = RED.plugins.get("@theotherwillembotha/nodetypeservice") as any;
389
- this.nodeTypeService = (RED.plugins.get("@theotherwillembotha/nodetypeservice") as any).instance as NodeTypeService;
390
-
391
- // process the backlog.
392
- for(let nodeType of this.typeBacklog){
393
- for(let tag of nodeType.getNodeDescriptor().tags()){
394
- this.nodeTypeService.registerNodeType(tag, nodeType);
395
- }
396
- }
397
- }
398
- }
399
-
400
- // check if the reverserproxyttypesewrvice is installed yet.
401
- this.nodeTypeService = (RED.plugins.get("@theotherwillembotha/nodetypeservice") as any).instance as NodeTypeService;
402
- if(!this.nodeTypeService) {
403
- RED.events.on('plugin.instantiated', nodeTypeServiceListener);
404
- }
405
- }
406
-
407
- public static get RED() {
408
- return NodeManager._RED;
409
- }
410
-
411
- public registerService(type:IServiceClass):NodeManager{
412
- let servicePlugin:any = NodeManager.RED.plugins.get(NodeManager.PLUGINID) as PluginDefinition<any>;
413
- if(servicePlugin){
414
- servicePlugin.instance.install(NodeManager.RED, type);
415
- }
416
- else{
417
- let serviceName = `${type.getServiceDescriptor().sourceFile()}.${type.getServiceDescriptor().name()}`;
418
- console.log(`ERROR: plugin ${NodeManager.PLUGINID} does not seem to be loaded. Cannot start service: ${serviceName}`);
419
- }
420
- return this;
421
- }
422
-
423
- public registerNodeType(typeName: string, type: INodeClass):NodeManager{
424
- if(!type){
425
- console.error(`Type "${typeName}" has no associated type. Make sure that it was exported and included in the index.js file!`)
426
- }
427
-
428
- let nodeConstructor = function(this: any, config: BaseNodeConfig | NodeDef){
429
- //console.log("constructing node", typeName);
430
-
431
- try{
432
- // instantiate the node.
433
- NodeManager.RED.nodes.createNode(this, config as any as NodeDef);
434
- // assign an instance of it to the config.
435
- //(config as any).__node = this;
436
-
437
- // instatiate the actual node.
438
- let node = new (type as any)(this, config);
439
- //this.__node = node;
440
-
441
- // run the constructor initializers.
442
- runPostConstructInitializers(node);
443
-
444
-
445
- // after the node has been instantiated, attempt to invole the onInit method.
446
- if(node.onInit){
447
- node.onInit();
448
- }
449
- }
450
- catch(error){
451
- console.log(error);
452
- console.log("typeName:", typeName)
453
- console.log("type:", type)
454
- console.log("config:", config)
455
- }
456
- }
457
-
458
- try{
459
- NodeManager.RED.nodes.registerType(typeName, nodeConstructor, { settings: {}});
460
-
461
- let nodeDescription = type.getNodeDescriptor();
462
-
463
- // registger the node tags.
464
- if(this.nodeTypeService){
465
- for(let tag of nodeDescription.tags()){
466
- this.nodeTypeService.registerNodeType(tag, type);
467
- }
468
- }
469
- else{
470
- this.typeBacklog.push(type);
471
- }
472
-
473
- }
474
- catch(error){
475
- console.error("registering node: " + typeName + " failed", error);
476
- }
477
- return this;
478
- }
479
- }
480
-
481
345
  export type FlowDeployment = {
482
346
  rev:string,
483
347
  flows:[FlowElement]
@@ -513,25 +377,27 @@ export abstract class BaseService {
513
377
  }
514
378
  }
515
379
 
516
- const { JSDOM } = jsdom;
517
- const md = markdownit({
518
- html: true,
519
- linkify: true,
520
- typographer: true
521
- })
522
-
523
- // Custom renderer for headings
524
- md.renderer.rules.heading_open = function (tokens, idx, options, env, self) {
525
- const token = tokens[idx];
526
- const level = token.tag.substring(1); // 'h1', 'h2', etc. -> '1', '2'
527
- return `<h${level} class="custom-heading">`;
528
- };
529
-
530
- md.renderer.rules.heading_close = function (tokens, idx, options, env, self) {
531
- const token = tokens[idx];
532
- const level = token.tag.substring(1);
533
- return `</h${level}>`;
534
- };
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
+ }
535
401
 
536
402
  let beautifyOptions = {
537
403
  "indent_size": 4,
@@ -591,7 +457,7 @@ export class NodeBuilder {
591
457
  }
592
458
 
593
459
  public addDefault(name:string, template:DefaultTemplateType):NodeBuilder{
594
- // if list:true, this is an array of config references generate shadow defaults for each slot.
460
+ // if list:true, this is an array of config references - generate shadow defaults for each slot.
595
461
  if(template.list === true) {
596
462
  template.maxInstances = template.maxInstances ? template.maxInstances : 10;
597
463
  this._defaults[name] = {value:"", required:(template.required) ? template.required : false, maxInstances:template.maxInstances};
@@ -641,9 +507,10 @@ export class NodeBuilder {
641
507
 
642
508
  public addTemplate(template: { template: ITemplateClass; config: TemplateConfig; }): NodeBuilder {
643
509
  let templateDescriptor = template.template.getTemplateDescriptor();
510
+ let templateConfig = template.config;
644
511
 
645
512
  // load the html source file
646
- let uiDom = new JSDOM(fs.readFileSync(templateDescriptor.templateFile()!));
513
+ let uiDom = new (getJSDOM())(fs.readFileSync(templateDescriptor.templateFile()!));
647
514
 
648
515
  // apply the onCompose section if available.
649
516
  let onCompose = uiDom.window.document.querySelector("script[template-section='onCompose']")?.innerHTML;
@@ -655,7 +522,16 @@ export class NodeBuilder {
655
522
  let sections:string[] = ['IncludeOnce', 'IncludeEditPrepare', 'IncludeEditSave', 'IncludeEditCancel', 'IncludeEditDelete', 'IncludeEditForm', 'IncludeDocumentation'];
656
523
  Object.values(sections).forEach(section => {
657
524
  let sectionData = uiDom.window.document.querySelector(`[template-section='on${section}']`)?.innerHTML;
525
+
658
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
+ }
659
535
  (this as any)[`add${section}`](templateDescriptor.name(), sectionData);
660
536
  }
661
537
  });
@@ -698,6 +574,69 @@ export class NodeBuilder {
698
574
  return this;
699
575
  }
700
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
+
701
640
  private serializeProperty(value:any):string{
702
641
  switch(typeof value){
703
642
  case "boolean": {
@@ -730,18 +669,18 @@ export class NodeBuilder {
730
669
  }
731
670
 
732
671
  return `<!-- ${source} -->
733
- ${Handlebars.compile(script)(context)}
672
+ ${getHandlebars().compile(script)(context)}
734
673
  `
735
674
  }
736
675
 
737
- private serializeJavaScript(source:string, script:string):string{
676
+ private serializeJavaScript(source:string, script:string):string{
738
677
  let context = {
739
- defaults: (this._category === "config")
678
+ defaults: (this._category === "config")
740
679
  ? Object.fromEntries(Object.keys(this._defaults).map(k => [k, `node-config-input-${k}`]))
741
680
  : Object.fromEntries(Object.keys(this._defaults).map(k => [k, `node-input-${k}`]))
742
681
  }
743
682
 
744
- let serialized = Handlebars.compile(script)(context).trim();
683
+ let serialized = getHandlebars().compile(script)(context).trim();
745
684
  if(!serialized.startsWith("{") && !serialized.endsWith("}")){
746
685
  serialized = "{\n" + serialized + "\n}";
747
686
  }
@@ -751,14 +690,14 @@ export class NodeBuilder {
751
690
  `
752
691
  }
753
692
 
754
- private serializeFormEntry(source:string, script:string):string{
693
+ private serializeFormEntry(source:string, script:string):string{
755
694
  let context = {
756
- defaults: (this._category === "config")
695
+ defaults: (this._category === "config")
757
696
  ? Object.fromEntries(Object.keys(this._defaults).map(k => [k, `node-config-input-${k}`]))
758
697
  : Object.fromEntries(Object.keys(this._defaults).map(k => [k, `node-input-${k}`]))
759
698
  }
760
699
  return `
761
- <div id='section_${source}'>${Handlebars.compile(script)(context)}
700
+ <div id='section_${source}'>${getHandlebars().compile(script)(context)}
762
701
  </div>
763
702
  `
764
703
  }
@@ -787,7 +726,7 @@ export class NodeBuilder {
787
726
  );
788
727
  }
789
728
 
790
- return beautify.html_beautify(`
729
+ return getBeautify().html_beautify(`
791
730
  ${this._onIncludeOnce.map(entry => this.serializeHTML(entry.source, entry.script)).join("")}
792
731
  <script type="text/javascript">
793
732
  RED.nodes.registerType('${this._name}',{
@@ -821,7 +760,7 @@ export class NodeBuilder {
821
760
  };
822
761
 
823
762
  public buildHtml():string{
824
- return beautify.html_beautify(`
763
+ return getBeautify().html_beautify(`
825
764
  <script type="text/html" data-template-name='${this._name}'>
826
765
  ${this._onIncludeEditForm.map(entry => this.serializeFormEntry(entry.source, entry.script)).join("")}
827
766
  </script>
@@ -854,13 +793,13 @@ let evalInContext = (context: any, js: string) => {
854
793
  }
855
794
  };
856
795
 
857
- let buildNode = function(node:INodeClass){
796
+ let buildNodeBuilder = function(node: INodeClass): NodeBuilder {
858
797
  // get the node descriptor and create the builder.
859
798
  let nodeDescriptor = node.getNodeDescriptor();
860
799
  let nodeBuilder = new NodeBuilder(nodeDescriptor.id(), nodeDescriptor.group());
861
800
 
862
801
  // load the html source file
863
- let uiDom = new JSDOM(fs.readFileSync(nodeDescriptor.sourceFile()));
802
+ let uiDom = new (getJSDOM())(fs.readFileSync(nodeDescriptor.sourceFile()));
864
803
 
865
804
  // apply the onCompose section if available.
866
805
  let onCompose = uiDom.window.document.querySelector("script[template-section='onCompose']")?.innerHTML;
@@ -880,10 +819,15 @@ let buildNode = function(node:INodeClass){
880
819
  // add some of the dependencies here.
881
820
  nodeDescriptor.templates().forEach(template => nodeBuilder.addTemplate(template));
882
821
 
883
- return [nodeBuilder.buildType(), nodeBuilder.buildHtml(), nodeBuilder.buildDocumentation()].join("\n\n");
822
+ return nodeBuilder;
823
+ }
884
824
 
825
+ let buildNode = function(node:INodeClass){
826
+ let nodeBuilder = buildNodeBuilder(node);
827
+ return [nodeBuilder.buildType(), nodeBuilder.buildHtml(), nodeBuilder.buildDocumentation()].join("\n\n");
885
828
  }
886
829
 
887
830
  export {
888
- buildNode
831
+ buildNode,
832
+ buildNodeBuilder
889
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 => {
@@ -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,43 @@ 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 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`);
242
297
  }
243
298
 
244
299
  private static generateHeader(node:NodeDescriptor){