Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scripts/hooks functionality added #718

Merged
merged 1 commit into from
Feb 10, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions HOOKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Install and Uninstall Hooks

Bower provides 3 separate hooks that can be used to trigger other automated tools during Bower usage. Importantly, these hooks are intended to allow external tools to help wire up the newly installed components into the parent project and other similar tasks. These hooks are not intended to provide a post-installation build step for component authors. As such, the configuration for these hooks is provided in the `.bowerrc` file in the parent project's directory.

## Configuring

In `.bowerrc` do:

```js
{
"scripts": {
"preinstall": "<your command here>",
"postinstall": "<your command here>",
"preuninstall": "<your command here>"
}
}
```

The value of each script hook may contain a % character. When your script is called, the % will be replaced with a space-separated list of components being installed or uninstalled.

Your script will also include an environment variable `BOWER_PID` containing the PID of the parent Bower process that triggered the script. This can be used to verify that a `preinstall` and `postinstall` steps are part of the same Bower process.
9 changes: 8 additions & 1 deletion lib/core/Manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var PackageRepository = require('./PackageRepository');
var semver = require('../util/semver');
var copy = require('../util/copy');
var createError = require('../util/createError');
var scripts = require('./scripts');

function Manager(config, logger) {
this._config = config;
Expand Down Expand Up @@ -108,7 +109,7 @@ Manager.prototype.resolve = function () {
}.bind(this));
};

Manager.prototype.install = function () {
Manager.prototype.install = function (json) {
var componentsDir;
var that = this;

Expand All @@ -124,6 +125,9 @@ Manager.prototype.install = function () {

componentsDir = path.join(this._config.cwd, this._config.directory);
return Q.nfcall(mkdirp, componentsDir)
.then(function () {
return scripts.preinstall(that._config, that._logger, that._dissected, that._installed, json);
})
.then(function () {
var promises = [];

Expand Down Expand Up @@ -165,6 +169,9 @@ Manager.prototype.install = function () {

return Q.all(promises);
})
.then(function () {
return scripts.postinstall(that._config, that._logger, that._dissected, that._installed, json);
})
.then(function () {
// Sync up dissected dependencies and dependants
// See: https://github.com/bower/bower/issues/879
Expand Down
71 changes: 38 additions & 33 deletions lib/core/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var md5 = require('../util/md5');
var createError = require('../util/createError');
var readJson = require('../util/readJson');
var validLink = require('../util/validLink');
var scripts = require('./scripts');

function Project(config, logger) {
// This is the only architecture component that ensures defaults
Expand Down Expand Up @@ -524,7 +525,7 @@ Project.prototype._bootstrap = function (targets, resolved, incompatibles) {
}
}.bind(this))
// Install resolved ones
.then(this._manager.install.bind(this._manager));
.then(this._manager.install.bind(this._manager, this._json));
};

Project.prototype._readJson = function () {
Expand Down Expand Up @@ -677,43 +678,47 @@ Project.prototype._removePackages = function (packages) {
var that = this;
var promises = [];

mout.object.forOwn(packages, function (dir, name) {
var promise;
return scripts.preuninstall(that._config, that._logger, packages, that._installed, that._json)
.then(function () {

// Delete directory
if (!dir) {
promise = Q.resolve();
that._logger.warn('not-installed', name, {
name: name
});
} else {
promise = Q.nfcall(rimraf, dir);
that._logger.action('uninstall', name, {
name: name,
dir: dir
});
}
mout.object.forOwn(packages, function (dir, name) {
var promise;

// Remove from json only if successfully deleted
if (that._options.save && that._json.dependencies) {
promise = promise
.then(function () {
delete that._json.dependencies[name];
});
}
// Delete directory
if (!dir) {
promise = Q.resolve();
that._logger.warn('not-installed', name, {
name: name
});
} else {
promise = Q.nfcall(rimraf, dir);
that._logger.action('uninstall', name, {
name: name,
dir: dir
});
}

if (that._options.saveDev && that._json.devDependencies) {
promise = promise
.then(function () {
delete that._json.devDependencies[name];
});
}
// Remove from json only if successfully deleted
if (that._options.save && that._json.dependencies) {
promise = promise
.then(function () {
delete that._json.dependencies[name];
});
}

promises.push(promise);
});
if (that._options.saveDev && that._json.devDependencies) {
promise = promise
.then(function () {
delete that._json.devDependencies[name];
});
}

return Q.all(promises)
// Save json
promises.push(promise);
});

return Q.all(promises);

})
.then(function () {
return that.saveJson();
})
Expand Down
96 changes: 96 additions & 0 deletions lib/core/scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
var mout = require('mout');
var cmd = require('../util/cmd');
var Q = require('q');
var shellquote = require('shell-quote');

var orderByDependencies = function (packages, installed, json) {
var ordered = [];
installed = mout.object.keys(installed);

var depsSatisfied = function (packageName) {
return mout.array.difference(mout.object.keys(packages[packageName].dependencies), installed, ordered).length === 0;
};

var depsFromBowerJson = json && json.dependencies ? mout.object.keys(json.dependencies) : [];
var packageNames = mout.object.keys(packages);

//get the list of the packages that are specified in bower.json in that order
//its nice to maintain that order for users
var desiredOrder = mout.array.intersection(depsFromBowerJson, packageNames);
//then add to the end any remaining packages that werent in bower.json
desiredOrder = desiredOrder.concat(mout.array.difference(packageNames, desiredOrder));

//the desired order isn't necessarily a correct dependency specific order
//so we ensure that below
var resolvedOne = true;
while (resolvedOne) {

resolvedOne = false;

for (var i = 0; i < desiredOrder.length; i++) {
var packageName = desiredOrder[i];
if (depsSatisfied(packageName)) {
ordered.push(packageName);
mout.array.remove(desiredOrder, packageName);
//as soon as we resolve a package start the loop again
resolvedOne = true;
break;
}
}

if (!resolvedOne && desiredOrder.length > 0) {
//if we're here then some package(s) doesn't have all its deps satisified
//so lets just jam those names on the end
ordered = ordered.concat(desiredOrder);
}

}

return ordered;
};

var run = function (cmdString, action, logger, config) {
logger.action(action, cmdString);

//pass env + BOWER_PID so callees can identify a preinstall+postinstall from the same bower instance
var env = mout.object.mixIn({ 'BOWER_PID': process.pid }, process.env);
var args = shellquote.parse(cmdString, env);
var cmdName = args[0];
mout.array.remove(args, cmdName); //no rest() in mout

var options = {
cwd: config.cwd,
env: env
};

var promise = cmd(cmdName, args, options);

promise.progress(function (progress) {
progress.split('\n').forEach(function (line) {
if (line) {
logger.action(action, line);
}
});
});

return promise;
};

var hook = function (action, ordered, config, logger, packages, installed, json) {
if (mout.object.keys(packages).length === 0 || !config.scripts || !config.scripts[action]) {
/*jshint newcap: false */
return Q();
}

var orderedPackages = ordered ? orderByDependencies(packages, installed, json) : mout.object.keys(packages);
var cmdString = mout.string.replace(config.scripts[action], '%', orderedPackages.join(' '));
return run(cmdString, action, logger, config);
};

module.exports = {
preuninstall: mout.function.partial(hook, 'preuninstall', false),
preinstall: mout.function.partial(hook, 'preinstall', true),
postinstall: mout.function.partial(hook, 'postinstall', true),
//only exposed for test
_orderByDependencies: orderByDependencies
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"which": "~1.0.5",
"p-throttler": "~0.0.1",
"insight": "~0.3.0",
"is-root": "~0.1.0"
"is-root": "~0.1.0",
"shell-quote": "~1.4.1"
},
"devDependencies": {
"expect.js": "~0.2.0",
Expand Down
128 changes: 128 additions & 0 deletions test/core/scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
var path = require('path');
var bower = require('../../lib/index.js');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
var fs = require('fs');
var expect = require('expect.js');
var scripts = require('../../lib/core/scripts.js');

describe('scripts', function () {

var tempDir = path.join(__dirname, '../assets/temp-scripts');
var packageName = 'package-zip';
var packageDir = path.join('..', packageName + '.zip');

var config = {
cwd: tempDir,
scripts: {
preinstall: 'touch preinstall_%',
postinstall: 'touch postinstall_%',
preuninstall: 'touch preuninstall_%'
}
};

before(function (next) {
mkdirp(tempDir, next);
});

after(function (next) {
rimraf(tempDir, next);
});

it('should run preinstall and postinstall hooks.', function (next) {

bower.commands
.install([packageDir], undefined, config)
.on('end', function (installed) {

expect(fs.existsSync(path.join(tempDir, 'preinstall_' + packageName))).to.be(true);
expect(fs.existsSync(path.join(tempDir, 'postinstall_' + packageName))).to.be(true);

next();
});

});

it('should run preuninstall hook.', function (next) {

bower.commands
.uninstall([packageName], undefined, config)
.on('end', function (installed) {

expect(fs.existsSync(path.join(tempDir, 'preuninstall_' + packageName))).to.be(true);

next();
});

});

it('should not break anything when no hooks configured.', function (next) {

bower.commands
.uninstall([packageName], undefined, { cwd: tempDir })
.on('end', function (installed) {

//no exception then we're good

next();
});

});

it('should reorder packages by dependencies, while trying to maintain order from bower.json, correctly.', function () {

var mockAngularUI = { dependencies: {
'angular': '*'
}};
var mockJQuery = { dependencies: {
}};
var mockAngular = { dependencies: {
'jquery': '*'
}};
var mockMoment = { dependencies: {
}};
var mockSelect2 = { dependencies: {
'jquery': '*'
}};
var mockBadPackage = { dependencies: {
'something-not-installed': '*'
}};

var packages = {
'select2': mockSelect2,
'angular-ui': mockAngularUI,
'jquery': mockJQuery,
'bad-package': mockBadPackage,
'angular': mockAngular,
'moment': mockMoment
};
var installed = [];
var mockBowerJson = { dependencies: {
'jquery': '*',
'select2': '*',
'angular-ui': '*',
'angular': '*',
'moment': '*'
} };

var ordered = scripts._orderByDependencies(packages, installed, mockBowerJson);
expect(ordered).to.eql(['jquery', 'select2', 'angular', 'angular-ui', 'moment', 'bad-package']);

});

it('should process scripts with quotes and vars in the cmd properly.', function (next) {

config.scripts.preinstall = 'touch "$BOWER_PID %"';

bower.commands
.install([packageDir], undefined, config)
.on('end', function (installed) {

expect(fs.existsSync(path.join(tempDir, process.pid + ' ' + packageName))).to.be(true);

next();
});

});

});