Skip to content

Commit

Permalink
feat!: Support URI encoded source maps (#75)
Browse files Browse the repository at this point in the history
fix!: No longer allow colon between charset and encoding
feat!: Add capture groups to the commentRegex

Co-authored-by: Blaine Bublitz <[email protected]>
  • Loading branch information
prantlf and phated authored Oct 15, 2022
1 parent 2572a2f commit e6b18c4
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 26 deletions.
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@ Returns source map converter from given object.

Returns source map converter from given json string.

### fromURI(uri)

Returns source map converter from given uri encoded json string.

### fromBase64(base64)

Returns source map converter from given base64 encoded json string.

### fromComment(comment)

Returns source map converter from given base64 encoded json string prefixed with `//# sourceMappingURL=...`.
Returns source map converter from given base64 or uri encoded json string prefixed with `//# sourceMappingURL=...`.

### fromMapFileComment(comment, mapFileDir)

Expand All @@ -50,11 +54,11 @@ generated file, i.e. the one containing the source map.

### fromSource(source)

Finds last sourcemap comment in file and returns source map converter or returns null if no source map comment was found.
Finds last sourcemap comment in file and returns source map converter or returns `null` if no source map comment was found.

### fromMapFileSource(source, mapFileDir)

Finds last sourcemap comment in file and returns source map converter or returns null if no source map comment was
Finds last sourcemap comment in file and returns source map converter or returns `null` if no source map comment was
found.

The sourcemap will be read from the map file found by parsing `# sourceMappingURL=file` comment. For more info see
Expand All @@ -70,6 +74,10 @@ Converts source map to json string. If `space` is given (optional), this will be
[JSON.stringify](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/stringify) when the
JSON string is generated.

### toURI()

Converts source map to uri encoded json string.

### toBase64()

Converts source map to base64 encoded json string.
Expand All @@ -81,6 +89,8 @@ Converts source map to an inline comment that can be appended to the source-file
By default, the comment is formatted like: `//# sourceMappingURL=...`, which you would
normally see in a JS source file.

When `options.encoding == 'uri'`, the data will be uri encoded, otherwise they will be base64 encoded.

When `options.multiline == true`, the comment is formatted like: `/*# sourceMappingURL=... */`, which you would find in a CSS source file.

### addProperty(key, value)
Expand All @@ -107,6 +117,8 @@ Returns `src` with all source map comments pointing to map files removed.

Provides __a fresh__ RegExp each time it is accessed. Can be used to find source map comments.

Breaks down a source map comment into groups: Groups: 1: media type, 2: MIME type, 3: charset, 4: encoding, 5: data.

### mapFileCommentRegex

Provides __a fresh__ RegExp each time it is accessed. Can be used to find source map comments pointing to map files.
Expand Down
55 changes: 44 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ var path = require('path');

Object.defineProperty(exports, 'commentRegex', {
get: function getCommentRegex () {
return /^\s*?\/(?:\/|\*?)[@#]\s+?sourceMappingURL=data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(?:.*?)$/mg;
// Groups: 1: media type, 2: MIME type, 3: charset, 4: encoding, 5: data.
return /^\s*?\/[\/\*][@#]\s+?sourceMappingURL=data:(((?:application|text)\/json)(?:;charset=([^;,]+?)?)?)?(?:;(base64))?,(.*?)$/mg;
}
});


Object.defineProperty(exports, 'mapFileCommentRegex', {
get: function getMapFileCommentRegex () {
// Matches sourceMappingURL in either // or /* comment styles.
Expand Down Expand Up @@ -64,10 +66,23 @@ function readFromFileMap(sm, dir) {
function Converter (sm, opts) {
opts = opts || {};

if (opts.isFileComment) sm = readFromFileMap(sm, opts.commentFileDir);
if (opts.hasComment) sm = stripComment(sm);
if (opts.isEncoded) sm = decodeBase64(sm);
if (opts.isJSON || opts.isEncoded) sm = JSON.parse(sm);
if (opts.isFileComment) {
sm = readFromFileMap(sm, opts.commentFileDir);
}

if (opts.hasComment) {
sm = stripComment(sm);
}

if (opts.encoding === 'base64') {
sm = decodeBase64(sm);
} else if (opts.encoding === 'uri') {
sm = decodeURIComponent(sm);
}

if (opts.isJSON || opts.encoding) {
sm = JSON.parse(sm);
}

this.sourcemap = sm;
}
Expand Down Expand Up @@ -104,10 +119,22 @@ function encodeBase64WithBtoa() {
return btoa(unescape(encodeURIComponent(json)));
}

Converter.prototype.toURI = function () {
var json = this.toJSON();
return encodeURIComponent(json);
};

Converter.prototype.toComment = function (options) {
var base64 = this.toBase64();
var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64;
return options && options.multiline ? '/*# ' + data + ' */' : '//# ' + data;
var encoding, content, data;
if (options != null && options.encoding === 'uri') {
encoding = '';
content = this.toURI();
} else {
encoding = ';base64';
content = this.toBase64();
}
data = 'sourceMappingURL=data:application/json;charset=utf-8' + encoding + ',' + content;
return options != null && options.multiline ? '/*# ' + data + ' */' : '//# ' + data;
};

// returns copy instead of original
Expand Down Expand Up @@ -137,16 +164,22 @@ exports.fromJSON = function (json) {
return new Converter(json, { isJSON: true });
};

exports.fromURI = function (uri) {
return new Converter(uri, { encoding: 'uri' });
};

exports.fromBase64 = function (base64) {
return new Converter(base64, { isEncoded: true });
return new Converter(base64, { encoding: 'base64' });
};

exports.fromComment = function (comment) {
var m, encoding;
comment = comment
.replace(/^\/\*/g, '//')
.replace(/\*\/$/g, '');

return new Converter(comment, { isEncoded: true, hasComment: true });
m = exports.commentRegex.exec(comment);
encoding = m && m[4] || 'uri';
return new Converter(comment, { encoding: encoding, hasComment: true });
};

exports.fromMapFileComment = function (comment, dir) {
Expand Down
155 changes: 144 additions & 11 deletions test/comment-regex.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,29 @@ function comment(prefix, suffix) {
return rx.test(prefix + 'sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJmdW5jdGlvbiBmb28oKSB7XG4gY29uc29sZS5sb2coXCJoZWxsbyBJIGFtIGZvb1wiKTtcbiBjb25zb2xlLmxvZyhcIndobyBhcmUgeW91XCIpO1xufVxuXG5mb28oKTtcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9' + suffix)
}

function commentWithCharSet(prefix, suffix, sep) {
sep = sep || ':';
function commentWithCharSet(prefix, suffix) {
var rx = convert.commentRegex;
return rx.test(prefix + 'sourceMappingURL=data:application/json;charset' + sep +'utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJmdW5jdGlvbiBmb28oKSB7XG4gY29uc29sZS5sb2coXCJoZWxsbyBJIGFtIGZvb1wiKTtcbiBjb25zb2xlLmxvZyhcIndobyBhcmUgeW91XCIpO1xufVxuXG5mb28oKTtcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9' + suffix)
return rx.test(prefix + 'sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJmdW5jdGlvbiBmb28oKSB7XG4gY29uc29sZS5sb2coXCJoZWxsbyBJIGFtIGZvb1wiKTtcbiBjb25zb2xlLmxvZyhcIndobyBhcmUgeW91XCIpO1xufVxuXG5mb28oKTtcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9' + suffix)
}

function commentURI(prefix, suffix) {
var rx = convert.commentRegex;
return rx.test(prefix + 'sourceMappingURL=data:application/json,%7B%22version%22%3A3%2C%22file%22%3A%22%22%2C%22sources%22%3A%5B%22function%20foo()%20%7B%0A%20console.log(%22hello%20I%20am%20foo%22)%3B%0A%20console.log(%22who%20are%20you%22)%3B%0A%7D%0A%0Afoo()%3B%0A%22%5D%2C%22names%22%3A%5B%5D%2C%22mappings%22%3A%22AAAA%22%7D' + suffix)
}

function commentURIWithCharSet(prefix, suffix) {
var rx = convert.commentRegex;
return rx.test(prefix + 'sourceMappingURL=data:application/json;charset=utf-8,%7B%22version%22%3A3%2C%22file%22%3A%22%22%2C%22sources%22%3A%5B%22function%20foo()%20%7B%0A%20console.log(%22hello%20I%20am%20foo%22)%3B%0A%20console.log(%22who%20are%20you%22)%3B%0A%7D%0A%0Afoo()%3B%0A%22%5D%2C%22names%22%3A%5B%5D%2C%22mappings%22%3A%22AAAA%22%7D' + suffix)
}

function commentWithoutMediaType(prefix, suffix) {
var rx = convert.commentRegex;
return rx.test(prefix + 'sourceMappingURL=data:;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJmdW5jdGlvbiBmb28oKSB7XG4gY29uc29sZS5sb2coXCJoZWxsbyBJIGFtIGZvb1wiKTtcbiBjb25zb2xlLmxvZyhcIndobyBhcmUgeW91XCIpO1xufVxuXG5mb28oKTtcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9' + suffix)
}

function commentURIWithoutMediaType(prefix, suffix) {
var rx = convert.commentRegex;
return rx.test(prefix + 'sourceMappingURL=data:,%7B%22version%22%3A3%2C%22file%22%3A%22%22%2C%22sources%22%3A%5B%22function%20foo()%20%7B%0A%20console.log(%22hello%20I%20am%20foo%22)%3B%0A%20console.log(%22who%20are%20you%22)%3B%0A%7D%0A%0Afoo()%3B%0A%22%5D%2C%22names%22%3A%5B%5D%2C%22mappings%22%3A%22AAAA%22%7D' + suffix)
}

// Source Map v2 Tests
Expand All @@ -28,15 +47,21 @@ test('comment regex old spec - @', function (t) {
'\t/*@ ', // multi line style with leading tab
'/*@ ', // multi line style with leading text
].forEach(function (x) {
t.ok(comment(x, ''), 'matches ' + x)
t.ok(commentWithCharSet(x, ''), 'matches ' + x + ' with charset')
t.ok(commentWithCharSet(x, '', '='), 'matches ' + x + ' with charset')
t.ok(comment(x, ''), 'matches ' + x)
t.ok(commentWithCharSet(x, ''), 'matches ' + x + ' with charset')
t.ok(commentURI(x, ''), 'matches ' + x + ' uri')
t.ok(commentURIWithCharSet(x, ''), 'matches ' + x + ' uri with charset')
t.ok(commentWithoutMediaType(x, ''), 'matches ' + x + ' without media type')
t.ok(commentURIWithoutMediaType(x, ''), 'matches ' + x + ' uri without media type')
});

[
' @// @',
' @/* @',
].forEach(function (x) { t.ok(!comment(x, ''), 'should not match ' + x) })
].forEach(function (x) {
t.ok(!comment(x, ''), 'should not match ' + x)
t.ok(!commentURI(x, ''), 'should not match ' + x + ' uri')
})

t.end()
})
Expand All @@ -51,15 +76,123 @@ test('comment regex new spec - #', function (t) {
'\t/*# ', // multi line style with leading tab
'/*# ', // multi line style with leading text
].forEach(function (x) {
t.ok(comment(x, ''), 'matches ' + x)
t.ok(commentWithCharSet(x, ''), 'matches ' + x + ' with charset')
t.ok(commentWithCharSet(x, '', '='), 'matches ' + x + ' with charset')
t.ok(comment(x, ''), 'matches ' + x)
t.ok(commentWithCharSet(x, ''), 'matches ' + x + ' with charset')
t.ok(commentURI(x, ''), 'matches ' + x + ' uri')
t.ok(commentURIWithCharSet(x, ''), 'matches ' + x + ' uri with charset')
t.ok(commentWithoutMediaType(x, ''), 'matches ' + x + ' without media type')
t.ok(commentURIWithoutMediaType(x, ''), 'matches ' + x + ' uri without media type')
});

[
' #// #',
' #/* #',
].forEach(function (x) { t.ok(!comment(x, ''), 'should not match ' + x) })
].forEach(function (x) {
t.ok(!comment(x, ''), 'should not match ' + x)
t.ok(!commentURI(x, ''), 'should not match ' + x + ' uri')
})

t.end()
})

test('comment regex groups', function (t) {
function comment(prefix, suffix) {
var rx = convert.commentRegex;
return rx.exec(prefix + 'sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJmdW5jdGlvbiBmb28oKSB7XG4gY29uc29sZS5sb2coXCJoZWxsbyBJIGFtIGZvb1wiKTtcbiBjb25zb2xlLmxvZyhcIndobyBhcmUgeW91XCIpO1xufVxuXG5mb28oKTtcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9' + suffix)
}

function commentURI(prefix, suffix) {
var rx = convert.commentRegex;
return rx.exec(prefix + 'sourceMappingURL=data:application/json,%7B%22version%22%3A3%2C%22file%22%3A%22%22%2C%22sources%22%3A%5B%22function%20foo()%20%7B%0A%20console.log(%22hello%20I%20am%20foo%22)%3B%0A%20console.log(%22who%20are%20you%22)%3B%0A%7D%0A%0Afoo()%3B%0A%22%5D%2C%22names%22%3A%5B%5D%2C%22mappings%22%3A%22AAAA%22%7D' + suffix)
}

function commentWithCharSet(prefix, suffix) {
var rx = convert.commentRegex;
return rx.exec(prefix + 'sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJmdW5jdGlvbiBmb28oKSB7XG4gY29uc29sZS5sb2coXCJoZWxsbyBJIGFtIGZvb1wiKTtcbiBjb25zb2xlLmxvZyhcIndobyBhcmUgeW91XCIpO1xufVxuXG5mb28oKTtcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9' + suffix)
}

function commentURIWithCharSet(prefix, suffix) {
var rx = convert.commentRegex;
return rx.exec(prefix + 'sourceMappingURL=data:application/json;charset=utf-8,%7B%22version%22%3A3%2C%22file%22%3A%22%22%2C%22sources%22%3A%5B%22function%20foo()%20%7B%0A%20console.log(%22hello%20I%20am%20foo%22)%3B%0A%20console.log(%22who%20are%20you%22)%3B%0A%7D%0A%0Afoo()%3B%0A%22%5D%2C%22names%22%3A%5B%5D%2C%22mappings%22%3A%22AAAA%22%7D' + suffix)
}

function commentWithoutMediaType(prefix, suffix) {
var rx = convert.commentRegex;
return rx.exec(prefix + 'sourceMappingURL=data:;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJmdW5jdGlvbiBmb28oKSB7XG4gY29uc29sZS5sb2coXCJoZWxsbyBJIGFtIGZvb1wiKTtcbiBjb25zb2xlLmxvZyhcIndobyBhcmUgeW91XCIpO1xufVxuXG5mb28oKTtcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9' + suffix)
}

function commentURIWithoutMediaType(prefix, suffix) {
var rx = convert.commentRegex;
return rx.exec(prefix + 'sourceMappingURL=data:,%7B%22version%22%3A3%2C%22file%22%3A%22%22%2C%22sources%22%3A%5B%22function%20foo()%20%7B%0A%20console.log(%22hello%20I%20am%20foo%22)%3B%0A%20console.log(%22who%20are%20you%22)%3B%0A%7D%0A%0Afoo()%3B%0A%22%5D%2C%22names%22%3A%5B%5D%2C%22mappings%22%3A%22AAAA%22%7D' + suffix)
}

[
' //# ', // with leading spaces
'\t//# ', // with leading tab
'//# ', // with leading text
'/*# ', // multi line style
' /*# ', // multi line style with leading spaces
'\t/*# ', // multi line style with leading tab
'/*# ', // multi line style with leading text
].forEach(function (x) {
var m;
m = comment(x, '')
t.ok(m, 'matches ' + x)
t.ok(m[0], 'comment')
t.equal(m[1], 'application/json', 'media type')
t.equal(m[2], 'application/json', 'MIME type')
t.equal(m[3], undefined, 'undefined charset')
t.equal(m[4], 'base64', 'base64 encoding')
t.ok(m[5], 'data')
m = commentURI(x, '')
t.ok(m, 'matches ' + x + ' uri')
t.ok(m[0], 'comment uri')
t.equal(m[1], 'application/json', 'media type uri')
t.equal(m[2], 'application/json', 'MIME type uri')
t.equal(m[3], undefined, 'undefined charset uri')
t.equal(m[4], undefined, 'undefined encoding uri')
t.ok(m[5], 'data uri')
m = commentWithCharSet(x, '')
t.ok(m, 'matches ' + x + ' with charset')
t.ok(m[0], 'comment with charset')
t.equal(m[1], 'application/json;charset=utf-8', 'media type with charset')
t.equal(m[2], 'application/json', 'MIME type with charset')
t.equal(m[3], 'utf-8', 'charset with utf-8')
t.equal(m[4], 'base64', 'base64 encoding with charset')
t.ok(m[5], 'data with charset')
m = commentURIWithCharSet(x, '')
t.ok(m, 'matches ' + x + ' uri with charset')
t.ok(m[0], 'comment uri with charset')
t.equal(m[1], 'application/json;charset=utf-8', 'media type uri with charset')
t.equal(m[2], 'application/json', 'MIME type uri with charset')
t.equal(m[3], 'utf-8', 'charset uri with utf-8')
t.equal(m[4], undefined, 'undefined encoding uri with charset')
t.ok(m[5], 'data with charset')
m = commentWithoutMediaType(x, '')
t.ok(m, 'matches ' + x + ' without media type')
t.ok(m[0], 'comment without media type')
t.equal(m[1], undefined, 'undefined media type')
t.equal(m[2], undefined, 'undefined MIME type')
t.equal(m[3], undefined, 'undefined charset without media type')
t.equal(m[4], 'base64', 'base64 encoding without media type')
t.ok(m[5], 'data without media type')
m = commentURIWithoutMediaType(x, '')
t.ok(m, 'matches ' + x + ' uri without media type')
t.ok(m[0], 'comment uri without media type')
t.equal(m[1], undefined, 'undefined media type')
t.equal(m[2], undefined, 'undefined MIME type')
t.equal(m[3], undefined, 'undefined charset uri without media type')
t.equal(m[4], undefined, 'undefined encoding uri without media type')
t.ok(m[5], 'data uri without media type')
});

[
' #// #',
' #/* #',
].forEach(function (x) {
t.ok(!comment(x, ''), 'should not match ' + x)
t.ok(!commentURI(x, ''), 'should not match ' + x + ' uri')
})

t.end()
})
Expand Down
19 changes: 18 additions & 1 deletion test/convert-source-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,46 @@ var test = require('tap').test
, generator = require('inline-source-map')
, convert = require('..')

function decodeBase64WithBufferFrom(base64) {
return Buffer.from(base64, 'base64').toString();
}

function decodeBase64WithNewBuffer(base64) {
return new Buffer(base64, 'base64').toString();
}

var gen = generator({charset:"utf-8"})
.addMappings('foo.js', [{ original: { line: 2, column: 3 } , generated: { line: 5, column: 10 } }], { line: 5 })
.addGeneratedMappings('bar.js', 'var a = 2;\nconsole.log(a)', { line: 23, column: 22 })

, base64 = gen.base64Encode()
, decodeBase64 = typeof Buffer.from ? decodeBase64WithBufferFrom : decodeBase64WithNewBuffer
, uri = encodeURIComponent(decodeBase64(base64))
, comment = gen.inlineMappingUrl()
, comment2 = '//# sourceMappingURL=data:application/json;charset=utf-8,' + uri
, json = gen.toString()
, obj = JSON.parse(json)

test('different formats', function (t) {

t.equal(convert.fromComment(comment).toComment(), comment, 'comment -> comment')
t.equal(convert.fromComment(comment).toComment(), comment, 'comment -> comment (base64)')
t.equal(convert.fromComment(comment2).toComment({ encoding: 'uri' }), comment2, 'comment -> comment (uri)')
t.equal(convert.fromComment(comment).toBase64(), base64, 'comment -> base64')
t.equal(convert.fromComment(comment).toURI(), uri, 'comment -> uri')
t.equal(convert.fromComment(comment).toJSON(), json, 'comment -> json')
t.deepEqual(convert.fromComment(comment).toObject(), obj, 'comment -> object')

t.equal(convert.fromBase64(base64).toBase64(), base64, 'base64 -> base64')
t.equal(convert.fromURI(uri).toURI(), uri, 'uri -> uri')
t.equal(convert.fromBase64(base64).toComment(), comment, 'base64 -> comment')
t.equal(convert.fromBase64(base64).toJSON(), json, 'base64 -> json')
t.equal(convert.fromURI(uri).toJSON(), json, 'uri -> json')
t.deepEqual(convert.fromBase64(base64).toObject(), obj, 'base64 -> object')
t.deepEqual(convert.fromURI(uri).toObject(), obj, 'uri -> object')

t.equal(convert.fromJSON(json).toJSON(), json, 'json -> json')
t.equal(convert.fromJSON(json).toBase64(), base64, 'json -> base64')
t.equal(convert.fromJSON(json).toURI(), uri, 'json -> uri')
t.equal(convert.fromJSON(json).toComment(), comment, 'json -> comment')
t.deepEqual(convert.fromJSON(json).toObject(), obj, 'json -> object')
t.end()
Expand Down

0 comments on commit e6b18c4

Please sign in to comment.