From cf87e0bc259dfc84bb96d47fe8603674221a795d Mon Sep 17 00:00:00 2001 From: xzyfer Date: Fri, 25 Mar 2016 21:46:47 +1100 Subject: [PATCH] Better error messages for missing binaries This is another iteration on improving the infamous >The `libsass` binding was not found Messages will now provide more useful information which will - give users a chance to resolve the problem themselves - give us more debug information from the error message alone Error messages produce now will look like: >Node Sass does not yet support your current environment: OS X 64-bit with Node.js 4.x >For more information on which environments are supported please see: >http://.... >Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 4.x >Found bindings for the following environments: > - OS X 64-bit with io.js 3.x > - OS X 64-bit with Node.js 5.x >This usually happens because your environment has changed since running `npm install`. >Run `npm rebuild node-sass` to build the binding for your current environment. >Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 4.x >This usually happens because your environment has changed since running `npm install`. >Run `npm rebuild node-sass` to build the binding for your current environment. --- lib/errors.js | 48 ++++++++++++++++++++++ lib/extensions.js | 83 +++++++++++++++++++++++++++++++++----- lib/index.js | 12 +++++- test/api.js | 100 +++++++++++++++++++++++++++++++++++++++++++++- test/errors.js | 53 ++++++++++++++++++++++++ test/runtime.js | 25 ++++++------ 6 files changed, 295 insertions(+), 26 deletions(-) create mode 100644 lib/errors.js create mode 100644 test/errors.js diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 000000000..296289dcf --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,48 @@ +/*! + * node-sass: lib/errors.js + */ + +var sass = require('./extensions'); + +function humanEnvironment() { + return sass.getHumanEnvironment(sass.getBinaryName()); +} + +function foundBinaries() { + return [ + 'Found bindings for the following environments:', + foundBinariesList(), + ].join('\n'); +} + +function foundBinariesList() { + return sass.getInstalledBinaries().map(function(env) { + return ' - ' + sass.getHumanEnvironment(env); + }).join('\n'); +} + +function missingBinaryFooter() { + return [ + 'This usually happens because your environment has changed since running `npm install`.', + 'Run `npm rebuild node-sass` to build the binding for your current environment.', + ].join('\n'); +} + +module.exports.unsupportedEnvironment = function() { + return [ + 'Node Sass does not yet support your current environment: ' + humanEnvironment(), + 'For more information on which environments are supported please see:', + 'TODO URL' + ].join('\n'); +}; + +module.exports.missingBinary = function() { + return [ + 'Missing binding ' + sass.getBinaryPath(), + 'Node Sass could not find a binding for your current environment: ' + humanEnvironment(), + '', + foundBinaries(), + '', + missingBinaryFooter(), + ].join('\n'); +}; diff --git a/lib/extensions.js b/lib/extensions.js index fd549021d..7f55a45ce 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -5,7 +5,68 @@ var eol = require('os').EOL, fs = require('fs'), pkg = require('../package.json'), - path = require('path'); + path = require('path'), + defaultBinaryPath = path.join(__dirname, '..', 'vendor'); + +function getHumanPlatform(arg) { + switch (arg || process.platform) { + case 'darwin': return 'OS X'; + case 'freebsd': return 'FreeBSD'; + case 'linux': return 'Linux'; + case 'win32': return 'Windows'; + default: return false; + } +} + +function getHumanArchitecture(arg) { + switch (arg || process.arch) { + case 'ia32': return '32-bit'; + case 'x86': return '32-bit'; + case 'x64': return '64-bit'; + default: return false; + } +} + +function getHumanNodeVersion(arg) { + switch (parseInt(arg || process.versions.modules, 10)) { + case 11: return 'Node 0.10.x'; + case 14: return 'Node 0.12.x'; + case 42: return 'io.js 1.x'; + case 43: return 'io.js 1.1.x'; + case 44: return 'io.js 2.x'; + case 45: return 'io.js 3.x'; + case 46: return 'Node.js 4.x'; + case 47: return 'Node.js 5.x'; + default: return false; + } +} + +function getHumanEnvironment(env) { + var parts = env.replace(/_binding\.node$/, '').split('-'); + + if (parts.length !== 3) { + return 'Unknown environment'; + } + + return [ + getHumanPlatform(parts[0]), + getHumanArchitecture(parts[1]), + 'with', + getHumanNodeVersion(parts[2]), + ].join(' '); +} + +function getInstalledBinaries() { + return fs.readdirSync(defaultBinaryPath); +} + +function isSupportedEnvironment() { + return ( + false !== getHumanPlatform() && + false !== getHumanArchitecture() && + false !== getHumanNodeVersion() + ); +} /** * Get the value of a CLI argument @@ -110,7 +171,7 @@ function getBinaryUrl() { * @api public */ -function getBinaryPath(throwIfNotExists) { +function getBinaryPath() { var binaryPath; if (getArgument('--sass-binary-path')) { @@ -122,20 +183,16 @@ function getBinaryPath(throwIfNotExists) { } else if (pkg.nodeSassConfig && pkg.nodeSassConfig.binaryPath) { binaryPath = pkg.nodeSassConfig.binaryPath; } else { - binaryPath = path.join(__dirname, '..', 'vendor', getBinaryName().replace(/_/, '/')); - } - - if (!fs.existsSync(binaryPath) && throwIfNotExists) { - throw new Error([ - ['The `libsass` binding was not found in', binaryPath].join(' '), - ['This usually happens because your node version has changed.'], - ['Run `npm rebuild node-sass` to build the binding for your current node version.'], - ].join('\n')); + binaryPath = path.join(defaultBinaryPath, getBinaryName().replace(/_/, '/')); } return binaryPath; } +function hasBinary(binaryPath) { + return fs.existsSync(binaryPath); +} + /** * Get Sass version information * @@ -149,7 +206,11 @@ function getVersionInfo(binding) { ].join(eol); } +module.exports.hasBinary = hasBinary; module.exports.getBinaryUrl = getBinaryUrl; module.exports.getBinaryName = getBinaryName; module.exports.getBinaryPath = getBinaryPath; module.exports.getVersionInfo = getVersionInfo; +module.exports.getHumanEnvironment = getHumanEnvironment; +module.exports.getInstalledBinaries = getInstalledBinaries; +module.exports.isSupportedEnvironment = isSupportedEnvironment; diff --git a/lib/index.js b/lib/index.js index cc741bb3b..b8dc2ad78 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,13 +4,23 @@ var path = require('path'), util = require('util'), + errors = require('./errors'), sass = require('./extensions'); +if (!sass.hasBinary(sass.getBinaryPath())) { + if (!sass.isSupportedEnvironment()) { + throw new Error(errors.unsupportedEnvironment()); + } else { + throw new Error(errors.missingBinary()); + } +} + + /** * Require binding */ -var binding = require(sass.getBinaryPath(true)); +var binding = require(sass.getBinaryPath()); /** * Get input file diff --git a/test/api.js b/test/api.js index 6fa3e2ec2..f45434c66 100644 --- a/test/api.js +++ b/test/api.js @@ -2,7 +2,10 @@ var assert = require('assert'), fs = require('fs'), path = require('path'), read = fs.readFileSync, - sass = process.env.NODESASS_COV ? require('../lib-cov') : require('../lib'), + sassPath = process.env.NODESASS_COV + ? require.resolve('../lib-cov') + : require.resolve('../lib'), + sass = require(sassPath), fixture = path.join.bind(null, __dirname, 'fixtures'), resolveFixture = path.resolve.bind(null, __dirname, 'fixtures'); @@ -1777,4 +1780,99 @@ describe('api', function() { done(); }); }); + + describe('binding', function() { + beforeEach(function() { + delete require.cache[sassPath]; + }); + + afterEach(function() { + delete require.cache[sassPath]; + }); + + describe('missing error', function() { + beforeEach(function() { + process.env.SASS_BINARY_NAME = [ + (process.platform === 'win32' ? 'Linux' : 'Windows'), '-', + process.arch, '-', + process.versions.modules + ].join(''); + }); + + afterEach(function() { + delete process.env.SASS_BINARY_NAME; + }); + + it('should be useful', function() { + assert.throws( + function() { require(sassPath); }, + new RegExp('Missing binding.*?\\' + path.sep + 'vendor\\' + path.sep) + ); + }); + + it('should list currently installed bindings', function() { + assert.throws( + function() { require(sassPath); }, + function(err) { + var etx = require('../lib/extensions'); + + delete process.env.SASS_BINARY_NAME; + + if ((err instanceof Error)) { + return err.message.indexOf( + etx.getHumanEnvironment(etx.getBinaryName()) + ) !== -1; + } + } + ); + }); + }); + + describe('on unsupported environment', function() { + it('should error for unsupported architecture', function() { + var prevValue = process.arch; + + Object.defineProperty(process, 'arch', { + get: function () { return 'foo'; } + }); + + assert.throws( + function() { require(sassPath); }, + 'Node Sass does not yet support your current environment' + ); + + process.arch = prevValue; + }); + + it('should error for unsupported platform', function() { + var prevValue = process.platform; + + Object.defineProperty(process, 'platform', { + get: function () { return 'foo'; } + }); + + assert.throws( + function() { require(sassPath); }, + 'Node Sass does not yet support your current environment' + ); + + process.platform = prevValue; + }); + + it('should error for unsupported runtime', function() { + var prevValue = process.versions.modules; + + Object.defineProperty(process.versions, 'modules', { + get: function () { return 'foo'; } + }); + + assert.throws( + function() { require(sassPath); }, + 'Node Sass does not yet support your current environment' + ); + + process.versions.modules = prevValue; + }); + }); + }); }); diff --git a/test/errors.js b/test/errors.js new file mode 100644 index 000000000..f922c0632 --- /dev/null +++ b/test/errors.js @@ -0,0 +1,53 @@ +var assert = require('assert'), + path = require('path'), + errors = require('../lib/errors'); + +describe('binary errors', function() { + + function getCurrentPlatform() { + if (process.platform === 'win32') { + return 'Windows'; + } else if (process.platform === 'darwin') { + return 'OS X'; + } + return ''; + } + + function getCurrentArchitecture() { + if (process.arch === 'x86' || process.arch === 'ia32') { + return '32-bit'; + } else if (process.arch === 'x64') { + return '64-bit'; + } + return ''; + } + + function getCurrentEnvironment() { + return getCurrentPlatform() + ' ' + getCurrentArchitecture(); + } + + describe('for an unsupported environment', function() { + it('identifies the current environment', function() { + var message = errors.unsupportedEnvironment(); + assert.ok(message.indexOf(getCurrentEnvironment()) !== -1); + }); + + it('links to supported environment documentation', function() { + var message = errors.unsupportedEnvironment(); + assert.ok(message.indexOf('TODO URL') !== -1); + }); + }); + + describe('for an missing binary', function() { + it('identifies the current environment', function() { + var message = errors.missingBinary(); + assert.ok(message.indexOf(getCurrentEnvironment()) !== -1); + }); + + it('documents the expected binary location', function() { + var message = errors.missingBinary(); + assert.ok(message.indexOf(path.sep + 'vendor' + path.sep) !== -1); + }); + }); + +}); diff --git a/test/runtime.js b/test/runtime.js index 722b7494f..8d320bbca 100644 --- a/test/runtime.js +++ b/test/runtime.js @@ -1,5 +1,4 @@ var assert = require('assert'), - fs = require('fs'), extensionsPath = process.env.NODESASS_COV ? require.resolve('../lib-cov/extensions') : require.resolve('../lib/extensions'); @@ -137,17 +136,17 @@ describe('runtime parameters', function() { }); }); -describe('library detection', function() { - it('should throw error when libsass binary is missing.', function() { - var sass = require(extensionsPath), - originalBin = sass.getBinaryPath(), - renamedBin = [originalBin, '_moved'].join(''); +// describe('library detection', function() { +// it('should throw error when libsass binary is missing.', function() { +// var sass = require(extensionsPath), +// originalBin = sass.getBinaryPath(), +// renamedBin = [originalBin, '_moved'].join(''); - assert.throws(function() { - fs.renameSync(originalBin, renamedBin); - sass.getBinaryPath(true); - }, /The `libsass` binding was not found/); +// assert.throws(function() { +// fs.renameSync(originalBin, renamedBin); +// sass.getBinaryPath(true); +// }, /The `libsass` binding was not found/); - fs.renameSync(renamedBin, originalBin); - }); -}); +// fs.renameSync(renamedBin, originalBin); +// }); +// });