This repository has been archived by the owner on Nov 13, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 31
/
PluginManagementFacility.ts
1230 lines (1131 loc) · 57.2 KB
/
PluginManagementFacility.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/
import { PerfTiming } from "@zowe/perf-timing";
import { IImperativeConfig } from "../../src/doc/IImperativeConfig";
import { UpdateImpConfig } from "../../src/UpdateImpConfig";
import { isAbsolute, join } from "path";
import { ImperativeConfig, JsUtils } from "../../../utilities";
import { Logger } from "../../../logger";
import { existsSync } from "fs";
import { PMFConstants } from "./utilities/PMFConstants";
import { readFileSync, writeFileSync } from "jsonfile";
import { IPluginCfgProps } from "./doc/IPluginCfgProps";
import { ICommandDefinition, ICommandProfileTypeConfiguration } from "../../../cmd";
import { IssueSeverity, PluginIssues } from "./utilities/PluginIssues";
import { ConfigurationValidator } from "../ConfigurationValidator";
import { ConfigurationLoader } from "../ConfigurationLoader";
import { DefinitionTreeResolver } from "../DefinitionTreeResolver";
import { IImperativeOverrides } from "../doc/IImperativeOverrides";
import { AppSettings } from "../../../settings";
import { IO } from "../../../io";
/**
* This class is the main engine for the Plugin Management Facility. The
* underlying class should be treated as a singleton and should be accessed
* via PluginManagmentFacility.instance.
*/
export class PluginManagementFacility {
/**
* This is the variable that stores the specific instance of the PMF. Defined
* as static so that it can be accessed from anywhere.
*
* @private
* @type {PluginManagementFacility}
*/
private static mInstance: PluginManagementFacility;
/**
* Gets a single instance of the PMF. On the first call of
* PluginManagementFacility.instance, a new PMF is initialized and returned.
* Every subsequent call will use the one that was first created.
*
* @returns {PluginManagementFacility} - The newly initialized PMF object.
*/
public static get instance(): PluginManagementFacility {
if (this.mInstance == null) {
this.mInstance = new PluginManagementFacility();
}
return this.mInstance;
}
/**
* Internal reference to the set of configuration properties for all loaded plugins.
*/
private mAllPluginCfgProps: IPluginCfgProps[] = [];
/**
* Get the set of configuration properties for all loaded plugins.
*/
public get allPluginCfgProps(): IPluginCfgProps[] {
return this.mAllPluginCfgProps;
}
/**
* Internal reference to the overrides provided by plugins.
*/
private mPluginOverrides: IImperativeOverrides = {};
/**
* Object that defines what overrides will be provided by all plugins.
*/
public get pluginOverrides(): IImperativeOverrides {
return this.mPluginOverrides;
}
/**
* Used as a short-name access to PMF constants.
*/
private pmfConst: PMFConstants = PMFConstants.instance;
/**
* The CLI command tree with module globs already resolved.
*
* @private
* @type {ICommandDefinition}
*/
private resolvedCliCmdTree: ICommandDefinition = null;
/**
* The property name within package.json that holds the
* Imperative configuration object.
*
* @private
* @type {string}
*/
private readonly impConfigPropNm = "imperative";
/**
* Used for internal imperative logging.
*
* @private
* @type {Logger}
*/
private impLogger: Logger = Logger.getImperativeLogger();
/**
* A class with recorded issues for each plugin for which problems were detected.
*
* @private
* @type {IPluginIssues}
*/
private pluginIssues = PluginIssues.instance;
/**
* A set of bright dependencies used by plugins. Each item in the
* set contains the dependency's property name, and the the version
* of that dependency.
*
* @type {Object}
*/
private readonly npmPkgNmProp = "name";
private readonly noPeerDependency = "-1";
/**
* The semantic versioning module (which does not have the
* typing to do an 'import').
*/
private readonly semver = require("semver");
/**
* Tracker to ensure that [init]{@link PluginManagementFacility#init} was
* called. Most methods cannot be used unless init was called first.
*
* @private
* @type {boolean}
*/
private wasInitCalled = false;
// __________________________________________________________________________
/**
* Initialize the PMF. Must be called to enable the various commands provided
* by the facility.
*/
public init(): void {
this.impLogger.debug("PluginManagementFacility.init() - Start");
// Load lib after the fact to save on speed when plugins not enabled
const { PluginRequireProvider } = require("./PluginRequireProvider");
// Create the hook for imperative and the application cli
PluginRequireProvider.createPluginHooks([
PMFConstants.instance.IMPERATIVE_PKG_NAME,
PMFConstants.instance.CLI_CORE_PKG_NAME
]);
// Add the plugin group and related commands.
UpdateImpConfig.addCmdGrp({
name: "plugins",
type: "group",
description: "Install and manage plug-ins.",
children: [
// Done dynamically so that PMFConstants can be initialized
require("./cmd/install/install.definition").installDefinition,
require("./cmd/list/list.definition").listDefinition,
require("./cmd/uninstall/uninstall.definition").uninstallDefinition,
require("./cmd/update/update.definition").updateDefinition,
require("./cmd/validate/validate.definition").validateDefinition
]
});
// When everything is done set this variable to true indicating successful
// initialization.
this.wasInitCalled = true;
this.impLogger.debug("PluginManagementFacility.init() - Success");
}
// __________________________________________________________________________
/**
* Add all installed plugins' commands and profiles into the host CLI's command tree.
*
* @param resolvedCliCmdTree - The CLI command tree with
* module globs already resolved.
*/
public addAllPluginsToHostCli(resolvedCliCmdTree: ICommandDefinition): void {
// Store the host CLI command tree. Later functions will use it.
this.resolvedCliCmdTree = resolvedCliCmdTree;
// Loop through each plugin and add it to the CLI command tree
for (const nextPluginCfgProps of this.mAllPluginCfgProps) {
this.addPluginToHostCli(nextPluginCfgProps);
// log the issue list for this plugin
const issueListForPlugin = this.pluginIssues.getIssueListForPlugin(nextPluginCfgProps.pluginName);
if (issueListForPlugin.length > 0) {
this.impLogger.warn("addAllPluginsToHostCli: Issues for plugin = '" +
nextPluginCfgProps.pluginName + "':\n" +
JSON.stringify(issueListForPlugin, null, 2));
} else {
this.impLogger.info("addAllPluginsToHostCli: Plugin = '" +
nextPluginCfgProps.pluginName +
"' was successfully validated with no issues."
);
}
}
}
// __________________________________________________________________________
/**
* Loads the configuration properties of each plugin. The configuration
* information is used when overriding a piece of the imperative
* infrastructure with a plugin's capability, when validating each plugin,
* and when adding each plugin's commands to the CLI command tree.
* Errors are recorded in PluginIssues.
*/
public loadAllPluginCfgProps(): void {
// Initialize the plugin.json file if needed
// TODO Skip creation of PMF_ROOT directory once it is deprecated by team config
if (!existsSync(this.pmfConst.PLUGIN_JSON)) {
if (!existsSync(this.pmfConst.PMF_ROOT)) {
this.impLogger.debug("Creating PMF_ROOT directory");
IO.mkdirp(this.pmfConst.PMF_ROOT);
}
this.impLogger.debug("Creating PLUGIN_JSON file");
writeFileSync(this.pmfConst.PLUGIN_JSON, {});
}
const loadedOverrides: { [key: string]: IImperativeOverrides } = {};
// iterate through all of our installed plugins
for (const nextPluginNm of Object.keys(this.pluginIssues.getInstalledPlugins())) {
const nextPluginCfgProps = this.loadPluginCfgProps(nextPluginNm);
if (nextPluginCfgProps) {
this.mAllPluginCfgProps.push(nextPluginCfgProps);
// Remember the overrides as a key of our temporary object
loadedOverrides[nextPluginNm] = nextPluginCfgProps.impConfig.overrides;
this.impLogger.trace("Next plugin's configuration properties:\n" +
JSON.stringify(nextPluginCfgProps, null, 2)
);
} else {
this.impLogger.error(
"loadAllPluginCfgProps: Unable to load the configuration for the plug-in named '" +
nextPluginNm + "' The plug-in was not added to the host CLI."
);
}
}
// Loop through each overrides setting here. Setting is an override that we are modifying while
// plugin is the pluginName from which to get the setting. This is probably the ugliest piece
// of code that I have ever written :/
for (const [setting, pluginName] of Object.entries(AppSettings.instance.getNamespace("overrides"))) {
if (pluginName !== false && pluginName !== ImperativeConfig.instance.hostPackageName) {
Logger.getImperativeLogger().debug(
`PluginOverride: Attempting to overwrite "${setting}" with value provided by plugin "${pluginName}"`
);
if (!Object.prototype.hasOwnProperty.call(loadedOverrides, pluginName)) {
// the plugin name specified in our settings is not available
const overrideErrMsg = `You attempted to override the "${setting}" setting ` +
`with a plugin named "${pluginName}" that is not installed and loadable.` +
`\nWe will use a "${setting}" that purposely fails until you reconfigure.`;
Logger.getImperativeLogger().error(overrideErrMsg);
/* We need to assign a class (which always shows an error when
* the CLI tries to use credentials) into the current override setting.
* We also need to embed our error message into that class. We cannot
* create a new object from a class and pass the error into its
* constructor, because the CredentialManagerFactory takes a class and
* it calls the constructor of our supplied class. Thus we need an
* anonymous class so that we can access our 'overrideErrMsg' variable.
* Our trick is that we simply throw an error in the constructor
* of our anonymous class. The CredentialManagerFactory catches
* our error, and places it into its InvalidCredentialManager,
* which in turn shows our error every time the CLI tries to use
* credentials. Finally since lint complains about more than one
* class in a file, we have to temporarily turn off that lint error.
*/
(this.mPluginOverrides as any)[setting] = class {
constructor() {
throw overrideErrMsg;
}
};
continue;
}
// Like the cli the overrides can be the actual class or the string path
let loadedSetting: string | object = (loadedOverrides[pluginName as any] as any)[setting];
// If the overrides loaded is a string path, just resolve it here since it would be much
// to do so in the overrides loader.
if (typeof loadedSetting === "string") {
let pathToPluginOverride = loadedSetting;
try {
if (!isAbsolute(pathToPluginOverride)) {
Logger.getImperativeLogger().trace(`PluginOverride: Resolving ${pathToPluginOverride} in ${pluginName}`);
// This is actually kind of disgusting. What is happening is that we are getting the
// entry file of the plugin using require.resolve since the modules loaded are different
// when using node or ts-node. This require gets us the index.js/index.ts file that
// the plugin defines. So we then cd up a directory and resolve the path relative
// to the plugin entry file.
pathToPluginOverride = join(
require.resolve(this.formPluginRuntimePath(pluginName as any)),
"../",
pathToPluginOverride
);
}
loadedSetting = require(pathToPluginOverride);
Logger.getImperativeLogger().info(`PluginOverride: Overrode "${setting}" ` +
`with "${pathToPluginOverride}" from plugin "${pluginName}"`);
} catch (requireError) {
const overrideErrMsg = `Unable to override "${setting}" with "${pathToPluginOverride}" ` +
`from plugin "${pluginName}"\n` + "Reason = " + requireError.message +
`\nWe will use a "${setting}" that purposely fails until you reconfigure.`;
PluginIssues.instance.recordIssue(pluginName as any, IssueSeverity.OVER_ERROR, overrideErrMsg);
// See the big block comment above about using an anonymous class.
(this.mPluginOverrides as any)[setting] = class {
constructor() {
throw overrideErrMsg;
}
};
continue;
}
}
// Save the setting in the mPluginsOverrides object that was stored previously in
// the loadedOverrides object as the plugin name.
(this.mPluginOverrides as any)[setting] = loadedSetting;
}
}
this.impLogger.info("All plugin configurations have been loaded. Details at trace level of logging.");
}
// __________________________________________________________________________
/**
* Produces a function that requires a module from a plugin using a relative
* path name from the plugin's root to the module. Used as a callback function
* from the ConfigurationLoader to load configuration handlers.
*
* @param {string} pluginName - The name of the plugin/module to load.
*
* @returns {function} - The method responsible for requiring the module
*/
public requirePluginModuleCallback(pluginName: string): ((relativePath: string) => any) {
return (relativePath: string) => {
const pluginModuleRuntimePath = this.formPluginRuntimePath(pluginName, relativePath);
try {
return require(pluginModuleRuntimePath);
} catch (requireError) {
PluginIssues.instance.recordIssue(pluginName, IssueSeverity.CMD_ERROR,
"Unable to load the following module for plug-in '" +
pluginName + "' :\n" + pluginModuleRuntimePath + "\n" +
"Reason = " + requireError.message
);
return "{}";
}
};
}
// __________________________________________________________________________
/**
* Add the specified plugin to the imperative command tree.
*
* @param {IPluginCfgProps} pluginCfgProps - The configuration properties for this plugin
*/
private addPluginToHostCli(pluginCfgProps: IPluginCfgProps): void {
const timingApi = PerfTiming.api;
if (PerfTiming.isEnabled) {
// Marks point START
timingApi.mark("START_ADD_PLUGIN");
}
/* Form a top-level command group for this plugin.
* Resolve all means of command definition into the pluginCmdGroup.children
*/
let pluginCmdGroup: ICommandDefinition = null;
try {
pluginCmdGroup = {
name: pluginCfgProps.impConfig.name,
description: pluginCfgProps.impConfig.rootCommandDescription,
type: "group",
children: DefinitionTreeResolver.combineAllCmdDefs(
this.formPluginRuntimePath(pluginCfgProps.pluginName, "./lib"),
pluginCfgProps.impConfig.definitions, pluginCfgProps.impConfig.commandModuleGlobs,
ImperativeConfig.instance.loadedConfig.baseProfile != null
)
};
/**
* Fill in the optional aliases and summary fields,
* if specified.
*/
if (pluginCfgProps.impConfig.pluginSummary != null) {
this.impLogger.debug("Adding summary from pluginSummary field of configuration");
pluginCmdGroup.summary = pluginCfgProps.impConfig.pluginSummary;
}
if (pluginCfgProps.impConfig.pluginAliases != null) {
this.impLogger.debug("Adding aliases from pluginAliases field of configuration");
pluginCmdGroup.aliases = pluginCfgProps.impConfig.pluginAliases;
}
}
catch (impErr) {
const errMsg = "Failed to combine command definitions. Reason = " + impErr.message;
this.impLogger.error("addPluginToHostCli: DefinitionTreeResolver.combineAllCmdDefs: " + errMsg);
this.pluginIssues.recordIssue(pluginCfgProps.pluginName, IssueSeverity.CMD_ERROR, errMsg);
return;
}
// validate the plugin's configuration
if (this.validatePlugin(pluginCfgProps, pluginCmdGroup) === false) {
this.impLogger.error("addPluginToHostCli: The plug-in named '" + pluginCfgProps.pluginName +
"' failed validation and was not added to the host CLI app.");
return;
}
if (pluginCmdGroup.children.length <= 0) {
this.impLogger.info("addPluginToHostCli: The plugin '" +
pluginCfgProps.pluginName +
"' has no commands, so no new commands will be added to the host CLI app."
);
} else {
// add the new plugin group into the imperative command tree
this.impLogger.info("addPluginToHostCli: Adding commands for plug-in '" +
pluginCfgProps.pluginName + "' to CLI command tree. Plugin command details at trace level of logging."
);
this.impLogger.trace("addPluginToHostCli: Commands for plugin = '" +
pluginCfgProps.pluginName + "':\n" + JSON.stringify(pluginCmdGroup, null, 2)
);
if (!this.addCmdGrpToResolvedCliCmdTree(pluginCfgProps.pluginName, pluginCmdGroup)) {
return;
}
}
// add the profiles for this plugin to our imperative config object
if (pluginCfgProps.impConfig.profiles && pluginCfgProps.impConfig.profiles.length > 0) {
this.impLogger.trace("addPluginToHostCli: Adding these profiles for plug-in = '" +
pluginCfgProps.pluginName + "':\n" +
JSON.stringify(pluginCfgProps.impConfig.profiles, null, 2)
);
try {
UpdateImpConfig.addProfiles(pluginCfgProps.impConfig.profiles);
}
catch (impErr) {
const errMsg = "Failed to add profiles for the plug-in = '" + pluginCfgProps.pluginName +
"'.\nReason = " + impErr.message +
"\nBecause of profile error, removing commands for this plug-in";
this.impLogger.error("addPluginToHostCli: " + errMsg);
this.pluginIssues.recordIssue(pluginCfgProps.pluginName, IssueSeverity.CMD_ERROR, errMsg);
this.removeCmdGrpFromResolvedCliCmdTree(pluginCmdGroup);
}
}
if (PerfTiming.isEnabled) {
// Marks point END
timingApi.mark("END_ADD_PLUGIN");
timingApi.measure("Add plugin completed: " + pluginCfgProps.impConfig.name, "START_ADD_PLUGIN", "END_ADD_PLUGIN");
}
}
// __________________________________________________________________________
/**
* Add a new command group into the host CLI's resolved command tree.
* We had to wait until the host CLI was resolved, so that we could check for
* name conflicts. So each plugin's commands are added to the host CLI
* command tree after both have been resolved.
*
* @param {string} pluginName - the name of the plugin to initialize
*
* @param {ICommandDefinition} cmdDefToAdd - command definition group to to be added.
*
* @returns True upon success. False upon error, and errors are recorded in pluginIssues.
*/
private addCmdGrpToResolvedCliCmdTree(pluginName: string, cmdDefToAdd: ICommandDefinition): boolean {
if (this.resolvedCliCmdTree == null) {
const errMsg = "The resolved command tree was null. " +
"Imperative should have created an empty command definition array.";
this.impLogger.error("addCmdGrpToResolvedCliCmdTree: While adding plugin = '" +
pluginName + "', " + errMsg);
this.pluginIssues.recordIssue(pluginName, IssueSeverity.CMD_ERROR, errMsg);
return false;
}
if (this.resolvedCliCmdTree.children == null) {
const errMsg = "The resolved command tree children was null. " +
"Imperative should have created an empty children array.";
this.impLogger.error("addCmdGrpToResolvedCliCmdTree: While adding plugin = '" +
pluginName + "', " + errMsg);
this.pluginIssues.recordIssue(pluginName, IssueSeverity.CMD_ERROR, errMsg);
return false;
}
const cmdDefInx = this.resolvedCliCmdTree.children.findIndex((existingCmdDef: ICommandDefinition) => {
return existingCmdDef.name === cmdDefToAdd.name;
});
if (cmdDefInx > -1) {
const errMsg = "The command group = '" + cmdDefToAdd.name +
"' already exists. Plugin management should have already rejected this plugin.";
this.impLogger.error("addCmdGrpToResolvedCliCmdTree: " + errMsg);
this.pluginIssues.recordIssue(pluginName, IssueSeverity.CMD_ERROR, errMsg);
return false;
}
this.impLogger.debug("Adding definition = '" + cmdDefToAdd.name + "' to the resolved command tree.");
this.resolvedCliCmdTree.children.push(cmdDefToAdd);
return true;
}
// __________________________________________________________________________
/**
* Compare the version of a plugin version property with a version property
* of its base CLI.
*
* If the versions do not intersect (according so semver rules), then a
* PluginIssue is recorded.
*
* @param pluginName - The name of the plugin.
*
* @param pluginVerPropNm - The name of the plugin property containing a version.
*
* @param pluginVerVal - value of the plugin's version.
*
* @param cliVerPropNm - The name of the base CLI property containing a version.
*
* @param cliVerVal - value of the base CLI's version.
*
*/
private comparePluginVersionToCli(
pluginName: string,
pluginVerPropNm: string,
pluginVerVal: string,
cliVerPropNm: string,
cliVerVal: string
): void {
const cliCmdName = ImperativeConfig.instance.rootCommandName;
try {
if (!this.semver.intersects(cliVerVal, pluginVerVal, false)) {
this.pluginIssues.recordIssue(pluginName, IssueSeverity.WARNING,
"The version value (" + pluginVerVal + ") of the plugin's '" +
pluginVerPropNm + "' property is incompatible with the version value (" +
cliVerVal + ") of the " + cliCmdName + " command's '" +
cliVerPropNm + "' property."
);
}
} catch (semverExcept) {
PluginIssues.instance.recordIssue(pluginName, IssueSeverity.WARNING,
"Failed to compare the version value (" +
pluginVerVal + ") of the plugin's '" + pluginVerPropNm +
"' property with the version value (" + cliVerVal +
") of the " + cliCmdName + " command's '" + cliVerPropNm + "' property.\n" +
"This can occur when one of the specified values is not a valid version string.\n" +
"Reported reason = " + semverExcept.message
);
}
}
// __________________________________________________________________________
/**
* Get the package name of our base CLI.
*
* @returns The CLI package name contained in the package.json 'name' property.
*/
private getCliPkgName(): string {
const cliPackageJson: any = ImperativeConfig.instance.callerPackageJson;
if (!Object.prototype.hasOwnProperty.call(cliPackageJson, this.npmPkgNmProp)) {
return "NoNameInCliPkgJson";
}
return cliPackageJson[this.npmPkgNmProp];
}
// __________________________________________________________________________
/**
* Remove a command group that was previously added.
* We remove a command group if we discover errors after
* adding the command group.
*
* @param {ICommandDefinition} cmdDefToRemove - command definition to be removed.
*/
private removeCmdGrpFromResolvedCliCmdTree(cmdDefToRemove: ICommandDefinition): void {
if (this.resolvedCliCmdTree &&
this.resolvedCliCmdTree.children &&
this.resolvedCliCmdTree.children.length > 0
) {
const cmdDefInx = this.resolvedCliCmdTree.children.findIndex((existingCmdDef: ICommandDefinition) => {
return existingCmdDef.name === cmdDefToRemove.name;
});
if (cmdDefInx > -1) {
this.impLogger.debug("Removing definition = '" + cmdDefToRemove.name + "'");
this.resolvedCliCmdTree.children.splice(cmdDefInx, 1);
}
}
}
// __________________________________________________________________________
/**
* Does the supplied pluginGroupNm match an existing top-level
* name or alias in the imperative command tree?
* If a conflict occurs, plugIssues.doesPluginHaveError() will return true.
*
* @param {string} pluginName - The name of the plugin that we are checking.
*
* @param {ICommandDefinition} pluginGroupDefinition - A plugin's command group definition..
*
* @param {ICommandDefinition} cmdTreeDef - A top-level command tree
* definition against which we compare the supplied
* pluginGroupNm. It is typically the imperative command tree.
*
* @returns {[boolean, string]} - {hasConflict, message} - hasConflict: True when we found a conflict.
* False when find no conflicts.
* message: the message describing the conflict
*/
private conflictingNameOrAlias(
pluginName: string,
pluginGroupDefinition: ICommandDefinition,
cmdTreeDef: ICommandDefinition
): { hasConflict: boolean, message: string } {
const pluginGroupNm: string = pluginGroupDefinition.name;
/* Confirm that pluginGroupNm is not an existing top-level
* group or command in the imperative command tree
* and confirm that none of the plugin aliases match any command names
*/
if (pluginGroupNm.toLowerCase() === cmdTreeDef.name.toLowerCase()) {
const conflictMessage = this.impLogger.error("The plugin named '%s' attempted to add a group of commands" +
" with the name '%s'" +
". Your base application already contains a group with the name '%s'.", pluginGroupNm, pluginGroupDefinition.name,
cmdTreeDef.name);
return { hasConflict: true, message: conflictMessage };
}
if (pluginGroupDefinition.aliases != null) {
for (const pluginAlias of pluginGroupDefinition.aliases) {
if (pluginAlias.toLowerCase() === cmdTreeDef.name.toLowerCase()) {
const conflictMessage = this.impLogger.error("The plugin named '%s' attempted to add a group of commands" +
" with the alias '%s' " +
". Your base application already contains a group with the name '%s'.", pluginGroupNm, pluginAlias,
cmdTreeDef.name);
return { hasConflict: true, message: conflictMessage };
}
}
}
/* Confirm that pluginGroupNm is not an existing top-level
* alias in the command tree definition.
*/
if (Object.prototype.hasOwnProperty.call(cmdTreeDef, "aliases")) {
for (const nextAliasToTest of cmdTreeDef.aliases) {
// if the plugin name matches an alias of the definition tree
if (pluginGroupNm.toLowerCase() === nextAliasToTest.toLowerCase()) {
const conflictMessage = this.impLogger.error("The plugin attempted to add a group of commands with the name '%s' " +
". Your base application already contains a group with an alias '%s'.", pluginGroupNm, nextAliasToTest,
cmdTreeDef.name);
return { hasConflict: true, message: conflictMessage };
}
if (pluginGroupDefinition.aliases != null) {
for (const pluginAlias of pluginGroupDefinition.aliases) {
// if an alias of the plugin matches an alias of hte definition tree
if (pluginAlias.toLowerCase() === nextAliasToTest.toLowerCase()) {
const conflictMessage = this.impLogger.error("The plugin named '%s' attempted to add a " +
"group of command with the alias '%s', which conflicts with " +
"another alias of the same name for group '%s'.", pluginGroupDefinition.name, pluginAlias,
cmdTreeDef.name);
return { hasConflict: true, message: conflictMessage };
}
}
}
}
}
// no conflict if we got this far
return { hasConflict: false, message: undefined };
}
// __________________________________________________________________________
/**
* Form the absolute path to a runtime file for a plugin from a path name
* that is relative to the plugin's root directory (where its package.json lives).
*
* @param {string} pluginName - The name of the plugin.
*
* @param {string} relativePath - A relative path from plugin's root.
* Typically supplied as ./lib/blah/blah/blah.
* If not supplied, (or supplied as an an empty string,
* the result will be a path to
* <The_PLUGIN_NODE_MODULE_LOCATION_ForTheBaseCLI>/<pluginName>.
* If an absolute path is supplied, it is returned exactly as supplied.
*
* @returns {string} - The absolute path to the file.
*/
private formPluginRuntimePath(
pluginName: string,
relativePath: string = ""
): string {
// Attempt to find the node_modules that contains the plugin
let pluginRuntimeDir = null;
for (const location of this.pmfConst.PLUGIN_NODE_MODULE_LOCATION) {
pluginRuntimeDir = join(location, pluginName);
if (existsSync(pluginRuntimeDir)) {
break;
}
}
if (relativePath.length === 0) {
return pluginRuntimeDir;
}
/* If the relative path is already absolute, do not place our
* plugin's runtime location in front of the supplied path.
*/
if (isAbsolute(relativePath)) {
return relativePath;
}
return join(pluginRuntimeDir, relativePath);
}
// __________________________________________________________________________
/**
* Read a plugin's configuration properties. The properties are obtained
* from the plugins package.json file, including it's imperative property.
*
* @param {string} pluginName - the name of the plugin
*
* @returns {IPluginCfgProps} - The plugin's configuration properties
* or null if the plugin's configuration cannot be retrieved.
* Errors are recorded in PluginIssues.
*/
private loadPluginCfgProps(pluginName: string): IPluginCfgProps {
const pluginCfgProps: IPluginCfgProps = {
pluginName,
npmPackageName: "PluginHasNoNpmPkgName",
impConfig: {},
cliDependency: {
peerDepName: this.pmfConst.CLI_CORE_PKG_NAME,
peerDepVer: this.noPeerDependency
},
impDependency: {
peerDepName: this.pmfConst.IMPERATIVE_PKG_NAME,
peerDepVer: this.noPeerDependency
}
};
this.impLogger.trace("loadPluginCfgProps: Reading configuration for plugin = '" +
pluginName + "' from its package.json file.");
// this is the starting point for reporting plugin issues, so clear old ones
this.pluginIssues.removeIssuesForPlugin(pluginName);
// confirm that we can find the path to the plugin node_module
const pluginRunTimeRootPath = this.formPluginRuntimePath(pluginName);
if (!existsSync(pluginRunTimeRootPath)) {
this.pluginIssues.recordIssue(pluginName, IssueSeverity.CFG_ERROR,
"The path to the plugin does not exist: " + pluginRunTimeRootPath);
return null;
}
// confirm that we can find the path to the plugin's package.json
const pluginPkgJsonPathNm = join(pluginRunTimeRootPath, "package.json");
if (!existsSync(pluginPkgJsonPathNm)) {
this.pluginIssues.recordIssue(pluginName, IssueSeverity.CFG_ERROR,
"Configuration file does not exist: '" + pluginPkgJsonPathNm + "'");
return null;
}
// read package.json
let pkgJsonData: any = null;
try {
pkgJsonData = readFileSync(pluginPkgJsonPathNm);
}
catch (ioErr) {
this.pluginIssues.recordIssue(pluginName, IssueSeverity.CFG_ERROR,
"Cannot read '" + pluginPkgJsonPathNm +
"' Reason = " + ioErr.message);
return null;
}
// extract the plugin npm package name property for later use in class
if (Object.prototype.hasOwnProperty.call(pkgJsonData, this.npmPkgNmProp)) {
pluginCfgProps.npmPackageName = pkgJsonData[this.npmPkgNmProp];
}
// use the CLI's package name as a peer dependency in the plugin
const cliPkgName = this.getCliPkgName();
const cliCmdName = ImperativeConfig.instance.rootCommandName;
if (cliPkgName === "NoNameInCliPkgJson") {
this.pluginIssues.recordIssue(pluginName, IssueSeverity.WARNING,
"The property '" + this.npmPkgNmProp +
"' does not exist in the package.json file of the '" +
cliCmdName + "' project. Defaulting to " +
"'" + pluginCfgProps.cliDependency.peerDepName + "',"
);
} else {
pluginCfgProps.cliDependency.peerDepName = cliPkgName;
}
// confirm that the peerDependencies property exists in plugin's package.json
const peerDepPropNm = "peerDependencies";
if (Object.prototype.hasOwnProperty.call(pkgJsonData, peerDepPropNm)) {
// get the version of the host CLI dependency for this plugin
if (Object.prototype.hasOwnProperty.call(pkgJsonData[peerDepPropNm], pluginCfgProps.cliDependency.peerDepName)) {
pluginCfgProps.cliDependency.peerDepVer =
pkgJsonData[peerDepPropNm][pluginCfgProps.cliDependency.peerDepName];
}
// get the version of the imperative dependency for this plugin
if (Object.prototype.hasOwnProperty.call(pkgJsonData[peerDepPropNm], pluginCfgProps.impDependency.peerDepName)) {
pluginCfgProps.impDependency.peerDepVer =
pkgJsonData[peerDepPropNm][pluginCfgProps.impDependency.peerDepName];
} else {
this.pluginIssues.recordIssue(pluginName, IssueSeverity.WARNING,
"The property '" + pluginCfgProps.impDependency.peerDepName +
"' does not exist within the '" + peerDepPropNm +
"' property in the file '" + pluginPkgJsonPathNm + "'."
);
}
} else {
this.pluginIssues.recordIssue(pluginName, IssueSeverity.WARNING,
"Your '" + this.pmfConst.NPM_NAMESPACE +
"' dependencies must be contained within a '" + peerDepPropNm +
"' property. That property does not exist in the file '" +
pluginPkgJsonPathNm + "'."
);
}
// extract the imperative property
if (!Object.prototype.hasOwnProperty.call(pkgJsonData, this.impConfigPropNm)) {
this.pluginIssues.recordIssue(pluginName, IssueSeverity.CFG_ERROR,
"The required property '" + this.impConfigPropNm +
"' does not exist in file '" + pluginPkgJsonPathNm + "'.");
return null;
}
// use the core imperative loader because it will load config modules
const timingApi = PerfTiming.api;
if (PerfTiming.isEnabled) {
// Marks point START
timingApi.mark("START_LOAD_PLUGIN");
}
let pluginConfig: IImperativeConfig;
try {
pluginConfig = ConfigurationLoader.load(
null, pkgJsonData, this.requirePluginModuleCallback(pluginName)
);
}
catch (impError) {
this.pluginIssues.recordIssue(pluginName, IssueSeverity.CFG_ERROR,
"Failed to load the plugin's configuration from:\n" +
pluginPkgJsonPathNm +
"\nReason = " + impError.message
);
return null;
}
if (PerfTiming.isEnabled) {
// Marks point END
timingApi.mark("END_LOAD_PLUGIN");
timingApi.measure("Load plugin completed", "START_LOAD_PLUGIN", "END_LOAD_PLUGIN");
}
pluginCfgProps.impConfig = pluginConfig;
return pluginCfgProps;
}
// __________________________________________________________________________
/**
* Validates that the semver range strings specified by the plugin for
* versions of the imperative framework and host CLI program are compatible
* with those specified in the host CLI.
*
* Both range strings come from the package.json files of the plugin and the
* hosting CLI. We consider the version ranges to be compatible if the two
* ranges intersect. This should allow npm to download one common version
* of core and of imperative to be owned by the base CLI and shared by the plugin.
*
* Any errors are recorded in PluginIssues.
*
* @param {IPluginCfgProps} pluginCfgProps - The configuration properties for this plugin
*/
private validatePeerDepVersions(pluginCfgProps: IPluginCfgProps): void {
// get the name of the base CLI for error messages
const cliCmdName = ImperativeConfig.instance.rootCommandName;
const cliPackageJson: any = ImperativeConfig.instance.callerPackageJson;
let cliVerPropName = "version";
// compare the plugin's requested CLI version with the CLI's actual version
if (pluginCfgProps.cliDependency.peerDepVer !== this.noPeerDependency) {
if (Object.prototype.hasOwnProperty.call(cliPackageJson, cliVerPropName)) {
this.comparePluginVersionToCli(
pluginCfgProps.pluginName,
pluginCfgProps.cliDependency.peerDepName,
pluginCfgProps.cliDependency.peerDepVer,
cliVerPropName,
cliPackageJson[cliVerPropName]
);
} else {
this.pluginIssues.recordIssue(pluginCfgProps.pluginName, IssueSeverity.CFG_ERROR,
"The property '" + cliVerPropName +
"' does not exist within the package.json file of the '" +
cliCmdName + "' project."
);
}
}
// compare the plugin's requested imperative version with the CLI's actual version
if (pluginCfgProps.impDependency.peerDepVer !== this.noPeerDependency) {
/* The CLI's imperative version is within its dependencies property
* under the same property name as the plugin uses.
*/
const cliDepPropName = "dependencies";
cliVerPropName = pluginCfgProps.impDependency.peerDepName;
if (Object.prototype.hasOwnProperty.call(cliPackageJson, cliDepPropName)) {
if (Object.prototype.hasOwnProperty.call(cliPackageJson[cliDepPropName], cliVerPropName)) {
this.comparePluginVersionToCli(
pluginCfgProps.pluginName,
pluginCfgProps.impDependency.peerDepName,
pluginCfgProps.impDependency.peerDepVer,
cliVerPropName,
cliPackageJson[cliDepPropName][cliVerPropName]
);
} else {
this.pluginIssues.recordIssue(pluginCfgProps.pluginName, IssueSeverity.CFG_ERROR,
"The property '" + cliVerPropName +
"' does not exist within the '" + cliDepPropName +
"' property in the package.json file of the '" +
cliCmdName + "' project."
);
}
} else {
this.pluginIssues.recordIssue(pluginCfgProps.pluginName, IssueSeverity.CFG_ERROR,
"The property '" + cliDepPropName +
"' does not exist in the package.json file of the '" +
cliCmdName + "' project."
);
}
}
}
// __________________________________________________________________________
/**
* Validate the plugin.
*
* @param {IPluginCfgProps} pluginCfgProps - The configuration properties for this plugin
*
* @param {ICommandDefinition} pluginCmdGroup - The command group to be added
* for this plugin, with all commands resolved into its children property.
*
* @returns {boolean} - True if valid. False otherwise.
* PluginIssues contains the set of issues.
*/
private validatePlugin(
pluginCfgProps: IPluginCfgProps,
pluginCmdGroup: ICommandDefinition
): boolean {
if (JsUtils.isObjEmpty(pluginCfgProps.impConfig)) {
// without a config object, we can do no further validation
this.pluginIssues.recordIssue(pluginCfgProps.pluginName, IssueSeverity.CFG_ERROR,
"The plugin's configuration is empty.");
return false;
}
this.impLogger.info("validatePlugin: Validating plugin '" +
pluginCfgProps.pluginName + "'. Plugin config details at trace level of logging."
);
this.impLogger.trace("validatePlugin: Config for plugin '" +
pluginCfgProps.pluginName + "':\n" +
JSON.stringify(pluginCfgProps.impConfig, null, 2)
);
// is there an imperative.name property?
if (!Object.prototype.hasOwnProperty.call(pluginCfgProps.impConfig, "name")) {
// can we default to the npm package name?
if (pluginCfgProps.npmPackageName === "PluginHasNoNpmPkgName" ||
pluginCfgProps.npmPackageName.length === 0) {
this.pluginIssues.recordIssue(pluginCfgProps.pluginName, IssueSeverity.CFG_ERROR,
"The plugin's configuration does not contain an '" +
this.impConfigPropNm + ".name' property, or an npm package 'name' property in package.json.");
} else {
pluginCfgProps.impConfig.name = pluginCfgProps.npmPackageName;
}
}
/* Confirm that the plugin group name does not conflict with another
* top-level item in the imperative command tree.
*/
if (Object.prototype.hasOwnProperty.call(pluginCfgProps.impConfig, "name")) {
for (const nextImpCmdDef of this.resolvedCliCmdTree.children) {
const conflictAndMessage = this.conflictingNameOrAlias(pluginCfgProps.pluginName,
pluginCmdGroup, nextImpCmdDef);