UNPKG

8.18 kBJavaScriptView Raw
1const fs = require('fs');
2const os = require('os');
3const path = require('path');
4const { spawn } = require('child_process');
5const util = require('util');
6const YAWN = require('yawn-yaml/cjs');
7const readFile = util.promisify(fs.readFile);
8const writeFile = util.promisify(fs.writeFile);
9
10class PluginInstaller {
11 constructor (options) {
12 options = options || {};
13 this.packageName = options.packageName || null;
14 this.pluginManifest = options.pluginManifest || null;
15 this.config = options.config;
16 }
17
18 static get PACKAGE_PREFIX () {
19 return 'express-gateway-plugin-';
20 }
21
22 static create (options) {
23 return new PluginInstaller(options);
24 }
25
26 runNPMInstallation ({ packageSpecifier, cwd, env }) {
27 return new Promise((resolve, reject) => {
28 // manually spawn npm
29 // use --parseable flag to get tab-delimited output
30 // forward sterr to process.stderr
31 // capture stdout to get package name
32
33 let pluginPath = null;
34
35 const installArgs = [
36 'install', packageSpecifier,
37 '--cache-min', 24 * 60 * 60,
38 '--parseable',
39 '--save'
40 ];
41
42 const installOpts = {
43 cwd: cwd || process.cwd(),
44 env: env || process.env,
45 stdio: ['ignore', 'pipe', 'inherit']
46 };
47
48 const npmCommand = os.platform() === 'win32' ? 'npm.cmd' : 'npm';
49 const npmInstall = spawn(npmCommand, installArgs, installOpts);
50
51 npmInstall.on('error', _ => {
52 reject(new Error('Cannot install', packageSpecifier));
53 });
54
55 const bufs = [];
56 let len = 0;
57 npmInstall.stdout.on('readable', () => {
58 const buf = npmInstall.stdout.read();
59
60 if (buf) {
61 bufs.push(buf);
62 len += buf.length;
63 }
64 });
65
66 npmInstall.stdout.on('end', () => {
67 const lines = Buffer.concat(bufs, len)
68 .toString()
69 .trim()
70 .split('\n');
71
72 const line = lines[lines.length - 1];
73
74 if (line.indexOf('\t') > -1) {
75 // npm >= 5
76 const output = lines[lines.length - 1].split('\t');
77
78 if (output.length < 4) {
79 reject(new Error('Cannot parse npm output while installing plugin.'));
80 return;
81 }
82
83 this.packageName = output[1];
84 pluginPath = path.join(cwd, output[3]);
85 } else {
86 // npm < 5
87 this.packageName = path.basename(line);
88 pluginPath = line;
89 }
90 });
91
92 npmInstall.on('exit', () => {
93 if (pluginPath) {
94 this.pluginManifest = require(pluginPath);
95
96 resolve({
97 packageName: this.packageName,
98 pluginManifest: this.pluginManifest
99 });
100 }
101 });
102 });
103 }
104
105 get existingPluginOptions () {
106 const config = this.config || require('./config');
107 const systemConfig = config.systemConfig;
108
109 const name = this.pluginKey;
110
111 const existingPluginOptions =
112 systemConfig.plugins && systemConfig.plugins[name]
113 ? systemConfig.plugins[name] : {};
114
115 return existingPluginOptions;
116 }
117
118 get pluginKey () {
119 let name = this.pluginManifest.name || this.packageName;
120
121 if (!this.pluginManifest.name &&
122 this.packageName.startsWith(PluginInstaller.PACKAGE_PREFIX)) {
123 name = this.packageName.substr(PluginInstaller.PACKAGE_PREFIX.length);
124 }
125
126 return name;
127 }
128
129 updateConfigurationFiles ({
130 pluginOptions,
131 enablePlugin,
132 addPoliciesToWhitelist }) {
133 // WARNING (kevinswiber): Updating YAML while maintaining presentation
134 // style is not easy. We're using the YAWN library here, which has
135 // a decent approach given the current state of available YAML parsers,
136 // but it's far from perfect. Take a look at existing YAWN issues
137 // before making any optimizations. If any section of this code looks
138 // ugly or inefficient, it may be that way for a reason (or maybe not).
139 //
140 // ¯\_(ツ)_/¯
141 //
142 // https://github.com/mohsen1/yawn-yaml/issues
143
144 if (!this.pluginManifest) {
145 return Promise.reject(
146 new Error('Configuration files require a plugin manifest.'));
147 }
148
149 let name = this.pluginManifest.name || this.packageName;
150
151 if (!this.pluginManifest.name &&
152 this.packageName.startsWith(PluginInstaller.PACKAGE_PREFIX)) {
153 name = this.packageName.substr(PluginInstaller.PACKAGE_PREFIX.length);
154 }
155
156 const maybeWriteSystemConfig = () => {
157 if (enablePlugin) {
158 return this._generateSystemConfigData(name, pluginOptions)
159 .then(({ systemConfigPath, output }) =>
160 writeFile(systemConfigPath, output));
161 }
162
163 return Promise.resolve();
164 };
165
166 const maybeWriteGatewayConfig = () => {
167 if (addPoliciesToWhitelist) {
168 const policyNames = this.pluginManifest.policies || [];
169
170 return this._generateGatewayConfigData(policyNames)
171 .then(({ gatewayConfigPath, output }) =>
172 writeFile(gatewayConfigPath, output));
173 }
174
175 return Promise.resolve();
176 };
177
178 return maybeWriteSystemConfig()
179 .then(maybeWriteGatewayConfig);
180 }
181
182 _updateYAML (obj, yawn) {
183 yawn.json = obj;
184 return yawn.json;
185 }
186
187 _generateSystemConfigData (name, pluginOptions) {
188 const config = this.config || require('./config');
189 const isJSON = config.systemConfigPath.toLowerCase().endsWith('.json');
190 const isYAML = !isJSON;
191
192 return readFile(config.systemConfigPath)
193 .then(systemConfig => {
194 // YAML-specific variables
195 let yawn = null;
196 let oldLength = null;
197
198 let obj = null;
199
200 if (isYAML) {
201 yawn = new YAWN(systemConfig.toString());
202 obj = Object.assign({}, yawn.json);
203
204 oldLength = obj.plugins ? null : yawn.yaml.length;
205 } else {
206 obj = JSON.parse(systemConfig.toString());
207 }
208
209 let plugins = obj.plugins || {};
210
211 if (!plugins.hasOwnProperty(name)) {
212 plugins[name] = {};
213 }
214
215 plugins[name].package = this.packageName;
216 obj.plugins = plugins;
217
218 if (isYAML) {
219 obj = this._updateYAML(obj, yawn);
220 }
221
222 if (pluginOptions) {
223 // YAWN needs to be updated by smallest atomic unit
224 Object.keys(pluginOptions).forEach(key => {
225 plugins[name][key] = pluginOptions[key];
226 obj.plugins = plugins;
227
228 if (isYAML) {
229 obj = this._updateYAML(obj, yawn);
230 plugins = obj.plugins;
231 }
232 });
233 }
234
235 if (isYAML && oldLength) {
236 // add a line break before inserting a new plugins mapping
237 yawn.yaml = yawn.yaml.substr(0, oldLength - 1) + os.EOL + yawn.yaml.substr(oldLength - 1);
238 }
239
240 const output = isYAML ? yawn.yaml.trim() : JSON.stringify(obj, null, 2);
241
242 return {
243 systemConfigPath: config.systemConfigPath,
244 output
245 };
246 });
247 }
248
249 _generateGatewayConfigData (policyNames) {
250 const config = this.config || require('./config');
251 const isJSON =
252 config.gatewayConfigPath.toLowerCase().endsWith('.json');
253 const isYAML = !isJSON;
254
255 return readFile(config.gatewayConfigPath)
256 .then(gatewayConfig => {
257 // YAML-specific variable
258 let yawn = null;
259
260 let obj = null;
261
262 if (isYAML) {
263 yawn = new YAWN(gatewayConfig.toString());
264 obj = Object.assign({}, yawn.json);
265 } else {
266 obj = JSON.parse(gatewayConfig.toString());
267 }
268
269 const policies = obj.policies || [];
270
271 // YAWN reverses arrays. ¯\_(ツ)_/¯
272 const correctedPolicyNames = isYAML ? policyNames.reverse() : policyNames;
273
274 correctedPolicyNames.forEach(policy => {
275 if (policies.indexOf(policy) === -1) {
276 policies.push(policy);
277 }
278 });
279
280 obj.policies = policies;
281
282 if (isYAML) {
283 yawn.json = obj;
284 }
285
286 const output = isYAML ? yawn.yaml.trim() : JSON.stringify(obj, null, 2);
287
288 return {
289 gatewayConfigPath: config.gatewayConfigPath,
290 output
291 };
292 });
293 }
294}
295
296module.exports = PluginInstaller;