diff --git a/README.md b/README.md index 6668fb4..53a0b6a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,34 @@ -# comment +# @gulp-sourcemaps/comment + +[![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][travis-image]][travis-url] [![AppVeyor Build Status][appveyor-image]][appveyor-url] [![Coveralls Status][coveralls-image]][coveralls-url] + Gulp plugin for working with the sourceMappingURL comment of a file. + +## Example + +```js +TODO +``` + +## API + +### TODO + +## License + +MIT + +[vinyl-url]: https://github.com/gulpjs/vinyl + +[downloads-image]: http://img.shields.io/npm/dm/@gulp-sourcemaps/comment.svg +[npm-url]: https://npmjs.org/package/@gulp-sourcemaps/comment +[npm-image]: http://img.shields.io/npm/v/@gulp-sourcemaps/comment.svg + +[travis-url]: https://travis-ci.org/gulp-sourcemaps/comment +[travis-image]: http://img.shields.io/travis/gulp-sourcemaps/comment.svg?label=travis-ci + +[appveyor-url]: https://ci.appveyor.com/project/phated/comment +[appveyor-image]: https://img.shields.io/appveyor/ci/phated/comment.svg?label=appveyor + +[coveralls-url]: https://coveralls.io/r/gulp-sourcemaps/comment +[coveralls-image]: http://img.shields.io/coveralls/gulp-sourcemaps/comment.svg diff --git a/index.js b/index.js new file mode 100644 index 0000000..3cb9c50 --- /dev/null +++ b/index.js @@ -0,0 +1,106 @@ +'use strict'; + +var path = require('path'); +var through = require('through2'); +var convert = require('convert-source-map'); +var normalize = require('normalize-path'); + +function processInline(contents, mapper) { + var sourceMappingURL = convert.getCommentValue(contents); + + var result = mapper(sourceMappingURL); + + if (!result) { + return convert.removeComments(contents); + } + + if (result !== sourceMappingURL) { + // TODO: use the same comment type as original + // TODO: we don't want to parse/convert so this works but should be named different + result = convert.generateMapFileComment(result); + // TODO: add `replaceComments` to convert-source-map + return contents.replace(convert.commentRegex, result); + } +} + +function processExternal(contents, mapper) { + var sourceMappingURL = convert.getMapFileCommentValue(contents); + + var result = mapper(sourceMappingURL); + + if (!result) { + return convert.removeMapFileComments(contents); + } + + if (result !== sourceMappingURL) { + // TODO: use the same comment type as original + result = convert.generateMapFileComment(result); + // TODO: add `replaceMapFileComments` to convert-source-map + return contents.replace(convert.mapFileCommentRegex, result); + } +} + +function comment(mapFn) { + + function transform(file, _, cb) { + // TODO: should this error? Probably not + if (!file.isBuffer()) { + return cb(null, file); + } + + var contents = file.contents.toString(); + + var hasInlineSourcemap = convert.commentRegex.test(contents); + var hasExternalSourcemap = convert.mapFileCommentRegex.test(contents); + + if (!hasInlineSourcemap && !hasExternalSourcemap) { + return cb(null, file); + } + + function mapper(sourceMappingURL) { + var result = sourceMappingURL; + if (typeof mapFn === 'function') { + result = mapFn(sourceMappingURL, file); + } + + // This is inverted because hasExternalSourcemap covers inline also + if (hasInlineSourcemap || !result) { + return result; + } + + return normalize(result); + } + + // Always one of the 2 because we bail if neither + var processor = hasInlineSourcemap ? processInline : processExternal; + + var result = processor(contents, mapper); + + if (result) { + file.contents = new Buffer(result); + } + + return cb(null, file); + } + + return through.obj(transform); +} + +function prefix(str) { + // TODO: should this somehow check if it is a path vs data-uri? + return comment(function(sourceMappingURL) { + // TODO: url instead of path? + return str + path.join('/', sourceMappingURL); + }); +} +comment.prefix = prefix; + +function remove() { + return comment(function() { + // Returning anything falsey removes the comment + return null; + }); +} +comment.remove = remove; + +module.exports = comment; diff --git a/package.json b/package.json index 7eb1185..14ea45d 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,17 @@ "index.js" ], "scripts": { - "lint": "eslint . && jscs index.js test/", + "lint": "eslint . && jscs index.js test/index.js", "pretest": "npm run lint", "test": "mocha --async-only", "cover": "istanbul cover _mocha --report lcovonly", "coveralls": "npm run cover && istanbul-coveralls" }, - "dependencies": {}, + "dependencies": { + "convert-source-map": "thlorenz/convert-source-map#comment-values", + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, "devDependencies": { "eslint": "^1.7.3", "eslint-config-gulp": "^2.0.0", diff --git a/test/fixtures/helloworld.js b/test/fixtures/helloworld.js new file mode 100644 index 0000000..9406b3e --- /dev/null +++ b/test/fixtures/helloworld.js @@ -0,0 +1,5 @@ +'use strict'; + +function helloWorld() { + console.log('Hello world!'); +} diff --git a/test/fixtures/helloworld.map.js b/test/fixtures/helloworld.map.js new file mode 100644 index 0000000..d6b7a05 --- /dev/null +++ b/test/fixtures/helloworld.map.js @@ -0,0 +1,7 @@ +'use strict'; + +function helloWorld() { + console.log('Hello world!'); +} + +//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIiwic291cmNlcyI6WyJoZWxsb3dvcmxkLmpzIl0sInNvdXJjZXNDb250ZW50IjpbIid1c2Ugc3RyaWN0JztcblxuZnVuY3Rpb24gaGVsbG9Xb3JsZCgpIHtcbiAgICBjb25zb2xlLmxvZygnSGVsbG8gd29ybGQhJyk7XG59XG4iXSwiZmlsZSI6ImhlbGxvd29ybGQuanMifQ== diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..f4dd5ad --- /dev/null +++ b/test/index.js @@ -0,0 +1,703 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +var expect = require('expect'); + +var miss = require('mississippi'); +var File = require('vinyl'); +var normalize = require('normalize-path'); +var convert = require('convert-source-map'); + +var comment = require('../'); + +var pipe = miss.pipe; +var from = miss.from; +var concat = miss.concat; + +// TODO: use buffer directly? +var sourceContent = fs.readFileSync(path.join(__dirname, 'fixtures/helloworld.js'), 'utf8'); +var mappedContent = fs.readFileSync(path.join(__dirname, 'fixtures/helloworld.map.js'), 'utf8'); +var inlineMap = convert.getCommentValue(mappedContent); + +function makeFile() { + var file = new File({ + cwd: __dirname, + base: __dirname + '/assets', + path: __dirname + '/assets/helloworld.js', + contents: new Buffer(''), + }); + + file.sourceMap = { + version: 3, + file: 'helloworld.js', + names: [], + mappings: '', + sources: ['helloworld.js'], + }; + + return file; +} + +function makeExternalMapFile() { + var file = makeFile(); + file.contents = new Buffer(sourceContent + '\n//# sourceMappingURL=helloworld.js.map'); + return file; +} + +function makeInlineMapFile() { + var file = makeFile(); + file.contents = new Buffer(mappedContent); + return file; +} + +describe('comment', function() { + + // TODO: is this proper behavior? + it('ignores file if no comment', function(done) { + var file = makeFile(); + + var spy = expect.createSpy(); + + function assert(files) { + expect(files.length).toEqual(1); + expect(spy).toNotHaveBeenCalled(); + } + + pipe([ + from.obj([file]), + comment(spy), + concat(assert), + ], done); + }); + + it('ignores file if not Buffer contents', function(done) { + var file = makeFile(); + file.contents = null; + + var spy = expect.createSpy(); + + function assert(files) { + expect(files.length).toEqual(1); + expect(spy).toNotHaveBeenCalled(); + } + + pipe([ + from.obj([file]), + comment(spy), + concat(assert), + ], done); + }); + + it('only ignores a file without sourceMappingURL comment', function(done) { + var file = makeExternalMapFile(); + var file2 = makeFile(); + + function mapFn(sourceMappingURL) { + return sourceMappingURL; + } + + var spy = expect.createSpy().andCall(mapFn); + + function assert(files) { + expect(files.length).toEqual(2); + expect(spy.calls.length).toEqual(1); + } + + pipe([ + from.obj([file, file2]), + comment(spy), + concat(assert), + ], done); + }); + + it('ignores a file with invalid sourceMappingURL comment', function(done) { + var file = makeFile(); + file.contents = new Buffer(sourceContent + '\n//# sourceMappingURL='); + + var spy = expect.createSpy(); + + function assert(files) { + expect(files.length).toEqual(1); + expect(spy).toNotHaveBeenCalled(); + } + + pipe([ + from.obj([file]), + comment(spy), + concat(assert), + ], done); + }); + + describe('with external sourceMappingURL', function() { + + it('does not care about the file.sourceMap property', function(done) { + var file = makeExternalMapFile(); + delete file.sourceMap; + + function mapFn(sourceMappingURL) { + return sourceMappingURL; + } + + var spy = expect.createSpy().andCall(mapFn); + + function assert(files) { + expect(files.length).toEqual(1); + expect(spy).toHaveBeenCalled(); + } + + pipe([ + from.obj([file]), + comment(spy), + concat(assert), + ], done); + }); + + it('calls map function per file', function(done) { + var file = makeExternalMapFile(); + + function mapFn(sourceMappingURL) { + return '/test/' + sourceMappingURL; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual('/test/helloworld.js.map'); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('normalizes Windows paths to unix paths', function(done) { + var file = makeExternalMapFile(); + + function mapFn(sourceMappingURL) { + return '\\test\\' + sourceMappingURL; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual('/test/helloworld.js.map'); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('does not need a map function', function(done) { + var file = makeExternalMapFile(); + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual('helloworld.js.map'); + } + + pipe([ + from.obj([file]), + comment(), + concat(assert), + ], done); + }); + + it('ignores non-function argument', function(done) { + var file = makeExternalMapFile(); + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual('helloworld.js.map'); + } + + pipe([ + from.obj([file]), + comment('invalid argument'), + concat(assert), + ], done); + }); + + it('still normalizes without a map function', function(done) { + var file = makeFile(); + file.contents = new Buffer(sourceContent + '\n//# sourceMappingURL=\\test\\helloworld.js.map'); + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual('/test/helloworld.js.map'); + } + + pipe([ + from.obj([file]), + comment(), + concat(assert), + ], done); + }); + + it('calls map function with the sourceMappingURL value and the vinyl file', function(done) { + var file = makeExternalMapFile(); + + function mapFn(sourceMappingURL, file) { + expect(File.isVinyl(file)).toEqual(true); + + return file.base + '/' + sourceMappingURL; + } + + function assert(files) { + expect(files.length).toEqual(1); + + var file = files[0]; + var base = normalize(file.base); + var comment = convert.getMapFileCommentValue(file.contents.toString()); + expect(comment).toEqual(base + '/helloworld.js.map'); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('removes the comment if null is returned from the map function', function(done) { + var file = makeExternalMapFile(); + + function mapFn() { + return null; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('removes the comment if undefined is returned from the map function', function(done) { + var file = makeExternalMapFile(); + + function mapFn() { + return undefined; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('removes the comment if false is returned from the map function', function(done) { + var file = makeExternalMapFile(); + + function mapFn() { + return false; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('removes the comment if an empty string is returned from the map function', function(done) { + var file = makeExternalMapFile(); + + function mapFn() { + return ''; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('changes the buffer reference when values are different', function(done) { + var file = makeExternalMapFile(); + var contents = file.contents; + + function mapFn(sourceMappingURL) { + return '/test/' + sourceMappingURL; + } + + function assert(files) { + expect(files.length).toEqual(1); + expect(files[0].contents).toNotBe(contents); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('keeps the buffer reference when values are same', function(done) { + var file = makeExternalMapFile(); + var contents = file.contents; + + function mapFn(sourceMappingURL) { + return sourceMappingURL; + } + + function assert(files) { + expect(files.length).toEqual(1); + expect(files[0].contents).toBe(contents); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + }); + + describe('with inline sourceMappingURL', function() { + + it('does not care about the file.sourceMap property', function(done) { + var file = makeInlineMapFile(); + delete file.sourceMap; + + function mapFn(sourceMappingURL) { + return sourceMappingURL; + } + + var spy = expect.createSpy().andCall(mapFn); + + function assert(files) { + expect(files.length).toEqual(1); + expect(spy).toHaveBeenCalled(); + } + + pipe([ + from.obj([file]), + comment(spy), + concat(assert), + ], done); + }); + + it('calls map function per file', function(done) { + var file = makeInlineMapFile(); + + function mapFn(sourceMappingURL) { + return sourceMappingURL.replace('utf8', 'utf-8'); + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toContain('utf-8'); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('does not normalize mapped sourceMappingURLs', function(done) { + var file = makeInlineMapFile(); + + function mapFn(sourceMappingURL) { + // Don't do this at home; things WILL break + return sourceMappingURL + '\\'; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toContain('\\'); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('does not need a map function', function(done) { + var file = makeInlineMapFile(); + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toEqual(inlineMap); + } + + pipe([ + from.obj([file]), + comment(), + concat(assert), + ], done); + }); + + it('ignores non-function argument', function(done) { + var file = makeInlineMapFile(); + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toEqual(inlineMap); + } + + pipe([ + from.obj([file]), + comment('invalid argument'), + concat(assert), + ], done); + }); + + it('does not normalize without a map function', function(done) { + var file = makeFile(); + // Don't do this at home; things WILL break + file.contents = new Buffer(sourceContent + '\n//# sourceMappingURL=' + inlineMap + '\\'); + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toContain('\\'); + } + + pipe([ + from.obj([file]), + comment(), + concat(assert), + ], done); + }); + + it('calls map function with the sourceMappingURL value and the vinyl file', function(done) { + var file = makeInlineMapFile(); + + function mapFn(sourceMappingURL, file) { + expect(File.isVinyl(file)).toEqual(true); + + return sourceMappingURL.replace('utf8', 'utf-8'); + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toContain('utf-8'); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('removes the comment if null is returned from the map function', function(done) { + var file = makeInlineMapFile(); + + function mapFn() { + return null; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('removes the comment if undefined is returned from the map function', function(done) { + var file = makeInlineMapFile(); + + function mapFn() { + return undefined; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('removes the comment if false is returned from the map function', function(done) { + var file = makeInlineMapFile(); + + function mapFn() { + return false; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('removes the comment if an empty string is returned from the map function', function(done) { + var file = makeInlineMapFile(); + + function mapFn() { + return ''; + } + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('changes the buffer reference when values are different', function(done) { + var file = makeInlineMapFile(); + var contents = file.contents; + + function mapFn(sourceMappingURL) { + return sourceMappingURL.replace('utf8', 'utf-8'); + } + + function assert(files) { + expect(files.length).toEqual(1); + expect(files[0].contents).toNotBe(contents); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + + it('keeps the buffer reference when values are same', function(done) { + var file = makeInlineMapFile(); + var contents = file.contents; + + function mapFn(sourceMappingURL) { + return sourceMappingURL; + } + + function assert(files) { + expect(files.length).toEqual(1); + expect(files[0].contents).toBe(contents); + } + + pipe([ + from.obj([file]), + comment(mapFn), + concat(assert), + ], done); + }); + }); +}); + +describe('comment.prefix', function() { + + it('prefixes the sourceMappingURL with the string provided', function(done) { + var file = makeExternalMapFile(); + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual('/test/helloworld.js.map'); + } + + pipe([ + from.obj([file]), + comment.prefix('/test'), + concat(assert), + ], done); + }); +}); + +describe('comment.remove', function() { + + it('removes an external sourcemap comment', function(done) { + var file = makeExternalMapFile(); + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getMapFileCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment.remove(), + concat(assert), + ], done); + }); + + it('removes an inline sourcemap comment', function(done) { + var file = makeInlineMapFile(); + + function assert(files) { + expect(files.length).toEqual(1); + var comment = convert.getCommentValue(files[0].contents.toString()); + expect(comment).toEqual(null); + } + + pipe([ + from.obj([file]), + comment.remove(), + concat(assert), + ], done); + }); +});