Fixes #397 - support for source map's sourcesContent property.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Sun, 15 Mar 2015 08:42:15 +0000 (08:42 +0000)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Mon, 16 Mar 2015 20:55:18 +0000 (20:55 +0000)
When handling input source map it checks whether sourcesContent is present
and if it is so then it's reused in the output source map.

Adds `sourceMapInlineSources` / `--source-map-inline-source` switches to
control whether an inlined source map is created or not.

In case an input source map with a `sourcesContent` field is provided
then all sources from that source map are carried over to the output source map.

12 files changed:
History.md
README.md
bin/cleancss
lib/clean.js
lib/imports/inliner.js
lib/selectors/source-map-stringifier.js
lib/utils/input-source-map-tracker.js
lib/utils/source-maps.js
lib/utils/source-reader.js
test/binary-test.js
test/selectors/tokenizer-source-maps-test.js
test/source-map-test.js

index de7983d..c264ed8 100644 (file)
@@ -6,6 +6,7 @@
 * Makes `root` option implicitely default to `process.cwd()`.
 * Fixed issue [#376](https://github.com/jakubpawlowicz/clean-css/issues/376) - option to disable `0[unit]` -> `0`.
 * Fixed issue [#396](https://github.com/jakubpawlowicz/clean-css/issues/396) - better input source maps tracking.
+* Fixed issue [#397](https://github.com/jakubpawlowicz/clean-css/issues/397) - support for source map sources.
 * Fixed issue [#480](https://github.com/jakubpawlowicz/clean-css/issues/480) - extracting uppercase property names.
 
 [3.1.7 / 2015-03-16](https://github.com/jakubpawlowicz/clean-css/compare/v3.1.6...v3.1.7)
index 920fd7c..2e7db48 100644 (file)
--- a/README.md
+++ b/README.md
@@ -65,6 +65,7 @@ cleancss [options] source-file, [source-file, ...]
 --rounding-precision [N]        Rounds to `N` decimal places. Defaults to 2. -1 disables rounding.
 -c, --compatibility [ie7|ie8]   Force compatibility mode (see Readme for advanced examples)
 --source-map                    Enables building input's source map
+--source-map-inline-sources     Enables inlining sources inside source maps
 -d, --debug                     Shows debug information (minification time & compression efficiency)
 ```
 
@@ -130,6 +131,8 @@ CleanCSS constructor accepts a hash as a parameter, i.e.,
 * `shorthandCompacting` - set to false to skip shorthand compacting (default is true unless sourceMap is set when it's false)
 * `sourceMap` - exposes source map under `sourceMap` property, e.g. `new CleanCSS().minify(source).sourceMap` (default is false)
   If input styles are a product of CSS preprocessor (LESS, SASS) an input source map can be passed as a string.
+* `sourceMapInlineSources` - set to true to inline sources inside a source map (default is false)
+  It is also required to process inlined sources from input source maps.
 * `target` - path to a folder or an output file to which __rebase__ all URLs
 
 #### How to make sure remote `@import`s are processed correctly?
@@ -267,7 +270,6 @@ new CleanCSS({ sourceMap: true, target: pathToOutputDirectory }).minify({
 #### Caveats
 
 * Shorthand compacting is currently disabled when source maps are enabled, see [#399](https://github.com/GoalSmashers/clean-css/issues/399)
-* Sources inlined in source maps are not supported, see [#397](https://github.com/GoalSmashers/clean-css/issues/397)
 
 ### How to minify multiple files with API
 
index ef7d6d1..5898319 100755 (executable)
@@ -30,6 +30,7 @@ commands
   .option('--rounding-precision [n]', 'Rounds to `N` decimal places. Defaults to 2. -1 disables rounding.', parseInt)
   .option('-c, --compatibility [ie7|ie8]', 'Force compatibility mode (see Readme for advanced examples)')
   .option('--source-map', 'Enables building input\'s source map')
+  .option('--source-map-inline-sources', 'Enables inlining sources inside source maps')
   .option('-t, --timeout [seconds]', 'Per connection timeout when fetching remote @imports (defaults to 5 seconds)')
   .option('-d, --debug', 'Shows debug information (minification time & compression efficiency)');
 
@@ -73,6 +74,7 @@ var options = {
   roundingPrecision: commands.roundingPrecision,
   shorthandCompacting: commands.skipShorthandCompacting ? false : true,
   sourceMap: commands.sourceMap,
+  sourceMapInlineSources: commands.sourceMapInlineSources,
   target: commands.output
 };
 
index 018992e..73a6bc3 100644 (file)
@@ -49,6 +49,7 @@ var CleanCSS = module.exports = function CleanCSS(options) {
     roundingPrecision: options.roundingPrecision,
     shorthandCompacting: !!options.sourceMap ? false : (undefined === options.shorthandCompacting ? true : !!options.shorthandCompacting),
     sourceMap: options.sourceMap,
+    sourceMapInlineSources: !!options.sourceMapInlineSources,
     target: options.target && fs.existsSync(options.target) && fs.statSync(options.target).isDirectory() ? options.target : path.dirname(options.target)
   };
 
@@ -63,13 +64,15 @@ CleanCSS.prototype.minify = function(data, callback) {
     warnings: [],
     options: this.options,
     debug: this.options.debug,
+    localOnly: !callback,
     sourceTracker: new SourceTracker()
   };
 
   if (context.options.sourceMap)
     context.inputSourceMapTracker = new InputSourceMapTracker(context);
 
-  data = new SourceReader(context, data).toString();
+  context.sourceReader = new SourceReader(context, data);
+  data = context.sourceReader.toString();
 
   if (context.options.processImport || data.indexOf('@shallow') > 0) {
     // inline all imports
@@ -79,7 +82,7 @@ CleanCSS.prototype.minify = function(data, callback) {
 
     return runner(function () {
       return new ImportInliner(context).process(data, {
-        localOnly: !callback,
+        localOnly: context.localOnly,
         whenDone: runMinifier(callback, context)
       });
     });
@@ -102,7 +105,15 @@ function runMinifier(callback, context) {
 
   return function (data) {
     if (context.options.sourceMap) {
-      return context.inputSourceMapTracker.track(data, function () { return whenSourceMapReady(data); });
+      return context.inputSourceMapTracker.track(data, function () {
+        if (context.options.sourceMapInlineSources) {
+          return context.inputSourceMapTracker.resolveSources(function () {
+            return whenSourceMapReady(data);
+          });
+        } else {
+          return whenSourceMapReady(data);
+        }
+      });
     } else {
       return whenSourceMapReady(data);
     }
index 8414bfd..a58c26d 100644 (file)
@@ -28,6 +28,7 @@ ImportInliner.prototype.process = function (data, context) {
     rebase: this.outerContext.options.rebase,
     relativeTo: this.outerContext.options.relativeTo || root,
     root: root,
+    sourceReader: this.outerContext.sourceReader,
     sourceTracker: this.outerContext.sourceTracker,
     warnings: this.outerContext.warnings,
     visited: []
@@ -261,6 +262,7 @@ function inlineRemoteResource(importedFile, mediaQuery, context) {
       var importedData = chunks.join('');
       if (context.rebase)
         importedData = new UrlRewriter({ toBase: importedUrl }, context).process(importedData);
+      context.sourceReader.trackSource(importedUrl, importedData);
       importedData = context.sourceTracker.store(importedUrl, importedData);
       importedData = rebaseMap(importedData, importedUrl);
 
@@ -315,7 +317,10 @@ function inlineLocalResource(importedFile, mediaQuery, context) {
     }, context);
     importedData = rewriter.process(importedData);
   }
-  importedData = context.sourceTracker.store(path.relative(context.root, fullPath), importedData);
+
+  var relativePath = path.relative(context.root, fullPath);
+  context.sourceReader.trackSource(relativePath, importedData);
+  importedData = context.sourceTracker.store(relativePath, importedData);
 
   if (mediaQuery.length > 0)
     importedData = '@media ' + mediaQuery + '{' + importedData + '}';
index 367edf6..d09b2b0 100644 (file)
@@ -8,6 +8,7 @@ function Rebuilder(options, restoreCallback, inputMapTracker) {
   this.line = 1;
   this.output = [];
   this.keepBreaks = options.keepBreaks;
+  this.sourceMapInlineSources = options.sourceMapInlineSources;
   this.restore = restoreCallback;
   this.inputMapTracker = inputMapTracker;
   this.outputMap = new SourceMapGenerator();
@@ -88,14 +89,19 @@ Rebuilder.prototype.track = function (value, metadata) {
 };
 
 Rebuilder.prototype.trackMetadata = function (metadata) {
+  var source = metadata.source || SourceMap.unknownSource;
+
   this.outputMap.addMapping({
     generated: {
       line: this.line,
       column: this.column
     },
-    source: metadata.source || SourceMap.unknownSource,
+    source: source,
     original: metadata.original
   });
+
+  if (metadata.sourcesContent)
+    this.outputMap.setSourceContent(source, metadata.sourcesContent[metadata.source]);
 };
 
 function SourceMapStringifier(options, restoreCallback, inputMapTracker) {
index 40d312f..3644399 100644 (file)
@@ -14,11 +14,15 @@ var REMOTE_RESOURCE = /^(https?:)?\/\//;
 function InputSourceMapStore(outerContext) {
   this.options = outerContext.options;
   this.errors = outerContext.errors;
+  this.warnings = outerContext.warnings;
   this.sourceTracker = outerContext.sourceTracker;
   this.timeout = this.options.inliner.timeout;
   this.requestOptions = this.options.inliner.request;
+  this.localOnly = outerContext.localOnly;
+  this.relativeTo = outerContext.options.target || process.cwd();
 
   this.maps = {};
+  this.sourcesContent = {};
 }
 
 function fromString(self, _, whenDone) {
@@ -77,34 +81,48 @@ function fromSource(self, data, whenDone, context) {
   return whenDone();
 }
 
+
 function fetchMapFile(self, sourceUrl, context, done) {
-  function handleError(status) {
-    context.errors.push('Broken source map at "' + sourceUrl + '" - ' + status);
+  fetch(self, sourceUrl, function (data) {
+    self.trackLoaded(context.files[context.files.length - 1] || undefined, sourceUrl, data);
+    done();
+  }, function (message) {
+    context.errors.push('Broken source map at "' + sourceUrl + '" - ' + message);
     return done();
-  }
+  });
+}
 
-  var method = sourceUrl.indexOf('https') === 0 ? https : http;
-  var requestOptions = override(url.parse(sourceUrl), self.requestOptions);
+function fetch(self, path, onSuccess, onFailure) {
+  var protocol = path.indexOf('https') === 0 ? https : http;
+  var requestOptions = override(url.parse(path), self.requestOptions);
+  var errorHandled = false;
 
-  method
+  protocol
     .get(requestOptions, function (res) {
       if (res.statusCode < 200 || res.statusCode > 299)
-        return handleError(res.statusCode);
+        return onFailure(res.statusCode);
 
       var chunks = [];
       res.on('data', function (chunk) {
         chunks.push(chunk.toString());
       });
       res.on('end', function () {
-        self.trackLoaded(context.files[context.files.length - 1] || undefined, sourceUrl, chunks.join(''));
-        done();
+        onSuccess(chunks.join(''));
       });
     })
     .on('error', function(res) {
-      handleError(res.message);
+      if (errorHandled)
+        return;
+
+      onFailure(res.message);
+      errorHandled = true;
     })
     .on('timeout', function() {
-      handleError('timeout');
+      if (errorHandled)
+        return;
+
+      onFailure('timeout');
+      errorHandled = true;
     })
     .setTimeout(self.timeout);
 }
@@ -139,6 +157,57 @@ function originalPositionIn(trackedSource, sourceInfo, token, allowNFallbacks) {
   return originalPosition;
 }
 
+function trackContentSources(self, sourceFile) {
+  var consumer = self.maps[sourceFile].data;
+  var isRemote = REMOTE_RESOURCE.test(sourceFile);
+  var sourcesMapping = {};
+
+  consumer.sources.forEach(function (file, index) {
+    var uniquePath = isRemote ?
+      url.resolve(path.dirname(sourceFile), file) :
+      path.relative(self.relativeTo, path.resolve(path.dirname(sourceFile), file));
+
+    sourcesMapping[uniquePath] = consumer.sourcesContent && consumer.sourcesContent[index];
+  });
+  self.sourcesContent[sourceFile] = sourcesMapping;
+}
+
+function _resolveSources(self, remaining, whenDone) {
+  function processNext() {
+    return _resolveSources(self, remaining, whenDone);
+  }
+
+  if (remaining.length === 0)
+    return whenDone();
+
+  var current = remaining.shift();
+  var sourceFile = current[0];
+  var originalFile = current[1];
+  var isRemote = REMOTE_RESOURCE.test(sourceFile);
+
+  if (isRemote && self.localOnly) {
+    self.warnings.push('No callback given to `#minify` method, cannot fetch a remote file from "' + originalFile + '"');
+    return processNext();
+  }
+
+  if (isRemote) {
+    fetch(self, originalFile, function (data) {
+      self.sourcesContent[sourceFile][originalFile] = data;
+      processNext();
+    }, function (message) {
+      self.warnings.push('Broken original source file at "' + originalFile + '" - ' + message);
+      processNext();
+    });
+  } else {
+    var fullPath = path.join(self.options.root, originalFile);
+    if (fs.existsSync(fullPath))
+      self.sourcesContent[sourceFile][originalFile] = fs.readFileSync(fullPath, 'utf-8');
+    else
+      self.warnings.push('Missing original source file at "' + fullPath + '".');
+    return processNext();
+  }
+}
+
 InputSourceMapStore.prototype.track = function (data, whenDone) {
   return typeof this.options.sourceMap == 'string' ?
     fromString(this, data, whenDone) :
@@ -159,6 +228,8 @@ InputSourceMapStore.prototype.trackLoaded = function (sourcePath, mapPath, mapDa
     path: mapPath,
     data: new SourceMapConsumer(mapData)
   };
+
+  trackContentSources(this, sourcePath);
 };
 
 InputSourceMapStore.prototype.isTracking = function (source) {
@@ -169,4 +240,22 @@ InputSourceMapStore.prototype.originalPositionFor = function (sourceInfo, token,
   return originalPositionIn(this.maps[sourceInfo.source], sourceInfo.original, token, allowNFallbacks);
 };
 
+InputSourceMapStore.prototype.sourcesContentFor = function (contextSource) {
+  return this.sourcesContent[contextSource];
+};
+
+InputSourceMapStore.prototype.resolveSources = function (whenDone) {
+  var toResolve = [];
+
+  for (var sourceFile in this.sourcesContent) {
+    var contents = this.sourcesContent[sourceFile];
+    for (var originalFile in contents) {
+      if (!contents[originalFile])
+        toResolve.push([sourceFile, originalFile]);
+    }
+  }
+
+  return _resolveSources(this, toResolve, whenDone);
+};
+
 module.exports = InputSourceMapStore;
index 979515d..bb2af88 100644 (file)
@@ -50,6 +50,16 @@ var SourceMaps = {
       sourceMetadata.source :
       sourceFor(sourceMetadata, contextMetadata, context);
 
+    if (context.outer.options.sourceMapInlineSources) {
+      var sourceMapSourcesContent = context.outer.inputSourceMapTracker.sourcesContentFor(context.source);
+      var source = sourceMapSourcesContent && sourceMapSourcesContent[contextMetadata.source] ?
+        sourceMapSourcesContent :
+        context.outer.sourceReader.sourceAt(context.source);
+
+      if (source)
+        contextMetadata.sourcesContent = source;
+    }
+
     this.track(trimmedValue, context);
 
     if (hasSuffix)
index 0440b14..9393157 100644 (file)
@@ -6,61 +6,85 @@ var REMOTE_RESOURCE = /^(https?:)?\/\//;
 function SourceReader(context, data) {
   this.outerContext = context;
   this.data = data;
+  this.sources = {};
 }
 
+SourceReader.prototype.sourceAt = function (path) {
+  return this.sources[path];
+};
+
+SourceReader.prototype.trackSource = function (path, source) {
+  this.sources[path] = {};
+  this.sources[path][path] = source;
+};
+
 SourceReader.prototype.toString = function () {
   if (typeof this.data == 'string')
-    return this.data;
+    return fromString(this);
   if (Buffer.isBuffer(this.data))
-    return this.data.toString();
+    return fromBuffer(this);
   if (Array.isArray(this.data))
-    return fromArray(this.outerContext, this.data);
+    return fromArray(this);
 
-  return fromHash(this.outerContext, this.data);
+  return fromHash(this);
 };
 
-function fromArray(outerContext, sources) {
-  return sources
+function fromString(self) {
+  var data = self.data;
+  self.trackSource(undefined, data);
+  return data;
+}
+
+function fromBuffer(self) {
+  var data = self.data.toString();
+  self.trackSource(undefined, data);
+  return data;
+}
+
+function fromArray(self) {
+  return self.data
     .map(function (source) {
-      return outerContext.options.processImport === false ?
+      return self.outerContext.options.processImport === false ?
         source + '@shallow' :
         source;
     })
     .map(function (source) {
-      return !outerContext.options.relativeTo || /^https?:\/\//.test(source) ?
+      return !self.outerContext.options.relativeTo || /^https?:\/\//.test(source) ?
         source :
-        path.relative(outerContext.options.relativeTo, source);
+        path.relative(self.outerContext.options.relativeTo, source);
     })
     .map(function (source) { return '@import url(' + source + ');'; })
     .join('');
 }
 
-function fromHash(outerContext, sources) {
+function fromHash(self) {
   var data = [];
-  var toBase = path.resolve(outerContext.options.target || outerContext.options.root);
+  var toBase = path.resolve(self.outerContext.options.target || self.outerContext.options.root);
 
-  for (var source in sources) {
-    var styles = sources[source].styles;
-    var inputSourceMap = sources[source].sourceMap;
+  for (var source in self.data) {
+    var styles = self.data[source].styles;
+    var inputSourceMap = self.data[source].sourceMap;
     var isRemote = REMOTE_RESOURCE.test(source);
     var absoluteSource = isRemote ? source : path.resolve(source);
-    var absolutePath = path.dirname(absoluteSource);
+    var absoluteSourcePath = path.dirname(absoluteSource);
 
     var rewriter = new UrlRewriter({
-      absolute: outerContext.options.explicitRoot,
-      relative: !outerContext.options.explicitRoot,
+      absolute: self.outerContext.options.explicitRoot,
+      relative: !self.outerContext.options.explicitRoot,
       imports: true,
-      urls: outerContext.options.rebase,
-      fromBase: absolutePath,
-      toBase: isRemote ? absolutePath : toBase
-    }, this.outerContext);
+      urls: self.outerContext.options.rebase,
+      fromBase: absoluteSourcePath,
+      toBase: isRemote ? absoluteSourcePath : toBase
+    }, self.outerContext);
     styles = rewriter.process(styles);
 
-    if (outerContext.options.sourceMap && inputSourceMap) {
-      styles = outerContext.sourceTracker.store(source, styles);
-      // here we assume source map lies in the same directory as `source` does
-      outerContext.inputSourceMapTracker.trackLoaded(source, source, inputSourceMap);
-    }
+    self.trackSource(source, styles);
+
+    styles = self.outerContext.sourceTracker.store(source, styles);
+
+    // here we assume source map lies in the same directory as `source` does
+    if (self.outerContext.options.sourceMap && inputSourceMap)
+      self.outerContext.inputSourceMapTracker.trackLoaded(source, source, inputSourceMap);
 
     data.push(styles);
   }
index 2c4317a..4f58e61 100644 (file)
@@ -481,6 +481,22 @@ exports.commandsSuite = vows.describe('binary commands').addBatch({
         deleteFile('import.min.css');
         deleteFile('import.min.css.map');
       }
+    }),
+    'with input source map and source 1inlining': binaryContext('--source-map --source-map-inline-sources -o ./import-inline.min.css ./test/fixtures/source-maps/import.css', {
+      'includes map in minified file': function () {
+        assert.include(readFile('./import-inline.min.css'), '/*# sourceMappingURL=import-inline.min.css.map */');
+      },
+      'includes embedded sources': function () {
+        var sourceMap = new SourceMapConsumer(readFile('./import-inline.min.css.map'));
+        var count = 0;
+        sourceMap.eachMapping(function () { count++; });
+
+        assert.equal(count, 4);
+      },
+      'teardown': function () {
+        deleteFile('import-inline.min.css');
+        deleteFile('import-inline.min.css.map');
+      }
     })
   }
 });
index b38ff75..95f41e9 100644 (file)
@@ -2,6 +2,7 @@ var vows = require('vows');
 var assert = require('assert');
 var Tokenizer = require('../../lib/selectors/tokenizer');
 var SourceTracker = require('../../lib/utils/source-tracker');
+var SourceReader = require('../../lib/utils/source-reader');
 var InputSourceMapTracker = require('../../lib/utils/input-source-map-tracker');
 
 var fs = require('fs');
@@ -22,12 +23,22 @@ function sourceMapContext(group, specs) {
     for (var i = 0; i < specs[test][1].length; i++) {
       var target = specs[test][1][i];
       var sourceTracker = new SourceTracker();
-      var inputSourceMapTracker = new InputSourceMapTracker({ options: { inliner: {} }, errors: {}, sourceTracker: sourceTracker });
+      var sourceReader = new SourceReader();
+      var inputSourceMapTracker = new InputSourceMapTracker({
+        options: { inliner: {} },
+        errors: {},
+        sourceTracker: sourceTracker
+      });
 
       ctx[group + ' ' + test + ' - #' + (i + 1)] = {
         topic: typeof specs[test][0] == 'function' ?
           specs[test][0]() :
-          new Tokenizer({ sourceTracker: sourceTracker, inputSourceMapTracker: inputSourceMapTracker, options: {} }, false, true).toTokens(specs[test][0]),
+          new Tokenizer({
+            sourceTracker: sourceTracker,
+            sourceReader: sourceReader,
+            inputSourceMapTracker: inputSourceMapTracker,
+            options: {}
+          }, false, true).toTokens(specs[test][0]),
         tokenized: tokenizedContext(target, i)
       };
     }
@@ -444,8 +455,9 @@ vows.describe('source-maps/analyzer')
       'one': [
         function () {
           var tracker = new SourceTracker();
+          var reader = new SourceReader();
           var inputTracker = new InputSourceMapTracker({ options: { inliner: {} }, errors: {}, sourceTracker: tracker });
-          var tokenizer = new Tokenizer({ sourceTracker: tracker, inputSourceMapTracker: inputTracker, options: {} }, false, true);
+          var tokenizer = new Tokenizer({ sourceTracker: tracker, sourceReader: reader, inputSourceMapTracker: inputTracker, options: {} }, false, true);
 
           var data = tracker.store('one.css', 'a{}');
           return tokenizer.toTokens(data);
@@ -459,8 +471,9 @@ vows.describe('source-maps/analyzer')
       'two': [
         function () {
           var tracker = new SourceTracker();
+          var reader = new SourceReader();
           var inputTracker = new InputSourceMapTracker({ options: { inliner: {} }, errors: {}, sourceTracker: tracker });
-          var tokenizer = new Tokenizer({ sourceTracker: tracker, inputSourceMapTracker: inputTracker, options: {} }, false, true);
+          var tokenizer = new Tokenizer({ sourceTracker: tracker, sourceReader: reader, inputSourceMapTracker: inputTracker, options: {} }, false, true);
 
           var data1 = tracker.store('one.css', 'a{}');
           var data2 = tracker.store('two.css', '\na{color:red}');
@@ -490,10 +503,11 @@ vows.describe('source-maps/analyzer')
       'one': [
         function () {
           var tracker = new SourceTracker();
+          var reader = new SourceReader();
           var inputTracker = new InputSourceMapTracker({ options: { inliner: {}, sourceMap: inputMap, options: {} }, errors: {}, sourceTracker: tracker });
           inputTracker.track('', function () {});
 
-          var tokenizer = new Tokenizer({ sourceTracker: tracker, inputSourceMapTracker: inputTracker, options: {} }, false, true);
+          var tokenizer = new Tokenizer({ sourceTracker: tracker, sourceReader: reader, inputSourceMapTracker: inputTracker, options: {} }, false, true);
           return tokenizer.toTokens('div > a {\n  color: red;\n}');
         },
         [{
index e147147..37f3e9b 100644 (file)
@@ -15,6 +15,8 @@ var enableDestroy = require('server-destroy');
 
 var port = 24682;
 
+var lineBreak = require('os').EOL;
+
 vows.describe('source-map')
   .addBatch({
     'vendor prefix with comments': {
@@ -987,6 +989,504 @@ vows.describe('source-map')
       }
     }
   })
+  .addBatch({
+    'inlined sources': {
+      'from string - off': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true }).minify('div > a {\n  color: red;\n}');
+        },
+        'should have 2 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 2);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, ['$stdin']);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.isUndefined(JSON.parse(minified.sourceMap.toString()).sourcesContent);
+        },
+        'should have selector mapping': function (minified) {
+          var mapping = {
+            generatedLine: 1,
+            generatedColumn: 0,
+            originalLine: 1,
+            originalColumn: 0,
+            source: '$stdin',
+            name: null
+          };
+          assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
+        },
+        'should have _color:red_ mapping': function (minified) {
+          var mapping = {
+            generatedLine: 1,
+            generatedColumn: 6,
+            originalLine: 2,
+            originalColumn: 2,
+            source: '$stdin',
+            name: null
+          };
+          assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+        }
+      },
+      'from string - on': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify('div > a {\n  color: red;\n}');
+        },
+        'should have 2 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 2);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, ['$stdin']);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, ['div > a {\n  color: red;\n}']);
+        }
+      },
+      'from array - off': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true }).minify([
+            'test/fixtures/partials/one.css',
+            'test/fixtures/partials/three.css'
+          ]);
+        },
+        'should have 4 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 4);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            path.join('test', 'fixtures', 'partials', 'one.css'),
+            path.join('test', 'fixtures', 'partials', 'three.css')
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.isUndefined(JSON.parse(minified.sourceMap.toString()).sourcesContent);
+        }
+      },
+      'from array - on': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify([
+            'test/fixtures/partials/one.css',
+            'test/fixtures/partials/three.css'
+          ]);
+        },
+        'should have 4 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 4);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            path.join('test', 'fixtures', 'partials', 'one.css'),
+            path.join('test', 'fixtures', 'partials', 'three.css')
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
+            '.one { color:#f00; }' + lineBreak,
+            '.three {background-image: url(test/fixtures/partials/extra/down.gif);}' + lineBreak
+          ]);
+        }
+      },
+      'from array - on remote': {
+        'topic': function () {
+          this.reqMocks = nock('http://127.0.0.1')
+            .get('/some.css')
+            .reply(200, 'div{background:url(image.png)}');
+
+          new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify([
+            'http://127.0.0.1/some.css'
+          ], this.callback);
+        },
+        'should have 2 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 2);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            'http://127.0.0.1/some.css'
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
+            'div{background:url(http://127.0.0.1/image.png)}',
+          ]);
+        },
+        'teardown': function () {
+          assert.isTrue(this.reqMocks.isDone());
+          nock.cleanAll();
+        }
+      },
+      'from hash - off': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true }).minify({
+            'test/fixtures/source-maps/some.css': {
+              styles: 'div {\n  color: red;\n}'
+            },
+            'test/fixtures/source-maps/styles.css': {
+              styles: 'div > a {\n  color: blue;\n}'
+            },
+            'test/fixtures/source-maps/nested/once.css': {
+              styles: 'section > div a {\n  color: red;\n}'
+            }
+          });
+        },
+        'should have 5 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            'test/fixtures/source-maps/some.css',
+            'test/fixtures/source-maps/nested/once.css',
+            'test/fixtures/source-maps/styles.css'
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.isUndefined(JSON.parse(minified.sourceMap.toString()).sourcesContent);
+        }
+      },
+      'from hash - on': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify({
+            'test/fixtures/source-maps/some.css': {
+              styles: 'div {\n  color: red;\n}'
+            },
+            'test/fixtures/source-maps/styles.css': {
+              styles: 'div > a {\n  color: blue;\n}'
+            },
+            'test/fixtures/source-maps/nested/once.css': {
+              styles: 'section > div a {\n  color: red;\n}'
+            }
+          });
+        },
+        'should have 5 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            'test/fixtures/source-maps/some.css',
+            'test/fixtures/source-maps/nested/once.css',
+            'test/fixtures/source-maps/styles.css'
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
+            'div {\n  color: red;\n}',
+            'section > div a {\n  color: red;\n}',
+            'div > a {\n  color: blue;\n}'
+          ]);
+        }
+      }
+    }
+  })
+  .addBatch({
+    'inlined sources from source map(s)': {
+      'single': {
+        'topic': function () {
+          return new CleanCSS({
+            sourceMap: '{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GAAI;EACF,WAAA","file":"styles.css","sourcesContent":["div > a {\\n  color: blue;\\n}\\n"]}',
+            sourceMapInlineSources: true
+          }).minify('div > a {\n  color: red;\n}');
+        },
+        'should have 2 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 2);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, ['styles.less']);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, ['div > a {\n  color: blue;\n}\n']);
+        },
+        'should have selector 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);
+        },
+        'should have _color:red_ 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);
+        }
+      },
+      'multiple': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify({
+            'test/fixtures/source-maps/some.css': {
+              styles: 'div {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css","sourcesContent":["div {\\n  color: red;\\n}\\n"]}'
+            },
+            'test/fixtures/source-maps/styles.css': {
+              styles: 'div > a {\n  color: blue;\n}',
+              sourceMap: '{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GAAI;EACF,WAAA","file":"styles.css","sourcesContent":["div > a {\\n  color: blue;\\n}\\n"]}'
+            },
+            'test/fixtures/source-maps/nested/once.css': {
+              styles: 'section > div a {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["once.less"],"names":[],"mappings":"AAAA,OACE,MAAM;EACJ,UAAA","file":"once.css","sourcesContent":["section {\\n  > div a {\\n    color:red;\\n  }\\n}\\n"]}'
+            }
+          });
+        },
+        'should have 5 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            path.join('test', 'fixtures', 'source-maps', 'some.less'),
+            path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
+            path.join('test', 'fixtures', 'source-maps', 'styles.less')
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
+            'div {\n  color: red;\n}\n',
+            'section {\n  > div a {\n    color:red;\n  }\n}\n',
+            'div > a {\n  color: blue;\n}\n'
+          ]);
+        }
+      },
+      'multiple relative to a target path': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true, target: path.join(process.cwd(), 'test') }).minify({
+            'test/fixtures/source-maps/some.css': {
+              styles: 'div {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css","sourcesContent":["div {\\n  color: red;\\n}\\n"]}'
+            },
+            'test/fixtures/source-maps/styles.css': {
+              styles: 'div > a {\n  color: blue;\n}',
+              sourceMap: '{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GAAI;EACF,WAAA","file":"styles.css","sourcesContent":["div > a {\\n  color: blue;\\n}\\n"]}'
+            },
+            'test/fixtures/source-maps/nested/once.css': {
+              styles: 'section > div a {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["once.less"],"names":[],"mappings":"AAAA,OACE,MAAM;EACJ,UAAA","file":"once.css","sourcesContent":["section {\\n  > div a {\\n    color:red;\\n  }\\n}\\n"]}'
+            }
+          });
+        },
+        'should have 5 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            path.join('fixtures', 'source-maps', 'some.less'),
+            path.join('fixtures', 'source-maps', 'nested', 'once.less'),
+            path.join('fixtures', 'source-maps', 'styles.less')
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
+            'div {\n  color: red;\n}\n',
+            'section {\n  > div a {\n    color:red;\n  }\n}\n',
+            'div > a {\n  color: blue;\n}\n'
+          ]);
+        }
+      },
+      'mixed': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify({
+            'test/fixtures/source-maps/some.css': {
+              styles: 'div {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css","sourcesContent":["div {\\n  color: red;\\n}\\n"]}'
+            },
+            'test/fixtures/source-maps/styles.css': {
+              styles: 'div > a {\n  color: blue;\n}',
+              sourceMap: '{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GAAI;EACF,WAAA","file":"styles.css"}'
+            },
+            'test/fixtures/source-maps/nested/once.css': {
+              styles: 'section > div a {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["once.less"],"names":[],"mappings":"AAAA,OACE,MAAM;EACJ,UAAA","file":"once.css","sourcesContent":["section {\\n  > div a {\\n    color:red;\\n  }\\n}\\n"]}'
+            }
+          });
+        },
+        'should have 5 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            path.join('test', 'fixtures', 'source-maps', 'some.less'),
+            path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
+            path.join('test', 'fixtures', 'source-maps', 'styles.less')
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
+            'div {\n  color: red;\n}\n',
+            'section {\n  > div a {\n    color:red;\n  }\n}\n',
+            'div > a {' + lineBreak + '  color: blue;' + lineBreak + '}' + lineBreak
+          ]);
+        }
+      },
+      'mixed without inline sources switch': {
+        'topic': function () {
+          return new CleanCSS({ sourceMap: true }).minify({
+            'test/fixtures/source-maps/some.css': {
+              styles: 'div {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css","sourcesContent":["div {\\n  color: red;\\n}\\n"]}'
+            },
+            'test/fixtures/source-maps/styles.css': {
+              styles: 'div > a {\n  color: blue;\n}',
+              sourceMap: '{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GAAI;EACF,WAAA","file":"styles.css"}'
+            },
+            'test/fixtures/source-maps/nested/once.css': {
+              styles: 'section > div a {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["once.less"],"names":[],"mappings":"AAAA,OACE,MAAM;EACJ,UAAA","file":"once.css","sourcesContent":["section {\\n  > div a {\\n    color:red;\\n  }\\n}\\n"]}'
+            }
+          });
+        },
+        'should have 5 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            path.join('test', 'fixtures', 'source-maps', 'some.less'),
+            path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
+            path.join('test', 'fixtures', 'source-maps', 'styles.less')
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.isUndefined(JSON.parse(minified.sourceMap.toString()).sourcesContent);
+        }
+      },
+      'mixed remote': {
+        'topic': function () {
+          this.reqMocks = nock('http://127.0.0.1')
+            .get('/some.less')
+            .reply(200, 'div {\n  color: red;\n}\n')
+            .get('/styles.less')
+            .reply(200, 'div > a {\n  color: blue;\n}\n');
+
+          new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify({
+            'http://127.0.0.1/some.css': {
+              styles: 'div {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css"}'
+            },
+            'http://127.0.0.1/other/styles.css': {
+              styles: 'div > a {\n  color: blue;\n}',
+              sourceMap: '{"version":3,"sources":["../styles.less"],"names":[],"mappings":"AAAA,GAAI;EACF,WAAA","file":"styles.css"}'
+            },
+            'test/fixtures/source-maps/nested/once.css': {
+              styles: 'section > div a {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["once.less"],"names":[],"mappings":"AAAA,OACE,MAAM;EACJ,UAAA","file":"once.css"}'
+            }
+          }, this.callback);
+        },
+        'should have 5 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            'http://127.0.0.1/some.less',
+            path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
+            'http://127.0.0.1/styles.less'
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
+            'div {\n  color: red;\n}\n',
+            'section {' + lineBreak + '  > div a {' + lineBreak + '    color:red;' + lineBreak + '  }' + lineBreak + '}' + lineBreak,
+            'div > a {\n  color: blue;\n}\n'
+          ]);
+        },
+        'teardown': function () {
+          assert.isTrue(this.reqMocks.isDone());
+          nock.cleanAll();
+        }
+      },
+      'mixed remote and 404 resource': {
+        'topic': function () {
+          this.reqMocks = nock('http://127.0.0.1')
+            .get('/some.less')
+            .reply(404)
+            .get('/styles.less')
+            .reply(200, 'div > a {\n  color: blue;\n}\n');
+
+          new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify({
+            'http://127.0.0.1/some.css': {
+              styles: 'div {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css"}'
+            },
+            'http://127.0.0.1/other/styles.css': {
+              styles: 'div > a {\n  color: blue;\n}',
+              sourceMap: '{"version":3,"sources":["../styles.less"],"names":[],"mappings":"AAAA,GAAI;EACF,WAAA","file":"styles.css"}'
+            },
+            'test/fixtures/source-maps/nested/once.css': {
+              styles: 'section > div a {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["once.less"],"names":[],"mappings":"AAAA,OACE,MAAM;EACJ,UAAA","file":"once.css"}'
+            }
+          }, this.callback);
+        },
+        'should have 5 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        },
+        'should warn about some.less': function (minified) {
+          assert.deepEqual(minified.warnings, ['Broken original source file at "http://127.0.0.1/some.less" - 404']);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            'http://127.0.0.1/some.less',
+            path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
+            'http://127.0.0.1/styles.less'
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
+            null,
+            'section {' + lineBreak + '  > div a {' + lineBreak + '    color:red;' + lineBreak + '  }' + lineBreak + '}' + lineBreak,
+            'div > a {\n  color: blue;\n}\n'
+          ]);
+        },
+        'teardown': function () {
+          assert.isTrue(this.reqMocks.isDone());
+          nock.cleanAll();
+        }
+      },
+      'mixed remote and no callback': {
+        'topic': function () {
+           return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify({
+            'http://127.0.0.1/some.css': {
+              styles: 'div {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css"}'
+            },
+            'http://127.0.0.1/other/styles.css': {
+              styles: 'div > a {\n  color: blue;\n}',
+              sourceMap: '{"version":3,"sources":["../styles.less"],"names":[],"mappings":"AAAA,GAAI;EACF,WAAA","file":"styles.css"}'
+            },
+            'test/fixtures/source-maps/nested/once.css': {
+              styles: 'section > div a {\n  color: red;\n}',
+              sourceMap: '{"version":3,"sources":["once.less"],"names":[],"mappings":"AAAA,OACE,MAAM;EACJ,UAAA","file":"once.css"}'
+            }
+          });
+        },
+        'should have 5 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        },
+        'should warn about some.less and styles.less': function (minified) {
+          assert.deepEqual(minified.warnings, [
+            'No callback given to `#minify` method, cannot fetch a remote file from "http://127.0.0.1/some.less"',
+            'No callback given to `#minify` method, cannot fetch a remote file from "http://127.0.0.1/styles.less"'
+          ]);
+        },
+        'should have embedded sources': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
+            'http://127.0.0.1/some.less',
+            path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
+            'http://127.0.0.1/styles.less'
+          ]);
+        },
+        'should have embedded sources content': function (minified) {
+          assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
+            null,
+            'section {' + lineBreak + '  > div a {' + lineBreak + '    color:red;' + lineBreak + '  }' + lineBreak + '}' + lineBreak,
+            null
+          ]);
+        }
+      }
+    }
+  })
   .addBatch({
     'advanced optimizations': {
       'new property in smart sort': {