From: Jakub Pawlowicz Date: Sun, 15 Mar 2015 08:42:15 +0000 (+0000) Subject: Fixes #397 - support for source map's sourcesContent property. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=9c74966ff7bc2a0fafcf94277a8e5ac321b4a0cc;p=clean-css.git Fixes #397 - support for source map's sourcesContent property. 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. --- diff --git a/History.md b/History.md index de7983df..c264ed8b 100644 --- a/History.md +++ b/History.md @@ -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) diff --git a/README.md b/README.md index 920fd7c9..2e7db480 100644 --- 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 diff --git a/bin/cleancss b/bin/cleancss index ef7d6d16..58983193 100755 --- a/bin/cleancss +++ b/bin/cleancss @@ -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 }; diff --git a/lib/clean.js b/lib/clean.js index 018992ee..73a6bc36 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -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); } diff --git a/lib/imports/inliner.js b/lib/imports/inliner.js index 8414bfd1..a58c26d9 100644 --- a/lib/imports/inliner.js +++ b/lib/imports/inliner.js @@ -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 + '}'; diff --git a/lib/selectors/source-map-stringifier.js b/lib/selectors/source-map-stringifier.js index 367edf68..d09b2b07 100644 --- a/lib/selectors/source-map-stringifier.js +++ b/lib/selectors/source-map-stringifier.js @@ -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) { diff --git a/lib/utils/input-source-map-tracker.js b/lib/utils/input-source-map-tracker.js index 40d312f6..3644399c 100644 --- a/lib/utils/input-source-map-tracker.js +++ b/lib/utils/input-source-map-tracker.js @@ -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; diff --git a/lib/utils/source-maps.js b/lib/utils/source-maps.js index 979515d6..bb2af88c 100644 --- a/lib/utils/source-maps.js +++ b/lib/utils/source-maps.js @@ -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) diff --git a/lib/utils/source-reader.js b/lib/utils/source-reader.js index 0440b146..9393157c 100644 --- a/lib/utils/source-reader.js +++ b/lib/utils/source-reader.js @@ -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); } diff --git a/test/binary-test.js b/test/binary-test.js index 2c4317af..4f58e612 100644 --- a/test/binary-test.js +++ b/test/binary-test.js @@ -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'); + } }) } }); diff --git a/test/selectors/tokenizer-source-maps-test.js b/test/selectors/tokenizer-source-maps-test.js index b38ff754..95f41e9a 100644 --- a/test/selectors/tokenizer-source-maps-test.js +++ b/test/selectors/tokenizer-source-maps-test.js @@ -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}'); }, [{ diff --git a/test/source-map-test.js b/test/source-map-test.js index e1471477..37f3e9ba 100644 --- a/test/source-map-test.js +++ b/test/source-map-test.js @@ -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': {