-
Notifications
You must be signed in to change notification settings - Fork 975
/
Copy pathpluginDependencies.js
271 lines (234 loc) · 8.42 KB
/
pluginDependencies.js
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
var log = require('../api/utils/log.js'),
_ = require('lodash'),
fs = require('fs');
const CLY_ROOT = "___CLY_ROOT___";
/**
* Decorates option object
*
* @param {object} options Input options object
* @returns {object} Output options object
*/
function getOptions(options) {
options = options || {};
if (options.env === "cli") {
options.logger = {
e: console.log,
i: console.log,
w: console.log
};
}
else {
options.logger = options.logger || log('plugins:dependencies');
}
options.discoveryStrategy = options.discoveryStrategy || "disableChildren";
return options;
}
/**
* Extends the dependencies graph with either ancestors or descendants of the given plugin
*
* @param {object} _graph Dependencies graph
* @param {String} code Plugin id
* @param {String} direction Relative direction (up|down)
* @returns {undefined} nothing
*/
function discoverRelativePlugins(_graph, code, direction) {
if (!Object.prototype.hasOwnProperty.call(_graph, code)) {
return [];
}
direction = direction || "up";
if (Object.prototype.hasOwnProperty.call(_graph[code], direction)) {
return _graph[code][direction];
}
var queue = [code],
visited = {},
relativeType = "parents";
if (direction !== "up") {
relativeType = "children";
}
while (queue.length > 0) {
var current = queue.pop();
if (Object.prototype.hasOwnProperty.call(visited, current)) {
continue;
}
if (!_graph[current]) {
visited[current] = 1;
continue;
}
for (var item in _graph[current][relativeType]) {
queue.push(item);
}
visited[current] = 1;
}
var relatives = [];
delete visited[CLY_ROOT];
for (var itemCode in visited) {
if (itemCode !== code) {
relatives.push(itemCode);
}
}
_graph[code][direction] = relatives;
}
/**
* Returns a dependency graph of the provided plugins list.
*
* If the discovery strategy is "disableChildren", then the returned graph will be a subset of the original dependencies.
*
* For instance,
* Let A <- B (plugin B depends on Plugin A);
* If A is disabled, A won't be included in the graph, but only B will be. This will cause an unmet dependency error,
* and reported via the errors object.
*
* In "enableParents" case, however, Plugin A (parent) will be enabled, so the graph will be complete.
*
* This function may report different types of errors; which are generally caused by cyclic dependencies, absent plugins, etc.
*
* @param {Array} plugins We use this array of plugin names to create dependency graph.
* @param {object} options Options object
* @returns {Object} Returns an object with dpcs and errors fields
*/
function getDependencies(plugins, options) {
var errors = {},
dpcs = {
[CLY_ROOT]: {
children: {}
}
};
var {logger, discoveryStrategy} = getOptions(options);
if (["disableChildren", "enableParents"].indexOf(discoveryStrategy) === -1) {
logger.e(`Invalid discoveryStrategy (${discoveryStrategy}) for analyzeAndFixDependencies.`);
return null;
}
var _loadPluginMeta = function(pName) {
try {
var pkgInfo = require("./" + pName + "/package.json");
var currentDpcs = pkgInfo.cly_dependencies || {};
if (Object.keys(currentDpcs).length === 0) {
// Zero-dependency plugins are linked to an imaginary root, where traverse starts at
currentDpcs[CLY_ROOT] = true;
}
dpcs[pName] = {parents: currentDpcs, children: {}};
return true;
}
catch (ex) {
errors[pName] = {"reason": "not_found"};
return false;
}
};
var loadQueue = JSON.parse(JSON.stringify(plugins));
while (loadQueue.length > 0) {
var name = loadQueue.shift();
if (!Object.prototype.hasOwnProperty.call(dpcs, name)) {
if (_loadPluginMeta(name) && discoveryStrategy === "enableParents") {
// Register parents before error analysis
for (var parentName in dpcs[name].parents) {
loadQueue.push(parentName);
}
}
}
}
for (var dname in dpcs) {
for (var pname in dpcs[dname].parents) {
if (!Object.prototype.hasOwnProperty.call(dpcs, pname)) {
if (discoveryStrategy === "enableParents") {
// If dependency record is not found even at this point, it means dependency doesn't exist under plugins.
errors[dname] = {"reason": "parent_not_found", "cly_dependencies": dpcs[dname].parents};
}
else { //else if (discoveryStrategy === "disableChildren") {
errors[dname] = {"reason": "child_disabled", "cly_dependencies": dpcs[dname].parents};
}
break;
}
else {
dpcs[pname].children[dname] = true;
}
}
}
for (var center in dpcs) {
discoverRelativePlugins(dpcs, center, "up");
discoverRelativePlugins(dpcs, center, "down");
}
return {dpcs, errors};
}
/**
* Fixes the order of plugins, enables/disables when there are broken dependencies
*
* @param {Array} plugins Plugin List
* @param {Object} options Options object
* @returns {Array} Fixed list of the plugins
*/
var getFixedPluginList = function(plugins, options) {
options = getOptions(options);
var {dpcs, errors} = getDependencies(plugins, options),
fixedPlugins = [],
visited = new Set(),
logger = options.logger;
var _traverse = function(pluginName) {
if (visited.has(pluginName)) {
return;
}
for (var parentName in dpcs[pluginName].parents) {
if (!visited.has(parentName)) {
// Plugin has to wait for another parent(s) to be added first.
return;
}
}
// If all parents were added before, there is nothing blocking plugin itself.
// So we add it too.
if (pluginName !== CLY_ROOT) {
fixedPlugins.push(pluginName);
}
visited.add(pluginName);
// As we add the new plugin, we can traverse its children.
for (var childName in dpcs[pluginName].children) {
_traverse(childName);
}
};
_traverse(CLY_ROOT);
for (var plugin in dpcs) {
if (plugin === CLY_ROOT) {
continue;
}
if (fixedPlugins.indexOf(plugin) === -1 && !Object.prototype.hasOwnProperty.call(errors, plugin)) {
errors[plugin] = {"reason": "parent_enable_failed", "cly_dependencies": dpcs[plugin].parents};
}
}
if (fixedPlugins.indexOf('dashboards') !== -1) {
fixedPlugins.splice(fixedPlugins.indexOf('dashboards'), 1);
fixedPlugins.push('dashboards');
}
if (Object.keys(errors).length > 0) {
logger.e("Loaded plugins:\n", fixedPlugins);
logger.e("Safe loader couldn't load following plugins:\n", errors);
}
else {
logger.i("Loaded successfully.");
}
if (!_.isEqual(plugins, fixedPlugins)) {
logger.w("Plugin list has changed.");
logger.w("Old Plugins >>>", JSON.stringify(plugins));
logger.w("New Plugins >>>", JSON.stringify(fixedPlugins));
if (options.overwrite) {
if (fs.existsSync(options.overwrite)) {
logger.w("The old version will be overwritten.");
try {
fs.renameSync(options.overwrite, options.overwrite + ".autobkp");
try {
fs.writeFileSync(options.overwrite, JSON.stringify(fixedPlugins));
}
catch (newWriteErr) {
logger.e(`Fixed ${options.overwrite} couldn't be written. Please check the original file.`, newWriteErr);
}
}
catch (renameErr) {
logger.e(`Old ${options.overwrite} couldn't be renamed. Overwrite was aborted.`, renameErr);
}
}
}
}
else {
logger.i("Plugin list is OK.");
}
return fixedPlugins;
};
exports.getDependencies = getDependencies;
exports.getFixedPluginList = getFixedPluginList;