Fixes #599 - inlined source maps.
authorAndrew Bradley <abradley@brightcove.com>
Fri, 12 Jun 2015 19:47:16 +0000 (15:47 -0400)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Tue, 23 Jun 2015 05:44:36 +0000 (06:44 +0100)
Enables ingestion of inline, data-uri source maps.

History.md
lib/utils/input-source-map-tracker.js
test/source-map-test.js

index 17a94ae..56f54f8 100644 (file)
@@ -1,3 +1,8 @@
+[3.4.0 / 2015-xx-xx](https://github.com/jakubpawlowicz/clean-css/compare/v3.3.3...master)
+==================
+
+* Fixed issue [#599](https://github.com/jakubpawlowicz/clean-css/issues/599) - support for inlined source maps.
+
 [3.3.3 / 2015-06-16](https://github.com/jakubpawlowicz/clean-css/compare/v3.3.2...v3.3.3)
 ==================
 
index f74e664..7db9eb9 100644 (file)
@@ -10,6 +10,9 @@ var override = require('../utils/object.js').override;
 
 var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//;
 var REMOTE_RESOURCE = /^(https?:)?\/\//;
+var DATA_URI = /^data:(\S*?)?(;charset=[^;]+)?(;[^,]+?)?,(.+)/;
+
+var unescape = global.unescape;
 
 function InputSourceMapStore(outerContext) {
   this.options = outerContext.options;
@@ -63,14 +66,23 @@ function fromSource(self, data, whenDone, context) {
       context.files.pop();
     } else if (nextAt == mapMatch.index) {
       var isRemote = /^https?:\/\//.test(sourceMapFile) || /^\/\//.test(sourceMapFile);
+      var isDataUri = DATA_URI.test(sourceMapFile);
+
       if (isRemote) {
         return fetchMapFile(self, sourceMapFile, context, proceedToNext);
       } else {
         var sourceFile = context.files[context.files.length - 1];
+        var sourceMapPath, sourceMapData;
         var sourceDir = sourceFile ? path.dirname(sourceFile) : self.options.relativeTo;
-        var sourceMapPath = path.resolve(self.options.root, path.join(sourceDir || '', sourceMapFile));
 
-        var sourceMapData = fs.readFileSync(sourceMapPath, 'utf-8');
+        if (isDataUri) {
+          // source map's path is the same as the source file it comes from
+          sourceMapPath = path.resolve(self.options.root, sourceFile || '');
+          sourceMapData = fromDataUri(sourceMapFile);
+        } else {
+          sourceMapPath = path.resolve(self.options.root, path.join(sourceDir || '', sourceMapFile));
+          sourceMapData = fs.readFileSync(sourceMapPath, 'utf-8');
+        }
         self.trackLoaded(sourceFile || undefined, sourceMapPath, sourceMapData);
       }
     }
@@ -81,6 +93,17 @@ function fromSource(self, data, whenDone, context) {
   return whenDone();
 }
 
+function fromDataUri(uriString) {
+  var match = DATA_URI.exec(uriString);
+  var charset = match[2] ? match[2].split(/[=;]/)[2] : 'us-ascii';
+  var encoding = match[3] ? match[3].split(';')[1] : 'utf8';
+  var data = encoding == 'utf8' ? unescape(match[4]) : match[4];
+
+  var buffer = new Buffer(data, encoding);
+  buffer.charset = charset;
+
+  return buffer.toString();
+}
 
 function fetchMapFile(self, sourceUrl, context, done) {
   fetch(self, sourceUrl, function (data) {
index 7f474c9..c4a1ac0 100644 (file)
@@ -16,6 +16,7 @@ var enableDestroy = require('server-destroy');
 var port = 24682;
 
 var lineBreak = require('os').EOL;
+var escape = global.escape;
 
 vows.describe('source-map')
   .addBatch({
@@ -548,6 +549,13 @@ vows.describe('source-map')
         assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       }
     },
+    'input map as inlined data URI with implicit charset us-ascii, not base64, no content-type': inlineDataUriContext('data:,' + escape(inputMap)),
+    'input map as inlined data URI with implicit charset us-ascii, base64': inlineDataUriContext('data:application/json;base64,' + new Buffer(inputMap, 'ascii').toString('base64')),
+    'input map as inlined data URI with implicit charset us-ascii, not base64': inlineDataUriContext('data:application/json,' + escape(inputMap)),
+    'input map as inlined data URI with charset utf-8, base64': inlineDataUriContext('data:application/json;charset=utf-8;base64,' + new Buffer(inputMap, 'utf8').toString('base64')),
+    'input map as inlined data URI with charset utf-8, not base64': inlineDataUriContext('data:application/json;charset=utf-8,' + escape(String.fromCharCode.apply(String, new Buffer(inputMap, 'utf8')))),
+    'input map as inlined data URI with explicit charset us-ascii, base64': inlineDataUriContext('data:application/json;charset=us-ascii;base64,' + new Buffer(inputMap, 'ascii').toString('base64')),
+    'input map as inlined data URI with explicit charset us-ascii, not base64': inlineDataUriContext('data:application/json;charset=us-ascii,' + escape(inputMap)),
     'complex input map': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true, root: path.dirname(inputMapPath) }).minify('@import url(import.css);');
@@ -1918,3 +1926,47 @@ vows.describe('source-map')
     }
   })
   .export(module);
+
+  function inlineDataUriContext(dataUri) {
+    return {
+      'topic': function() {
+        return new CleanCSS({sourceMap: true}).minify('div > a {\n  color: red;\n}/*# sourceMappingURL=' + dataUri + ' */');
+      },
+      'has 3 mappings': function(minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
+      },
+      'has `div > a` mapping': function(minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 0,
+          originalLine: 1,
+          originalColumn: 4,
+          source: 'styles.less',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
+      },
+      'has `color` mapping': function(minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 6,
+          originalLine: 2,
+          originalColumn: 2,
+          source: 'styles.less',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+      },
+      'has second `color` mapping': function(minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 12,
+          originalLine: 2,
+          originalColumn: 2,
+          source: 'styles.less',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
+      }
+    };
+  }