diff --git a/.travis.yml b/.travis.yml index 1048e76..13ce0c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ sudo: false node_js: - "0.10" - "0.12" - - "iojs" + - "stable" script: - "npm test" diff --git a/README.md b/README.md index 6ccade9..6493b6f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ unassertify ================================ -[Browserify](http://browserify.org/) transform to remove assertions on production build. Encourages Design by Contract (DbC). +[Browserify](http://browserify.org/) transform to remove assertions from code. Encourages Design by Contract (DbC). [![Build Status][travis-image]][travis-url] [![NPM version][npm-image]][npm-url] @@ -43,7 +43,7 @@ var glob = require('glob'), gulp.task('production_build', function() { var files = glob.sync('./src/*.js'); - var b = browserify({entries: files, debug: false}); + var b = browserify({entries: files}); b.transform('unassertify'); return b.bundle() .pipe(source('bundle.js')) diff --git a/index.js b/index.js index 4f3d302..02eb406 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ /** * unassertify - * Browserify transform to remove assertions on production build + * Browserify transform to remove assertions from code to encourage Design by Contract (DbC) * * https://github.com/twada/unassertify * @@ -13,22 +13,65 @@ var through = require('through'); var esprima = require('esprima'); var escodegen = require('escodegen'); +var convert = require('convert-source-map'); +var transfer = require('multi-stage-sourcemap').transfer; var unassert = require('unassert'); -function isDebugMode (options) { - return (options && options._flags && options._flags.debug); +function mergeSourceMap (incomingSourceMap, outgoingSourceMap) { + if (typeof outgoingSourceMap === 'string' || outgoingSourceMap instanceof String) { + outgoingSourceMap = JSON.parse(outgoingSourceMap); + } + if (!incomingSourceMap) { + return outgoingSourceMap; + } + return JSON.parse(transfer({fromSourceMap: outgoingSourceMap, toSourceMap: incomingSourceMap})); +} + +function handleIncomingSourceMap (originalCode) { + var commented = convert.fromSource(originalCode); + if (commented) { + return commented.toObject(); + } + return null; +} + +function applyUnassertWithSourceMap (code, filepath, options) { + var ast = esprima.parse(code, { sourceType: 'module', tolerant: true, loc: true }); + var inMap = handleIncomingSourceMap(code); + var instrumented = escodegen.generate(unassert(ast), { + sourceMap: filepath, + sourceContent: code, + sourceMapWithCode: true + }); + var outMap = convert.fromJSON(instrumented.map.toString()); + if (inMap) { + var mergedRawMap = mergeSourceMap(inMap, outMap.toObject()); + var reMap = convert.fromObject(mergedRawMap); + if (inMap.sources) { + reMap.setProperty('sources', inMap.sources); + } + if (inMap.sourceRoot) { + reMap.setProperty('sourceRoot', inMap.sourceRoot); + } + if (inMap.sourcesContent) { + reMap.setProperty('sourcesContent', inMap.sourcesContent); + } + return instrumented.code + '\n' + reMap.toComment() + '\n'; + } else { + return instrumented.code + '\n' + outMap.toComment() + '\n'; + } } -function applyUnassert (code, options) { +function applyUnassertWithoutSourceMap (code, filepath, options) { var ast = esprima.parse(code, { sourceType: 'module' }); return escodegen.generate(unassert(ast)); } -module.exports = function unassertify (filepath, options) { - if (isDebugMode(options)) { - return through(); - } +function shouldProduceSourceMap (options) { + return (options && options._flags && options._flags.debug); +} +module.exports = function unassertify (filepath, options) { var data = '', stream = through(write, end); @@ -37,7 +80,11 @@ module.exports = function unassertify (filepath, options) { } function end() { - stream.queue(applyUnassert(data, options)); + if (shouldProduceSourceMap(options)) { + stream.queue(applyUnassertWithSourceMap(data, filepath, options)); + } else { + stream.queue(applyUnassertWithoutSourceMap(data, filepath, options)); + } stream.queue(null); } diff --git a/package.json b/package.json index aa6ad49..c5abf2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unassertify", - "description": "Browserify transform to remove assertions on production build", + "description": "Browserify transform to remove assertions from code to encourage Design by Contract (DbC)", "version": "1.0.2", "author": { "name": "Takuto Wada", @@ -9,15 +9,21 @@ }, "bugs": "https://github.com/twada/unassertify/issues", "dependencies": { + "convert-source-map": "^1.1.1", "escodegen": "^1.6.1", "esprima": "^2.2.0", + "multi-stage-sourcemap": "^0.2.1", "through": "^2.3.7", - "unassert": "^1.1.0" + "unassert": "^1.2.0" }, "devDependencies": { - "browserify": "^11.0.1", + "browserify": "^11.2.0", + "coffeeify": "^1.1.0", + "espower-loader": "^1.0.0", "event-stream": "^3.3.1", - "mocha": "^2.2.5" + "intelli-espower-loader": "^1.0.0", + "mocha": "^2.3.3", + "power-assert": "^1.0.1" }, "files": [ "README.md", @@ -40,6 +46,6 @@ "url": "http://github.com/twada/unassertify.git" }, "scripts": { - "test": "mocha" + "test": "mocha --require intelli-espower-loader" } } diff --git a/test/fixtures/coffee/fixture.coffee b/test/fixtures/coffee/fixture.coffee new file mode 100644 index 0000000..c08d26e --- /dev/null +++ b/test/fixtures/coffee/fixture.coffee @@ -0,0 +1,8 @@ +assert = require('assert') + +add = (a, b) -> + console.assert typeof a == 'number' + assert !isNaN(a) + assert.equal typeof b, 'number' + assert.ok !isNaN(b) + a + b diff --git a/test/fixtures/func/expected-with-sourcemap.js b/test/fixtures/func/expected-with-sourcemap.js new file mode 100644 index 0000000..e11d1e6 --- /dev/null +++ b/test/fixtures/func/expected-with-sourcemap.js @@ -0,0 +1,5 @@ +'use strict'; +function add(a, b) { + return a + b; +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9hYnNvbHV0ZS9wYXRoL3RvL3Rlc3QvZml4dHVyZXMvZnVuYy9maXh0dXJlLmpzIl0sIm5hbWVzIjpbImFkZCIsImEiLCJiIl0sIm1hcHBpbmdzIjoiQUFBQTtBQUlBLFNBQVNBLEdBQVQsQ0FBY0MsQ0FBZCxFQUFpQkMsQ0FBakIsRUFBb0I7QUFBQSxJQUtoQixPQUFPRCxDQUFBLEdBQUlDLENBQVgsQ0FMZ0I7QUFBQSIsInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxudmFyIGFzc2VydCA9IHJlcXVpcmUoJ2Fzc2VydCcpO1xuXG5mdW5jdGlvbiBhZGQgKGEsIGIpIHtcbiAgICBjb25zb2xlLmFzc2VydCh0eXBlb2YgYSA9PT0gJ251bWJlcicpO1xuICAgIGFzc2VydCghaXNOYU4oYSkpO1xuICAgIGFzc2VydC5lcXVhbCh0eXBlb2YgYiwgJ251bWJlcicpO1xuICAgIGFzc2VydC5vayghaXNOYU4oYikpO1xuICAgIHJldHVybiBhICsgYjtcbn1cbiJdfQ== diff --git a/test/test.js b/test/test.js index 080cecb..8eba2c6 100644 --- a/test/test.js +++ b/test/test.js @@ -4,9 +4,11 @@ var unassertify = require('..'); var fs = require('fs'); var path = require('path'); var Stream = require('stream'); -var assert = require('assert'); +var assert = require('power-assert'); var browserify = require('browserify'); +var coffeeify = require('coffeeify'); var es = require('event-stream'); +var convert = require('convert-source-map'); describe('unassertify', function () { @@ -22,10 +24,72 @@ describe('unassertify', function () { done(); })); }); + it('produces sourcemap when debug: true', function (done) { + var b = browserify({debug: true}); + var testFilepath = path.normalize(path.join(__dirname, 'fixtures', 'func', 'fixture.js')); + b.add(testFilepath); + b.transform(unassertify); + b.bundle().pipe(es.wait(function(err, data) { + assert(!err); + var code = data.toString('utf-8'); + // console.log(code); + assert(! /assert/.test(code)); + var inlineMap = convert.fromSource(code); + assert(inlineMap); + var sourceMap = inlineMap.toObject(); + assert(sourceMap); + // console.log(JSON.stringify(sourceMap, null, 2)); + assert(sourceMap.sources.some(function (fpath) { return fpath === testFilepath; })); + done(); + })); + }); +}); + + +describe('with preceding transform', function () { + it('just remove assertions and dependencies when debug: false', function (done) { + var b = browserify(); + b.add(path.normalize(path.join(__dirname, 'fixtures', 'coffee', 'fixture.coffee'))); + b.transform(coffeeify); + b.transform(unassertify); + b.bundle().pipe(es.wait(function(err, data) { + assert(!err); + var code = data.toString('utf-8'); + // console.log(code); + var inlineMap = convert.fromSource(code); + assert(!inlineMap); + assert(! /require\('assert'\)/.test(code)); + done(); + })); + }); + it('adjust sourcemap if debug: true', function (done) { + var b = browserify({debug: true}); + var testFilepath = path.normalize(path.join(__dirname, 'fixtures', 'coffee', 'fixture.coffee')); + b.add(testFilepath); + b.transform(coffeeify); + b.transform(unassertify); + b.bundle().pipe(es.wait(function(err, data) { + assert(!err); + var code = data.toString('utf-8'); + // console.log(code); + assert(! /require\('assert'\)/.test(code)); + var inlineMap = convert.fromSource(code); + assert(inlineMap); + var sourceMap = inlineMap.toObject(); + assert(sourceMap); + // console.log(JSON.stringify(sourceMap, null, 2)); + assert(sourceMap.sources.some(function (fpath) { return fpath === testFilepath; })); + var originalCode = fs.readFileSync(testFilepath, 'utf-8'); + assert(sourceMap.sourcesContent.some(function (eachCode) { + return eachCode === originalCode; + })); + done(); + })); + }); }); -describe('do nothing if debug: true', function() { +describe('adjust sourcemap if debug: true', function() { var stream = unassertify( '/absolute/path/to/test/fixtures/func/fixture.js', { @@ -47,7 +111,7 @@ describe('do nothing if debug: true', function() { output += buf; }); stream.on('end', function() { - var expected = fs.readFileSync('test/fixtures/func/fixture.js', 'utf8'); + var expected = fs.readFileSync('test/fixtures/func/expected-with-sourcemap.js', 'utf8'); assert.equal(output, expected); done(); }); @@ -57,7 +121,7 @@ describe('do nothing if debug: true', function() { }); -describe('remove assertions if debug: false', function() { +describe('just remove assertions if debug: false', function() { var stream = unassertify( '/absolute/path/to/test/fixtures/func/fixture.js', {