ADD unit tests, split and refactor source code

This commit is contained in:
RecuencoJones
2019-03-25 20:38:29 +01:00
parent 6eaf5fc9af
commit 8257010937
18 changed files with 782 additions and 392 deletions

52
src/actions/install.js Normal file
View File

@@ -0,0 +1,52 @@
const mkdirp = require('mkdirp');
const request = require('request');
const { parsePackageJson } = require('../common');
const verifyAndPlaceBinary = require('../assets/binary');
/**
* Select a resource handling strategy based on given options.
*/
function getStrategy({ url }) {
if (url.endsWith('.tar.gz')) {
return require('../assets/untar');
} else {
return require('../assets/move');
}
}
/**
* Reads the configuration from application's package.json,
* validates properties, downloads the binary, untars, and stores at
* ./bin in the package's root. NPM already has support to install binary files
* specific locations when invoked with "npm install -g"
*
* See: https://docs.npmjs.com/files/package.json#bin
*/
function install(callback) {
const opts = parsePackageJson();
if (!opts) return callback('Invalid inputs');
mkdirp.sync(opts.binPath);
console.log('Downloading from URL: ' + opts.url);
const req = request({ uri: opts.url });
req.on('error', () => callback('Error downloading from URL: ' + opts.url));
req.on('response', (res) => {
if (res.statusCode !== 200) return callback('Error downloading binary. HTTP Status Code: ' + res.statusCode);
const strategy = getStrategy(opts);
strategy({
opts,
req,
onSuccess: () => verifyAndPlaceBinary(opts.binName, opts.binPath, callback),
onError: callback
});
});
}
module.exports = install;

24
src/actions/uninstall.js Normal file
View File

@@ -0,0 +1,24 @@
const { join } = require('path');
const { unlinkSync } = require('fs');
const { parsePackageJson, getInstallationPath } = require('../common');
function uninstall(callback) {
const { binName } = parsePackageJson();
getInstallationPath((err, installationPath) => {
if (err) {
return callback(err);
}
try {
unlinkSync(join(installationPath, binName));
} catch(ex) {
// Ignore errors when deleting the file.
}
return callback(null);
});
}
module.exports = uninstall;

25
src/assets/binary.js Normal file
View File

@@ -0,0 +1,25 @@
const { join } = require('path');
const { existsSync, renameSync, chmodSync } = require('fs');
const { getInstallationPath } = require('../common');
function verifyAndPlaceBinary(binName, binPath, callback) {
if (!existsSync(join(binPath, binName))) {
return callback(`Downloaded binary does not contain the binary specified in configuration - ${binName}`);
}
getInstallationPath((err, installationPath) => {
if (err) {
return callback(err);
}
// Move the binary file and make sure it is executable
renameSync(join(binPath, binName), join(installationPath, binName));
chmodSync(join(installationPath, binName), '755');
console.log('Placed binary on', join(installationPath, binName));
callback(null);
});
}
module.exports = verifyAndPlaceBinary;

17
src/assets/move.js Normal file
View File

@@ -0,0 +1,17 @@
const { join } = require('path');
const { createWriteStream } = require('fs');
/**
* Move strategy for binary resources without compression.
*/
function move({ opts, req, onSuccess, onError }) {
const stream = createWriteStream(join(opts.binPath, opts.binName));
stream.on('error', onError);
stream.on('close', onSuccess);
req.pipe(stream);
}
module.exports = move;

25
src/assets/untar.js Normal file
View File

@@ -0,0 +1,25 @@
const tar = require('tar');
const zlib = require('zlib');
/**
* Unzip strategy for resources using `.tar.gz`.
*
* First we will Un-GZip, then we will untar. So once untar is completed,
* binary is downloaded into `binPath`. Verify the binary and call it good.
*/
function untar({ opts, req, onSuccess, onError }) {
const ungz = zlib.createGunzip();
const untar = tar.Extract({ path: opts.binPath });
ungz.on('error', onError);
untar.on('error', onError);
// First we will Un-GZip, then we will untar. So once untar is completed,
// binary is downloaded into `binPath`. Verify the binary and call it good
untar.on('end', onSuccess);
req.pipe(ungz).pipe(untar);
}
module.exports = untar;

28
src/cli.js Normal file
View File

@@ -0,0 +1,28 @@
const actions = {
install: (callback) => require('./actions/install')(callback),
uninstall: (callback) => require('./actions/uninstall')(callback)
};
// Parse command line arguments and call the right action
module.exports = ({ argv, exit }) => {
if (argv && argv.length > 2) {
const cmd = argv[2];
if (!actions[cmd]) {
console.log('Invalid command to go-npm. `install` and `uninstall` are the only supported commands');
exit(1);
} else {
actions[cmd]((err) => {
if (err) {
console.error(err);
exit(1);
} else {
exit(0);
}
});
}
} else {
console.log('Invalid command to go-npm. `install` and `uninstall` are the only supported commands');
exit(1);
}
};

130
src/common.js Normal file
View File

@@ -0,0 +1,130 @@
const { join } = require('path');
const { exec } = require('child_process');
const { existsSync, readFileSync } = require('fs');
const mkdirp = require('mkdirp');
// Mapping from Node's `process.arch` to Golang's `$GOARCH`
const ARCH_MAPPING = {
ia32: '386',
x64: 'amd64',
arm: 'arm'
};
// Mapping between Node's `process.platform` to Golang's
const PLATFORM_MAPPING = {
darwin: 'darwin',
linux: 'linux',
win32: 'windows',
freebsd: 'freebsd'
};
function getInstallationPath(callback) {
// `npm bin` will output the path where binary files should be installed
exec('npm bin', (err, stdout, stderr) => {
let dir = null;
if (err || stderr || !stdout || stdout.length === 0) {
// We couldn't infer path from `npm bin`. Let's try to get it from
// Environment variables set by NPM when it runs.
// npm_config_prefix points to NPM's installation directory where `bin` folder is available
// Ex: /Users/foo/.nvm/versions/node/v4.3.0
const env = process.env;
if (env && env.npm_config_prefix) {
dir = join(env.npm_config_prefix, 'bin');
} else {
return callback(new Error('Error finding binary installation directory'));
}
} else {
dir = stdout.trim();
}
mkdirp.sync(dir);
callback(null, dir);
});
}
function validateConfiguration({ version, goBinary }) {
if (!version) {
return "'version' property must be specified";
}
if (!goBinary || typeof (goBinary) !== 'object') {
return "'goBinary' property must be defined and be an object";
}
if (!goBinary.name) {
return "'name' property is necessary";
}
if (!goBinary.path) {
return "'path' property is necessary";
}
if (!goBinary.url) {
return "'url' property is required";
}
}
function parsePackageJson() {
if (!(process.arch in ARCH_MAPPING)) {
console.error('Installation is not supported for this architecture: ' + process.arch);
return;
}
if (!(process.platform in PLATFORM_MAPPING)) {
console.error('Installation is not supported for this platform: ' + process.platform);
return
}
const packageJsonPath = join('.', 'package.json');
if (!existsSync(packageJsonPath)) {
console.error('Unable to find package.json. ' +
'Please run this script at root of the package you want to be installed');
return
}
const packageJson = JSON.parse(readFileSync(packageJsonPath));
const error = validateConfiguration(packageJson);
if (error && error.length > 0) {
console.error('Invalid package.json: ' + error);
return
}
// We have validated the config. It exists in all its glory
const binPath = packageJson.goBinary.path;
let binName = packageJson.goBinary.name;
let url = packageJson.goBinary.url;
let version = packageJson.version;
if (version[0] === 'v') version = version.substr(1); // strip the 'v' if necessary v0.0.1 => 0.0.1
// Binary name on Windows has .exe suffix
if (process.platform === 'win32') {
binName += '.exe'
url = url.replace(/{{win_ext}}/g, '.exe');
} else {
url = url.replace(/{{win_ext}}/g, '');
}
// Interpolate variables in URL, if necessary
url = url.replace(/{{arch}}/g, ARCH_MAPPING[process.arch]);
url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]);
url = url.replace(/{{version}}/g, version);
url = url.replace(/{{bin_name}}/g, binName);
return {
binName,
binPath,
url,
version
};
}
module.exports = { parsePackageJson, getInstallationPath };

View File

@@ -1,265 +1,3 @@
#!/usr/bin/env node
"use strict"
const request = require('request'),
path = require('path'),
tar = require('tar'),
zlib = require('zlib'),
mkdirp = require('mkdirp'),
fs = require('fs'),
exec = require('child_process').exec;
// Mapping from Node's `process.arch` to Golang's `$GOARCH`
const ARCH_MAPPING = {
"ia32": "386",
"x64": "amd64",
"arm": "arm"
};
// Mapping between Node's `process.platform` to Golang's
const PLATFORM_MAPPING = {
"darwin": "darwin",
"linux": "linux",
"win32": "windows",
"freebsd": "freebsd"
};
function getInstallationPath(callback) {
// `npm bin` will output the path where binary files should be installed
exec("npm bin", function(err, stdout, stderr) {
let dir = null;
if (err || stderr || !stdout || stdout.length === 0) {
// We couldn't infer path from `npm bin`. Let's try to get it from
// Environment variables set by NPM when it runs.
// npm_config_prefix points to NPM's installation directory where `bin` folder is available
// Ex: /Users/foo/.nvm/versions/node/v4.3.0
let env = process.env;
if (env && env.npm_config_prefix) {
dir = path.join(env.npm_config_prefix, "bin");
}
} else {
dir = stdout.trim();
}
mkdirp.sync(dir);
callback(null, dir);
});
}
function verifyAndPlaceBinary(binName, binPath, callback) {
if (!fs.existsSync(path.join(binPath, binName))) return callback(`Downloaded binary does not contain the binary specified in configuration - ${binName}`);
getInstallationPath(function(err, installationPath) {
if (err) return callback("Error getting binary installation path from `npm bin`");
// Move the binary file and make sure it is executable
fs.renameSync(path.join(binPath, binName), path.join(installationPath, binName));
fs.chmodSync(path.join(installationPath, binName), "755");
console.log("Placed binary on", path.join(installationPath, binName));
callback(null);
});
}
function validateConfiguration(packageJson) {
if (!packageJson.version) {
return "'version' property must be specified";
}
if (!packageJson.goBinary || typeof(packageJson.goBinary) !== "object") {
return "'goBinary' property must be defined and be an object";
}
if (!packageJson.goBinary.name) {
return "'name' property is necessary";
}
if (!packageJson.goBinary.path) {
return "'path' property is necessary";
}
if (!packageJson.goBinary.url) {
return "'url' property is required";
}
// if (!packageJson.bin || typeof(packageJson.bin) !== "object") {
// return "'bin' property of package.json must be defined and be an object";
// }
}
function parsePackageJson() {
if (!(process.arch in ARCH_MAPPING)) {
console.error("Installation is not supported for this architecture: " + process.arch);
return;
}
if (!(process.platform in PLATFORM_MAPPING)) {
console.error("Installation is not supported for this platform: " + process.platform);
return
}
const packageJsonPath = path.join(".", "package.json");
if (!fs.existsSync(packageJsonPath)) {
console.error("Unable to find package.json. " +
"Please run this script at root of the package you want to be installed");
return
}
let packageJson = JSON.parse(fs.readFileSync(packageJsonPath));
let error = validateConfiguration(packageJson);
if (error && error.length > 0) {
console.error("Invalid package.json: " + error);
return
}
// We have validated the config. It exists in all its glory
let binName = packageJson.goBinary.name;
let binPath = packageJson.goBinary.path;
let url = packageJson.goBinary.url;
let version = packageJson.version;
if (version[0] === 'v') version = version.substr(1); // strip the 'v' if necessary v0.0.1 => 0.0.1
// Binary name on Windows has .exe suffix
if (process.platform === "win32") {
binName += ".exe";
url = url.replace(/{{win_ext}}/g, '.exe');
} else {
url = url.replace(/{{win_ext}}/g, '');
}
// Interpolate variables in URL, if necessary
url = url.replace(/{{arch}}/g, ARCH_MAPPING[process.arch]);
url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]);
url = url.replace(/{{version}}/g, version);
url = url.replace(/{{bin_name}}/g, binName);
return {
binName: binName,
binPath: binPath,
url: url,
version: version
};
}
/**
* Unzip strategy for resources using `.tar.gz`.
*
* First we will Un-GZip, then we will untar. So once untar is completed,
* binary is downloaded into `binPath`. Verify the binary and call it good.
*/
function untarStrategy(opts, req, callback) {
const ungz = zlib.createGunzip();
const untar = tar.Extract({path: opts.binPath});
ungz.on('error', callback);
untar.on('error', callback);
// First we will Un-GZip, then we will untar. So once untar is completed,
// binary is downloaded into `binPath`. Verify the binary and call it good
untar.on('end', verifyAndPlaceBinary.bind(null, opts.binName, opts.binPath, callback));
req.pipe(ungz).pipe(untar);
}
/**
* Move strategy for binary resources without compression.
*/
function moveStrategy(opts, req, callback) {
const stream = fs.createWriteStream(path.join(opts.binPath, opts.binName));
stream.on('error', callback);
stream.on('close', verifyAndPlaceBinary.bind(null, opts.binName, opts.binPath, callback));
req.pipe(stream);
}
/**
* Select a resource handling strategy based on given options.
*/
function getStrategy(opts) {
if (opts.url.endsWith('.tar.gz')) {
return untarStrategy;
} else {
return moveStrategy;
}
}
/**
* Reads the configuration from application's package.json,
* validates properties, downloads the binary, untars, and stores at
* ./bin in the package's root. NPM already has support to install binary files
* specific locations when invoked with "npm install -g"
*
* See: https://docs.npmjs.com/files/package.json#bin
*/
const INVALID_INPUT = "Invalid inputs";
function install(callback) {
const opts = parsePackageJson();
if (!opts) return callback(INVALID_INPUT);
const strategy = getStrategy(opts);
mkdirp.sync(opts.binPath);
console.log("Downloading from URL: " + opts.url);
let req = request({uri: opts.url});
req.on('error', callback.bind(null, "Error downloading from URL: " + opts.url));
req.on('response', function(res) {
if (res.statusCode !== 200) return callback("Error downloading binary. HTTP Status Code: " + res.statusCode);
strategy(opts, req, callback);
});
}
function uninstall(callback) {
let opts = parsePackageJson();
getInstallationPath(function(err, installationPath) {
if (err) callback("Error finding binary installation directory");
try {
fs.unlinkSync(path.join(installationPath, opts.binName));
} catch(ex) {
// Ignore errors when deleting the file.
}
return callback(null);
});
}
// Parse command line arguments and call the right method
let actions = {
"install": install,
"uninstall": uninstall
};
let argv = process.argv;
if (argv && argv.length > 2) {
let cmd = process.argv[2];
if (!actions[cmd]) {
console.log("Invalid command to go-npm. `install` and `uninstall` are the only supported commands");
process.exit(1);
}
actions[cmd](function(err) {
if (err) {
console.error(err);
process.exit(1);
} else {
process.exit(0);
}
});
}
require('./cli')(process);