From 2c0d4d38d1ecc6551d5b92d0d61da5e2229d442f Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Wed, 12 Nov 2014 23:10:57 +0000 Subject: [PATCH] Adds tracking input source maps in imported files. * InputSourceMapTracker tracks source maps on per-file basis. --- lib/clean.js | 22 ++----- lib/imports/inliner.js | 10 ++- lib/selectors/source-map-stringifier.js | 13 ++-- lib/selectors/tokenizer.js | 30 +++++++-- lib/utils/input-source-map-tracker.js | 65 +++++++++++++++++++ test/data/source-maps/import.css | 2 + test/data/source-maps/some.css | 4 ++ test/data/source-maps/some.css.map | 1 + test/data/source-maps/styles.css | 4 ++ .../{sample.map => styles.css.map} | 2 +- test/selectors/tokenizer-source-maps-test.js | 6 +- test/source-map-test.js | 54 ++++++++++++++- 12 files changed, 176 insertions(+), 37 deletions(-) create mode 100644 lib/utils/input-source-map-tracker.js create mode 100644 test/data/source-maps/import.css create mode 100644 test/data/source-maps/some.css create mode 100644 test/data/source-maps/some.css.map create mode 100644 test/data/source-maps/styles.css rename test/data/source-maps/{sample.map => styles.css.map} (62%) diff --git a/lib/clean.js b/lib/clean.js index 7be19d33..29b111ba 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -17,11 +17,7 @@ var FreeTextProcessor = require('./text/free-text-processor'); var UrlsProcessor = require('./text/urls-processor'); var Compatibility = require('./utils/compatibility'); - -var fs = require('fs'); -var path = require('path'); - -var SOURCE_MAP_MARKER = '/*# sourceMappingURL='; +var InputSourceMapTracker = require('./utils/input-source-map-tracker'); var CleanCSS = module.exports = function CleanCSS(options) { options = options || {}; @@ -62,9 +58,6 @@ CleanCSS.prototype.minify = function(data, callback) { if (Buffer.isBuffer(data)) data = data.toString(); - if (data.indexOf(SOURCE_MAP_MARKER) > 0) - overrideSourceMap(self.options, data); - if (options.processImport || data.indexOf('@shallow') > 0) { // inline all imports var runner = callback ? @@ -84,14 +77,10 @@ CleanCSS.prototype.minify = function(data, callback) { } }; -function overrideSourceMap(options, source) { - var markerAt = source.indexOf(SOURCE_MAP_MARKER); - var inputMapPath = source.substring(markerAt + SOURCE_MAP_MARKER.length, source.indexOf('*/', markerAt)).trim(); - options.sourceMap = fs.readFileSync(path.join(options.root || '', inputMapPath), 'utf-8'); -} - function runMinifier(callback, self) { return function (data) { + self.inputSourceMapTracker = new InputSourceMapTracker(self.options).track(data); + data = self.options.debug ? minifyWithDebug(self, data) : minify.call(self, data); @@ -104,7 +93,7 @@ function runMinifier(callback, self) { function minifyWithDebug(self, data) { var startedAt = process.hrtime(); - self.stats.originalSize = data.replace(/__ESCAPED_SOURCE_CLEAN_CSS\(.+\)__/g, '').length; + self.stats.originalSize = data.replace(/__ESCAPED_SOURCE_CLEAN_CSS\(.+\)__/g, '').replace(/__ESCAPED_SOURCE_END_CLEAN_CSS__/g, '').length; data = minify.call(self, data); @@ -129,6 +118,7 @@ function benchmark(runner) { function minify(data) { var options = this.options; var context = this.context; + var sourceMapTracker = this.inputSourceMapTracker; var commentsProcessor = new CommentsProcessor(context, options.keepSpecialComments, options.keepBreaks, options.sourceMap); var expressionsProcessor = new ExpressionsProcessor(options.sourceMap); @@ -160,7 +150,7 @@ function minify(data) { data = options.rebase ? urlRebase.process(data) : data; data = expressionsProcessor.restore(data); return commentsProcessor.restore(data); - }); + }, sourceMapTracker); return selectorsOptimizer.process(data, stringifier); }); diff --git a/lib/imports/inliner.js b/lib/imports/inliner.js index 0a641658..c6541f2c 100644 --- a/lib/imports/inliner.js +++ b/lib/imports/inliner.js @@ -17,6 +17,12 @@ var merge = function(source1, source2) { return target; }; +function wrap(data, source) { + return '__ESCAPED_SOURCE_CLEAN_CSS(' + source + ')__' + + data + + '__ESCAPED_SOURCE_END_CLEAN_CSS__'; +} + module.exports = function Inliner(context, options) { var defaultOptions = { timeout: 5000, @@ -249,7 +255,7 @@ module.exports = function Inliner(context, options) { res.on('end', function() { var importedData = chunks.join(''); importedData = UrlRewriter.process(importedData, { toBase: importedUrl }); - importedData = '__ESCAPED_SOURCE_CLEAN_CSS(' + url + ')__' + importedData; + importedData = wrap(importedData, url); if (mediaQuery.length > 0) importedData = '@media ' + mediaQuery + '{' + importedData + '}'; @@ -301,7 +307,7 @@ module.exports = function Inliner(context, options) { fromBase: importRelativeTo, toBase: options._baseRelativeTo }); - importedData = '__ESCAPED_SOURCE_CLEAN_CSS(' + path.relative(options.root, fullPath) + ')__' + importedData; + importedData = wrap(importedData, path.relative(options.root, fullPath)); 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 86e46037..ba2c4fb8 100644 --- a/lib/selectors/source-map-stringifier.js +++ b/lib/selectors/source-map-stringifier.js @@ -1,15 +1,12 @@ -var SourceMapConsumer = require('source-map').SourceMapConsumer; var SourceMapGenerator = require('source-map').SourceMapGenerator; var lineBreak = require('os').EOL; -function SourceMapStringifier(options, restoreCallback) { +function SourceMapStringifier(options, restoreCallback, inputMapTracker) { this.keepBreaks = options.keepBreaks; this.restoreCallback = restoreCallback; this.outputMap = new SourceMapGenerator(); - this.inputMap = typeof options.sourceMap == 'string' ? - new SourceMapConsumer(options.sourceMap) : - null; + this.inputMapTracker = inputMapTracker; } function valueRebuilder(list, store, separator) { @@ -57,8 +54,8 @@ function rebuild(tokens, store, keepBreaks, isFlatBlock) { function track(context, value, metadata) { if (metadata) { - var original = context.inputMap ? - context.inputMap.originalPositionFor(metadata) : + var original = context.inputMapTracker.isTracking() ? + context.inputMapTracker.originalPositionFor(metadata) : {}; context.outputMap.addMapping({ @@ -86,7 +83,7 @@ SourceMapStringifier.prototype.toString = function (tokens) { var context = { column: 1, line: 1, - inputMap: this.inputMap, + inputMapTracker: this.inputMapTracker, outputMap: this.outputMap }; diff --git a/lib/selectors/tokenizer.js b/lib/selectors/tokenizer.js index 2a45316e..a72053a0 100644 --- a/lib/selectors/tokenizer.js +++ b/lib/selectors/tokenizer.js @@ -25,6 +25,7 @@ Tokenizer.prototype.toTokens = function (data) { outer: this.minifyContext, addMetadata: this.addMetadata, addSourceMap: this.addSourceMap, + state: [], line: 1, column: 1, source: undefined @@ -154,16 +155,35 @@ function tokenize(context) { } else if (what == 'escape') { nextEnd = chunk.indexOf('__', nextSpecial + 1); var escaped = chunk.substring(context.cursor, nextEnd + 2); - var isSourceMarker = escaped.indexOf('__ESCAPED_SOURCE_CLEAN_CSS') > -1; + var isStartSourceMarker = escaped.indexOf('__ESCAPED_SOURCE_CLEAN_CSS') > -1; + var isEndSourceMarker = escaped.indexOf('__ESCAPED_SOURCE_END_CLEAN_CSS') > -1; - if (isSourceMarker) { + if (isStartSourceMarker) { + if (addSourceMap) + SourceMaps.track(escaped, context); + + context.state.push({ + source: context.source, + line: context.line, + column: context.column + }); context.source = escaped.substring(escaped.indexOf('(') + 1, escaped.indexOf(')')); + context.line = 1; + context.column = 1; + } else if (isEndSourceMarker) { + var oldState = context.state.pop(); + context.source = oldState.source; + context.line = oldState.line; + context.column = oldState.column; + + if (addSourceMap) + SourceMaps.track(escaped, context); } else { tokenized.push({ kind: 'text', value: escaped }); - } - if (addSourceMap) - SourceMaps.track(escaped, context); + if (addSourceMap) + SourceMaps.track(escaped, context); + } context.cursor = nextEnd + 2; } else if (what == 'bodyStart') { diff --git a/lib/utils/input-source-map-tracker.js b/lib/utils/input-source-map-tracker.js new file mode 100644 index 00000000..15ebe085 --- /dev/null +++ b/lib/utils/input-source-map-tracker.js @@ -0,0 +1,65 @@ +var SourceMapConsumer = require('source-map').SourceMapConsumer; + +var fs = require('fs'); +var path = require('path'); + +var SOURCE_MARKER_START = /__ESCAPED_SOURCE_CLEAN_CSS\(([^~][^\)]+)\)__/; +var SOURCE_MARKER_END = /__ESCAPED_SOURCE_END_CLEAN_CSS__/; +var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//; + +function InputSourceMapStore(options) { + this.options = options; + this.maps = {}; +} + +InputSourceMapStore.prototype.track = function (data) { + if (typeof this.options.sourceMap == 'string') { + this.maps[undefined] = new SourceMapConsumer(this.options.sourceMap); + this.options.sourceMap = true; + return this; + } + + var files = []; + for (var cursor = 0, len = data.length; cursor < len; ) { + var fragment = data.substring(cursor, len); + + var markerStartMatch = SOURCE_MARKER_START.exec(fragment) || { index: -1 }; + var markerEndMatch = SOURCE_MARKER_END.exec(fragment) || { index: -1 }; + var mapMatch = MAP_MARKER.exec(fragment) || { index: -1 }; + + var nextAt = len; + if (markerStartMatch.index > -1) + nextAt = markerStartMatch.index; + if (markerEndMatch.index > -1 && markerEndMatch.index < nextAt) + nextAt = markerEndMatch.index; + if (mapMatch.index > -1 && mapMatch.index < nextAt) + nextAt = mapMatch.index; + + if (nextAt == len) + break; + + if (nextAt == markerStartMatch.index) { + files.push(markerStartMatch[1]); + } else if (nextAt == markerEndMatch.index) { + files.pop(); + } else if (nextAt == mapMatch.index) { + var inputMapData = fs.readFileSync(path.join(this.options.root || '', mapMatch[1]), 'utf-8'); + this.maps[files[files.length - 1] || undefined] = new SourceMapConsumer(inputMapData); + this.options.sourceMap = true; + } + + cursor += nextAt + 1; + } + + return this; +}; + +InputSourceMapStore.prototype.isTracking = function () { + return Object.keys(this.maps).length > 0; +}; + +InputSourceMapStore.prototype.originalPositionFor = function (sourceInfo) { + return this.maps[sourceInfo.source].originalPositionFor(sourceInfo); +}; + +module.exports = InputSourceMapStore; diff --git a/test/data/source-maps/import.css b/test/data/source-maps/import.css new file mode 100644 index 00000000..58e84d6c --- /dev/null +++ b/test/data/source-maps/import.css @@ -0,0 +1,2 @@ +@import url(some.css); +@import url(styles.css); diff --git a/test/data/source-maps/some.css b/test/data/source-maps/some.css new file mode 100644 index 00000000..a10316e6 --- /dev/null +++ b/test/data/source-maps/some.css @@ -0,0 +1,4 @@ +div { + color: red; +} +/*# sourceMappingURL=some.css.map */ \ No newline at end of file diff --git a/test/data/source-maps/some.css.map b/test/data/source-maps/some.css.map new file mode 100644 index 00000000..0465e28b --- /dev/null +++ b/test/data/source-maps/some.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css"} \ No newline at end of file diff --git a/test/data/source-maps/styles.css b/test/data/source-maps/styles.css new file mode 100644 index 00000000..3ce538a9 --- /dev/null +++ b/test/data/source-maps/styles.css @@ -0,0 +1,4 @@ +div > a { + color: blue; +} +/*# sourceMappingURL=styles.css.map */ diff --git a/test/data/source-maps/sample.map b/test/data/source-maps/styles.css.map similarity index 62% rename from test/data/source-maps/sample.map rename to test/data/source-maps/styles.css.map index dab8bffc..868cb57a 100644 --- a/test/data/source-maps/sample.map +++ b/test/data/source-maps/styles.css.map @@ -1 +1 @@ -{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GACE;EACE,UAAA","file":"styles.css"} \ No newline at end of file +{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GACE;EACE,WAAA","file":"styles.css"} \ No newline at end of file diff --git a/test/selectors/tokenizer-source-maps-test.js b/test/selectors/tokenizer-source-maps-test.js index 99d56434..e556ef82 100644 --- a/test/selectors/tokenizer-source-maps-test.js +++ b/test/selectors/tokenizer-source-maps-test.js @@ -439,7 +439,7 @@ vows.describe('source-maps/analyzer') .addBatch( sourceMapContext('sources', { 'one': [ - '__ESCAPED_SOURCE_CLEAN_CSS(one.css)__a{}', + '__ESCAPED_SOURCE_CLEAN_CSS(one.css)__a{}__ESCAPED_SOURCE_END_CLEAN_CSS__', [{ kind: 'selector', value: [{ value: 'a', metadata: { line: 1, column: 1, source: 'one.css' } }], @@ -447,7 +447,7 @@ vows.describe('source-maps/analyzer') }] ], 'two': [ - '__ESCAPED_SOURCE_CLEAN_CSS(one.css)__a{}\n__ESCAPED_SOURCE_CLEAN_CSS(two.css)__a{color:red}', + '__ESCAPED_SOURCE_CLEAN_CSS(one.css)__a{}__ESCAPED_SOURCE_END_CLEAN_CSS____ESCAPED_SOURCE_CLEAN_CSS(two.css)__\na{color:red}__ESCAPED_SOURCE_END_CLEAN_CSS__', [ { kind: 'selector', @@ -459,7 +459,7 @@ vows.describe('source-maps/analyzer') { kind: 'selector', value: [ - { value: 'a', metadata: { line: 2, column: 1, source: 'two.css' } } + { value: '\na', metadata: { line: 2, column: 1, source: 'two.css' } } ], body: [{ value: 'color:red', metadata: { line: 2, column: 3, source: 'two.css' } }] } diff --git a/test/source-map-test.js b/test/source-map-test.js index e5e9fb71..a9089caa 100644 --- a/test/source-map-test.js +++ b/test/source-map-test.js @@ -4,7 +4,7 @@ var CleanCSS = require('../index'); var fs = require('fs'); var path = require('path'); -var inputMapPath = path.join('test', 'data', 'source-maps', 'sample.map'); +var inputMapPath = path.join('test', 'data', 'source-maps', 'styles.css.map'); var inputMap = fs.readFileSync(inputMapPath, 'utf-8'); vows.describe('source-map') @@ -257,7 +257,7 @@ vows.describe('source-map') } }, 'input map from source with root': { - 'topic': new CleanCSS({ root: path.dirname(inputMapPath) }).minify('div > a {\n color: red;\n}/*# sourceMappingURL=sample.map */'), + 'topic': new CleanCSS({ root: path.dirname(inputMapPath) }).minify('div > a {\n color: red;\n}/*# sourceMappingURL=styles.css.map */'), 'should have 2 mappings': function (minified) { assert.equal(2, minified.sourceMap._mappings.length); }, @@ -283,6 +283,56 @@ vows.describe('source-map') }; assert.deepEqual(mapping, minified.sourceMap._mappings[1]); } + }, + 'complex input map': { + 'topic': new CleanCSS({ root: path.dirname(inputMapPath) }).minify('@import url(import.css);'), + 'should have 4 mappings': function (minified) { + assert.equal(4, minified.sourceMap._mappings.length); + }, + 'should have first selector mapping': function (minified) { + var mapping = { + generatedLine: 1, + generatedColumn: 1, + originalLine: 1, + originalColumn: 1, + source: 'some.less', + name: 'div' + }; + assert.deepEqual(mapping, minified.sourceMap._mappings[0]); + }, + 'should have _color:red_ mapping': function (minified) { + var mapping = { + generatedLine: 1, + generatedColumn: 5, + originalLine: 2, + originalColumn: 2, + source: 'some.less', + name: 'color:red' + }; + assert.deepEqual(mapping, minified.sourceMap._mappings[1]); + }, + 'should have second selector mapping': function (minified) { + var mapping = { + generatedLine: 2, + generatedColumn: 1, + originalLine: 1, + originalColumn: 1, + source: 'styles.less', + name: 'div>a' + }; + assert.deepEqual(mapping, minified.sourceMap._mappings[2]); + }, + 'should have _color:blue_ mapping': function (minified) { + var mapping = { + generatedLine: 2, + generatedColumn: 7, + originalLine: 3, + originalColumn: 4, + source: 'styles.less', + name: 'color:#00f' + }; + assert.deepEqual(mapping, minified.sourceMap._mappings[3]); + } } }) .export(module); -- 2.34.1