mirror of
https://github.com/ershisan99/go-npm.git
synced 2025-12-16 20:59:28 +00:00
ADD unit tests, split and refactor source code
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
node_modules
|
node_modules
|
||||||
|
coverage
|
||||||
bin
|
bin
|
||||||
|
|||||||
91
__test__/actions/install.spec.js
Normal file
91
__test__/actions/install.spec.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const request = require('request');
|
||||||
|
const common = require('../../src/common');
|
||||||
|
const install = require('../../src/actions/install');
|
||||||
|
const move = require('../../src/assets/move');
|
||||||
|
const untar = require('../../src/assets/untar');
|
||||||
|
const verifyAndPlaceCallback = require('../../src/assets/binary');
|
||||||
|
|
||||||
|
jest.mock('fs');
|
||||||
|
jest.mock('mkdirp');
|
||||||
|
jest.mock('request');
|
||||||
|
jest.mock('../../src/common');
|
||||||
|
jest.mock('../../src/assets/move');
|
||||||
|
jest.mock('../../src/assets/untar');
|
||||||
|
jest.mock('../../src/assets/binary');
|
||||||
|
|
||||||
|
describe('install()', () => {
|
||||||
|
|
||||||
|
let callback, requestEvents;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
callback = jest.fn();
|
||||||
|
|
||||||
|
requestEvents = new EventEmitter();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with error if package.json did not return value' , () => {
|
||||||
|
common.parsePackageJson.mockReturnValueOnce(undefined);
|
||||||
|
|
||||||
|
install(callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('Invalid inputs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with error on request error', () => {
|
||||||
|
request.mockReturnValueOnce(requestEvents);
|
||||||
|
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url' });
|
||||||
|
|
||||||
|
install(callback);
|
||||||
|
|
||||||
|
requestEvents.emit('error');
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('Error downloading from URL: http://url');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with error on response with status code different than 200', () => {
|
||||||
|
request.mockReturnValueOnce(requestEvents);
|
||||||
|
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url' });
|
||||||
|
|
||||||
|
install(callback);
|
||||||
|
|
||||||
|
requestEvents.emit('response', { statusCode: 404 });
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('Error downloading binary. HTTP Status Code: 404');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pick move strategy if url is an uncompressed binary', () => {
|
||||||
|
request.mockReturnValueOnce(requestEvents);
|
||||||
|
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url' });
|
||||||
|
|
||||||
|
install(callback);
|
||||||
|
|
||||||
|
requestEvents.emit('response', { statusCode: 200 });
|
||||||
|
|
||||||
|
expect(move).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pick untar strategy if url ends with .tar.gz', () => {
|
||||||
|
request.mockReturnValueOnce(requestEvents);
|
||||||
|
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url.tar.gz' });
|
||||||
|
|
||||||
|
install(callback);
|
||||||
|
|
||||||
|
requestEvents.emit('response', { statusCode: 200 });
|
||||||
|
|
||||||
|
expect(untar).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call verifyAndPlaceCallback on success', () => {
|
||||||
|
request.mockReturnValueOnce(requestEvents);
|
||||||
|
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url', binName: 'command', binPath: './bin' });
|
||||||
|
move.mockImplementationOnce(({ onSuccess }) => onSuccess());
|
||||||
|
|
||||||
|
install(callback);
|
||||||
|
|
||||||
|
requestEvents.emit('response', { statusCode: 200 });
|
||||||
|
|
||||||
|
expect(verifyAndPlaceCallback).toHaveBeenCalledWith('command', './bin', callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
59
__test__/actions/uninstall.spec.js
Normal file
59
__test__/actions/uninstall.spec.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const common = require('../../src/common');
|
||||||
|
const uninstall = require('../../src/actions/uninstall');
|
||||||
|
|
||||||
|
jest.mock('fs');
|
||||||
|
jest.mock('../../src/common');
|
||||||
|
|
||||||
|
describe('uninstall()', () => {
|
||||||
|
|
||||||
|
let callback;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
callback = jest.fn();
|
||||||
|
|
||||||
|
common.parsePackageJson.mockReturnValueOnce({ binName: 'command' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with error if binary not found', () => {
|
||||||
|
const error = new Error();
|
||||||
|
|
||||||
|
common.getInstallationPath.mockImplementationOnce((cb) => cb(error));
|
||||||
|
|
||||||
|
uninstall(callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call unlinkSync with binary and installation path', () => {
|
||||||
|
|
||||||
|
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, './bin'));
|
||||||
|
|
||||||
|
uninstall(callback);
|
||||||
|
|
||||||
|
expect(fs.unlinkSync).toHaveBeenCalledWith('bin/command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback on success', () => {
|
||||||
|
|
||||||
|
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, './bin'));
|
||||||
|
|
||||||
|
uninstall(callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback regardless of errors on unlink', () => {
|
||||||
|
|
||||||
|
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, './bin'));
|
||||||
|
|
||||||
|
fs.unlinkSync.mockImplementationOnce(() => {
|
||||||
|
throw new Error();
|
||||||
|
});
|
||||||
|
|
||||||
|
uninstall(callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
51
__test__/assets/binary.spec.js
Normal file
51
__test__/assets/binary.spec.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const common = require('../../src/common');
|
||||||
|
const verifyAndPlaceBinary = require('../../src/assets/binary');
|
||||||
|
|
||||||
|
jest.mock('fs');
|
||||||
|
jest.mock('../../src/common');
|
||||||
|
|
||||||
|
describe('verifyAndPlaceBinary()', () => {
|
||||||
|
let callback;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callback = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with error if binary downloaded differs from config', () => {
|
||||||
|
fs.existsSync.mockReturnValueOnce(false);
|
||||||
|
|
||||||
|
verifyAndPlaceBinary('command', './bin', callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('Downloaded binary does not contain the binary specified in configuration - command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with error if installation path cannot be found', () => {
|
||||||
|
const error = new Error();
|
||||||
|
|
||||||
|
fs.existsSync.mockReturnValueOnce(true);
|
||||||
|
common.getInstallationPath.mockImplementationOnce((cb) => cb(error));
|
||||||
|
|
||||||
|
verifyAndPlaceBinary('command', './bin', callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with null on success', () => {
|
||||||
|
fs.existsSync.mockReturnValueOnce(true);
|
||||||
|
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, '/usr/local/bin'));
|
||||||
|
|
||||||
|
verifyAndPlaceBinary('command', './bin', callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should move the binary to installation directory', () => {
|
||||||
|
fs.existsSync.mockReturnValueOnce(true);
|
||||||
|
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, '/usr/local/bin'));
|
||||||
|
|
||||||
|
verifyAndPlaceBinary('command', './bin', callback);
|
||||||
|
|
||||||
|
expect(fs.renameSync).toHaveBeenCalledWith('bin/command', '/usr/local/bin/command');
|
||||||
|
});
|
||||||
|
});
|
||||||
49
__test__/assets/move.spec.js
Normal file
49
__test__/assets/move.spec.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const fs = require('fs');
|
||||||
|
const move = require('../../src/assets/move');
|
||||||
|
|
||||||
|
jest.mock('fs');
|
||||||
|
|
||||||
|
describe('move()', () => {
|
||||||
|
|
||||||
|
let streamEvents, pipe, onSuccess, onError;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
streamEvents = new EventEmitter();
|
||||||
|
|
||||||
|
pipe = jest.fn();
|
||||||
|
onSuccess = jest.fn();
|
||||||
|
onError = jest.fn();
|
||||||
|
createWriteStream = jest.fn();
|
||||||
|
|
||||||
|
fs.createWriteStream.mockReturnValueOnce(streamEvents);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should download resource to given binPath', () => {
|
||||||
|
|
||||||
|
move({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||||
|
|
||||||
|
expect(fs.createWriteStream).toHaveBeenCalledWith('bin/command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSuccess on stream closed', () => {
|
||||||
|
|
||||||
|
move({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||||
|
|
||||||
|
streamEvents.emit('close');
|
||||||
|
|
||||||
|
expect(onSuccess).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onError with error on write stream error', () => {
|
||||||
|
|
||||||
|
const error = new Error();
|
||||||
|
|
||||||
|
move({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||||
|
|
||||||
|
streamEvents.emit('error', error);
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
65
__test__/assets/untar.spec.js
Normal file
65
__test__/assets/untar.spec.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
const tar = require('tar');
|
||||||
|
const untar = require('../../src/assets/untar');
|
||||||
|
|
||||||
|
jest.mock('zlib');
|
||||||
|
jest.mock('tar', () => ({
|
||||||
|
Extract: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('untar()', () => {
|
||||||
|
|
||||||
|
let ungzEvents, untarEvents, pipe, onSuccess, onError;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ungzEvents = new EventEmitter();
|
||||||
|
untarEvents = new EventEmitter();
|
||||||
|
|
||||||
|
pipe = jest.fn();
|
||||||
|
onSuccess = jest.fn();
|
||||||
|
onError = jest.fn();
|
||||||
|
|
||||||
|
pipe.mockReturnValueOnce({ pipe });
|
||||||
|
tar.Extract.mockReturnValueOnce(untarEvents);
|
||||||
|
zlib.createGunzip.mockReturnValueOnce(ungzEvents);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should download resource and untar to given binPath', () => {
|
||||||
|
|
||||||
|
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||||
|
|
||||||
|
expect(tar.Extract).toHaveBeenCalledWith({ path: './bin' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSuccess on untar end', () => {
|
||||||
|
|
||||||
|
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||||
|
|
||||||
|
untarEvents.emit('end');
|
||||||
|
|
||||||
|
expect(onSuccess).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onError with error on ungz error', () => {
|
||||||
|
|
||||||
|
const error = new Error();
|
||||||
|
|
||||||
|
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||||
|
|
||||||
|
ungzEvents.emit('error', error);
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onError with error on untar error', () => {
|
||||||
|
|
||||||
|
const error = new Error();
|
||||||
|
|
||||||
|
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||||
|
|
||||||
|
untarEvents.emit('error', error);
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
40
__test__/cli.spec.js
Normal file
40
__test__/cli.spec.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const cli = require('../src/cli');
|
||||||
|
const install = require('../src/actions/install');
|
||||||
|
|
||||||
|
jest.mock('../src/actions/install');
|
||||||
|
|
||||||
|
describe('cli()', () => {
|
||||||
|
let exit;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
exit = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit with error if not enough args are supplied', () => {
|
||||||
|
cli({ argv: [], exit });
|
||||||
|
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit with error if command does not exist', () => {
|
||||||
|
cli({ argv: [ '/usr/local/bin/node', 'index.js', 'command' ], exit });
|
||||||
|
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit with error if command returns error', () => {
|
||||||
|
install.mockImplementationOnce((cb) => cb(new Error()));
|
||||||
|
|
||||||
|
cli({ argv: [ '/usr/local/bin/node', 'index.js', 'install' ], exit });
|
||||||
|
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit with success if command runs fine', () => {
|
||||||
|
install.mockImplementationOnce((cb) => cb(null));
|
||||||
|
|
||||||
|
cli({ argv: [ '/usr/local/bin/node', 'index.js', 'install' ], exit });
|
||||||
|
|
||||||
|
expect(exit).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
123
__test__/common.spec.js
Normal file
123
__test__/common.spec.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const childProcess = require('child_process');
|
||||||
|
const common = require('../src/common');
|
||||||
|
|
||||||
|
jest.mock('fs');
|
||||||
|
jest.mock('child_process');
|
||||||
|
jest.mock('mkdirp');
|
||||||
|
|
||||||
|
describe('common', () => {
|
||||||
|
describe('getInstallationPath()', () => {
|
||||||
|
let callback, env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callback = jest.fn();
|
||||||
|
|
||||||
|
env = { ...process.env };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = env;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get binaries path from `npm bin`', () => {
|
||||||
|
childProcess.exec.mockImplementationOnce((_cmd, cb) => cb(null, '/usr/local/bin'));
|
||||||
|
|
||||||
|
common.getInstallationPath(callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(null, '/usr/local/bin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get binaries path from env', () => {
|
||||||
|
childProcess.exec.mockImplementationOnce((_cmd, cb) => cb(new Error()));
|
||||||
|
|
||||||
|
process.env.npm_config_prefix = '/usr/local';
|
||||||
|
|
||||||
|
common.getInstallationPath(callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(null, '/usr/local/bin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call callback with error if binaries path is not found', () => {
|
||||||
|
childProcess.exec.mockImplementationOnce((_cmd, cb) => cb(new Error()));
|
||||||
|
|
||||||
|
process.env.npm_config_prefix = undefined;
|
||||||
|
|
||||||
|
common.getInstallationPath(callback);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith(new Error('Error finding binary installation directory'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parsePackageJson()', () => {
|
||||||
|
let _process;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
_process = { ...global.process };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.process = _process;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validation', () => {
|
||||||
|
it('should return if architecture is unsupported', () => {
|
||||||
|
process.arch = 'mips';
|
||||||
|
|
||||||
|
expect(common.parsePackageJson()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return if platform is unsupported', () => {
|
||||||
|
process.platform = 'amiga';
|
||||||
|
|
||||||
|
expect(common.parsePackageJson()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return if package.json does not exist', () => {
|
||||||
|
fs.existsSync.mockReturnValueOnce(false);
|
||||||
|
|
||||||
|
expect(common.parsePackageJson()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('variable replacement', () => {
|
||||||
|
it('should append .exe extension on windows platform', () => {
|
||||||
|
fs.existsSync.mockReturnValueOnce(true);
|
||||||
|
fs.readFileSync.mockReturnValueOnce(JSON.stringify({
|
||||||
|
version: '1.0.0',
|
||||||
|
goBinary: {
|
||||||
|
name: 'command',
|
||||||
|
path: './bin',
|
||||||
|
url: 'https://github.com/foo/bar/releases/v{{version}}/assets/command{{win_ext}}'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
process.platform = 'win32';
|
||||||
|
|
||||||
|
expect(common.parsePackageJson()).toMatchObject({
|
||||||
|
binName: 'command.exe',
|
||||||
|
url: 'https://github.com/foo/bar/releases/v1.0.0/assets/command.exe'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not append .exe extension on platform different than windows', () => {
|
||||||
|
fs.existsSync.mockReturnValueOnce(true);
|
||||||
|
fs.readFileSync.mockReturnValueOnce(JSON.stringify({
|
||||||
|
version: '1.0.0',
|
||||||
|
goBinary: {
|
||||||
|
name: 'command',
|
||||||
|
path: './bin',
|
||||||
|
url: 'https://github.com/foo/bar/releases/v{{version}}/assets/command{{win_ext}}'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
process.platform = 'darwin';
|
||||||
|
|
||||||
|
expect(common.parsePackageJson()).toMatchObject({
|
||||||
|
binName: 'command',
|
||||||
|
url: 'https://github.com/foo/bar/releases/v1.0.0/assets/command'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
const rewire = require('rewire');
|
|
||||||
const EventEmitter = require('events').EventEmitter;
|
|
||||||
|
|
||||||
describe('go-npm', function() {
|
|
||||||
let mod;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
mod = rewire('../src/index.js');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Resource handling strategies', function() {
|
|
||||||
|
|
||||||
describe('untarStrategy()', function() {
|
|
||||||
|
|
||||||
let untarStrategy, ungzEvents, untarEvents, pipe, callback, zlib, createGunzip, tar;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
untarStrategy = mod.__get__('untarStrategy');
|
|
||||||
zlib = mod.__get__('zlib');
|
|
||||||
tar = mod.__get__('tar');
|
|
||||||
ungzEvents = new EventEmitter();
|
|
||||||
untarEvents = new EventEmitter();
|
|
||||||
|
|
||||||
createGunzip = jest.fn();
|
|
||||||
pipe = jest.fn();
|
|
||||||
callback = jest.fn();
|
|
||||||
|
|
||||||
pipe.mockReturnValueOnce({ pipe });
|
|
||||||
createGunzip.mockReturnValueOnce(ungzEvents);
|
|
||||||
jest.spyOn(tar, 'Extract').mockReturnValueOnce(untarEvents);
|
|
||||||
|
|
||||||
// jest.spyOn not working on read-only properties
|
|
||||||
Object.defineProperty(zlib, 'createGunzip', { value: createGunzip });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should download resource and untar to given binPath', function() {
|
|
||||||
|
|
||||||
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
|
||||||
|
|
||||||
expect(tar.Extract).toHaveBeenCalledWith({ path: './bin' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call verifyAndPlaceBinary on untar end', function() {
|
|
||||||
|
|
||||||
const verifyAndPlaceBinary = jest.fn();
|
|
||||||
|
|
||||||
mod.__set__('verifyAndPlaceBinary', verifyAndPlaceBinary);
|
|
||||||
|
|
||||||
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
|
||||||
|
|
||||||
untarEvents.emit('end');
|
|
||||||
|
|
||||||
expect(verifyAndPlaceBinary).toHaveBeenCalledWith('command', './bin', callback);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call callback with error on ungz error', function() {
|
|
||||||
|
|
||||||
const error = new Error();
|
|
||||||
|
|
||||||
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
|
||||||
|
|
||||||
ungzEvents.emit('error', error);
|
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledWith(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call callback with error on untar error', function() {
|
|
||||||
|
|
||||||
const error = new Error();
|
|
||||||
|
|
||||||
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
|
||||||
|
|
||||||
untarEvents.emit('error', error);
|
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledWith(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('moveStrategy()', function() {
|
|
||||||
|
|
||||||
let moveStrategy, streamEvents, pipe, callback, fs;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
|
|
||||||
moveStrategy = mod.__get__('moveStrategy');
|
|
||||||
fs = mod.__get__('fs');
|
|
||||||
streamEvents = new EventEmitter();
|
|
||||||
|
|
||||||
pipe = jest.fn();
|
|
||||||
callback = jest.fn();
|
|
||||||
|
|
||||||
jest.spyOn(fs, 'createWriteStream').mockReturnValueOnce(streamEvents);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should download resource to given binPath', function() {
|
|
||||||
|
|
||||||
moveStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
|
||||||
|
|
||||||
expect(fs.createWriteStream).toHaveBeenCalledWith('bin/command');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call verifyAndPlaceBinary on stream closed', function() {
|
|
||||||
|
|
||||||
const verifyAndPlaceBinary = jest.fn();
|
|
||||||
|
|
||||||
mod.__set__('verifyAndPlaceBinary', verifyAndPlaceBinary);
|
|
||||||
|
|
||||||
moveStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
|
||||||
|
|
||||||
streamEvents.emit('close');
|
|
||||||
|
|
||||||
expect(verifyAndPlaceBinary).toHaveBeenCalledWith('command', './bin', callback);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call callback with error on write stream error', function() {
|
|
||||||
|
|
||||||
const error = new Error();
|
|
||||||
|
|
||||||
moveStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
|
||||||
|
|
||||||
streamEvents.emit('error', error);
|
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledWith(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
"babel-cli": "^6.24.1",
|
"babel-cli": "^6.24.1",
|
||||||
"babel-core": "^6.25.0",
|
"babel-core": "^6.25.0",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"jest": "^24.5.0",
|
"jest": "^24.5.0"
|
||||||
"rewire": "^4.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/actions/install.js
Normal file
52
src/actions/install.js
Normal 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
24
src/actions/uninstall.js
Normal 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
25
src/assets/binary.js
Normal 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
17
src/assets/move.js
Normal 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
25
src/assets/untar.js
Normal 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
28
src/cli.js
Normal 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
130
src/common.js
Normal 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 };
|
||||||
264
src/index.js
264
src/index.js
@@ -1,265 +1,3 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
"use strict"
|
require('./cli')(process);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user