* http, https, and same protocol (//) are supported.
* Options from `inliner` hash are used for timeouts / extra request options.
* Errors & timeouts are handled gracefully.
* Apparently nock.restore() should be nock.cleanAll().
};
function runMinifier(callback, self) {
- return function (data) {
- if (self.options.sourceMap)
- self.inputSourceMapTracker = new InputSourceMapTracker(self.options).track(data);
-
+ function whenSourceMapReady (data) {
data = self.options.debug ?
minifyWithDebug(self, data) :
minify.call(self, data);
return callback ?
callback.call(self, self.context.errors.length > 0 ? self.context.errors : null, data) :
data;
+ }
+
+ return function (data) {
+ if (self.options.sourceMap) {
+ self.inputSourceMapTracker = new InputSourceMapTracker(self.options, self.context);
+ return self.inputSourceMapTracker.track(data, function () { return whenSourceMapReady(data); });
+ } else {
+ return whenSourceMapReady(data);
+ }
};
}
var UrlRewriter = require('../images/url-rewriter');
var Splitter = require('../utils/splitter.js');
+var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//;
+var REMOTE_RESOURCE = /^(https?:)?\/\//;
+
var merge = function(source1, source2) {
var target = {};
for (var key1 in source1)
return target;
};
+function rebaseMap(data, source) {
+ return data.replace(MAP_MARKER, function (match, sourceMapUrl) {
+ return REMOTE_RESOURCE.test(sourceMapUrl) ?
+ match :
+ match.replace(sourceMapUrl, url.resolve(source, sourceMapUrl));
+ });
+}
+
function wrap(data, source) {
return '__ESCAPED_SOURCE_CLEAN_CSS(' + source + ')__' +
data +
.replace(/^\)/, '')
.trim();
- var isRemote = options.isRemote ||
- /^(http|https):\/\//.test(importedFile) ||
- /^\/\//.test(importedFile);
+ var isRemote = options.isRemote || REMOTE_RESOURCE.test(importedFile);
if (options.localOnly && isRemote) {
context.warnings.push('Ignoring remote @import declaration of "' + importedFile + '" as no callback given.');
};
var inlineRemoteResource = function(importedFile, mediaQuery, options) {
- var importedUrl = /^https?:\/\//.test(importedFile) ?
+ var importedUrl = REMOTE_RESOURCE.test(importedFile) ?
importedFile :
url.resolve(options.relativeTo, importedFile);
res.on('end', function() {
var importedData = chunks.join('');
importedData = UrlRewriter.process(importedData, { toBase: importedUrl });
- importedData = wrap(importedData, url);
+ importedData = rebaseMap(wrap(importedData, importedUrl), importedUrl);
if (mediaQuery.length > 0)
importedData = '@media ' + mediaQuery + '{' + importedData + '}';
var fs = require('fs');
var path = require('path');
+var http = require('http');
+var https = require('https');
+var url = require('url');
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) {
+var DEFAULT_TIMEOUT = 5000;
+
+
+function InputSourceMapStore(options, outerContext) {
this.options = options;
+ this.errors = outerContext.errors;
+ this.timeout = (options.inliner && options.inliner.timeout) || DEFAULT_TIMEOUT;
+ this.requestOptions = (options.inliner && options.inliner.request) || {};
+
this.maps = {};
}
-InputSourceMapStore.prototype.track = function (data) {
- if (typeof this.options.sourceMap == 'string') {
- this.maps[undefined] = new SourceMapConsumer(this.options.sourceMap);
- return this;
+function merge(source1, source2) {
+ var target = {};
+ for (var key1 in source1)
+ target[key1] = source1[key1];
+ for (var key2 in source2)
+ target[key2] = source2[key2];
+
+ return target;
+}
+
+function fromString(self, data, whenDone) {
+ self.maps[undefined] = new SourceMapConsumer(self.options.sourceMap);
+ return whenDone();
+}
+
+function fromSource(self, data, whenDone, context) {
+ var nextAt = 0;
+
+ function proceedToNext() {
+ context.cursor += nextAt + 1;
+ fromSource(self, data, whenDone, context);
}
- var files = [];
- for (var cursor = 0, len = data.length; cursor < len; ) {
- var fragment = data.substring(cursor, len);
+ while (context.cursor < data.length) {
+ var fragment = data.substring(context.cursor);
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;
+ nextAt = data.length;
if (markerStartMatch.index > -1)
nextAt = markerStartMatch.index;
if (markerEndMatch.index > -1 && markerEndMatch.index < nextAt)
if (mapMatch.index > -1 && mapMatch.index < nextAt)
nextAt = mapMatch.index;
- if (nextAt == len)
+ if (nextAt == data.length)
break;
if (nextAt == markerStartMatch.index) {
- files.push(markerStartMatch[1]);
+ context.files.push(markerStartMatch[1]);
} else if (nextAt == markerEndMatch.index) {
- files.pop();
+ context.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);
+ var isRemote = /^https?:\/\//.test(mapMatch[1]) || /^\/\//.test(mapMatch[1]);
+ if (isRemote) {
+ return fetchMapFile(self, mapMatch[1], context, proceedToNext);
+ } else {
+ var inputMapData = fs.readFileSync(path.join(self.options.root || '', mapMatch[1]), 'utf-8');
+ self.maps[context.files[context.files.length - 1] || undefined] = new SourceMapConsumer(inputMapData);
+ }
}
- cursor += nextAt + 1;
+ context.cursor += nextAt + 1;
}
- return this;
+ return whenDone();
+}
+
+function fetchMapFile(self, mapSource, context, done) {
+ function handleError(status) {
+ context.errors.push('Broken source map at "' + mapSource + '" - ' + status);
+ return done();
+ }
+
+ var method = mapSource.indexOf('https') === 0 ? https : http;
+ var requestOptions = merge(url.parse(mapSource), self.requestOptions);
+
+ method
+ .get(requestOptions, function (res) {
+ if (res.statusCode < 200 || res.statusCode > 299)
+ return handleError(res.statusCode);
+
+ var chunks = [];
+ res.on('data', function (chunk) {
+ chunks.push(chunk.toString());
+ });
+ res.on('end', function () {
+ self.maps[context.files[context.files.length - 1] || undefined] = new SourceMapConsumer(chunks.join(''));
+ done();
+ });
+ })
+ .on('error', function(res) {
+ handleError(res.message);
+ })
+ .on('timeout', function() {
+ handleError('timeout');
+ })
+ .setTimeout(self.timeout);
+}
+
+InputSourceMapStore.prototype.track = function (data, whenDone) {
+ return typeof this.options.sourceMap == 'string' ?
+ fromString(this, data, whenDone) :
+ fromSource(this, data, whenDone, { files: [], cursor: 0, errors: this.errors });
};
InputSourceMapStore.prototype.isTracking = function () {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of an existing file': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of an existing file with spaces in path': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of an existing file via HTTPS': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of an existing file with media': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of an existing file with dependencies': {
teardown: function() {
assert.equal(this.reqMocks1.isDone(), true);
assert.equal(this.reqMocks2.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of an existing file with relative dependencies': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of an existing file missing relative dependency': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of an existing file with URLs to rebase': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of an existing file with relative URLs to rebase': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of a non-resolvable domain': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of a 30x response with relative URL': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of a timed out response': {
}).minify('@import url(http://localhost:' + port + '/timeout.css);a{color:red}', self.callback);
});
},
- 'should not raise errors': function(errors, minified) {
+ 'should raise errors': function(errors, minified) {
assert.equal(errors.length, 1);
assert.equal(errors[0], 'Broken @import declaration of "http://localhost:' + port + '/timeout.css" - timeout');
},
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of a resource without protocol': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of a resource available via POST only': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of a remote resource mixed with local ones': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), true);
- nock.restore();
+ nock.cleanAll();
}
},
'of a remote resource mixed with local ones but no callback': {
},
teardown: function() {
assert.equal(this.reqMocks.isDone(), false);
- nock.restore();
+ nock.cleanAll();
}
}
}).export(module);
+/* jshint unused: false */
+
var vows = require('vows');
var assert = require('assert');
var CleanCSS = require('../index');
var inputMapPath = path.join('test', 'data', 'source-maps', 'styles.css.map');
var inputMap = fs.readFileSync(inputMapPath, 'utf-8');
+var nock = require('nock');
+var http = require('http');
+
+var port = 24682;
+
vows.describe('source-map')
.addBatch({
'module #1': {
}
}
})
+ .addBatch({
+ 'invalid response for external source map': {
+ topic: function () {
+ this.reqMocks = nock('http://127.0.0.1')
+ .get('/remote.css')
+ .reply(200, '/*# sourceMappingURL=http://127.0.0.1/remote.css.map */')
+ .get('/remote.css.map')
+ .reply(404);
+
+ new CleanCSS({ sourceMap: true }).minify('@import url(http://127.0.0.1/remote.css);', this.callback);
+ },
+ 'has mapping': function (errors, minified) {
+ assert.isDefined(minified.sourceMap);
+ },
+ 'raises an error': function(errors, _) {
+ assert.equal(errors.length, 1);
+ assert.equal(errors[0], 'Broken source map at "http://127.0.0.1/remote.css.map" - 404');
+ },
+ teardown: function () {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.cleanAll();
+ }
+ },
+ 'timed out response for external source map': {
+ topic: function() {
+ var self = this;
+ var timeout = 100;
+
+ this.server = http.createServer(function(req, res) {
+ switch (req.url) {
+ case '/remote.css':
+ res.writeHead(200);
+ res.write('/*# sourceMappingURL=http://127.0.0.1:' + port + '/remote.css.map */');
+ res.end();
+ break;
+ case '/remote.css.map':
+ setTimeout(function() {}, timeout * 2);
+ }
+ });
+ this.server.listen(port, '127.0.0.1', function() {
+ new CleanCSS({ sourceMap: true, inliner: { timeout: timeout } })
+ .minify('@import url(http://127.0.0.1:' + port + '/remote.css);', self.callback);
+ });
+ },
+ 'has mapping': function (errors, minified) {
+ assert.isDefined(minified.sourceMap);
+ },
+ 'raises an error': function(errors, _) {
+ assert.equal(errors.length, 1);
+ assert.equal(errors[0], 'Broken source map at "http://127.0.0.1:' + port + '/remote.css.map" - timeout');
+ },
+ teardown: function () {
+ this.server.close();
+ }
+ },
+ 'absolute source map from external host via http': {
+ topic: function () {
+ this.reqMocks = nock('http://127.0.0.1')
+ .get('/remote.css')
+ .reply(200, '/*# sourceMappingURL=http://127.0.0.1/remote.css.map */')
+ .get('/remote.css.map')
+ .reply(200, inputMap);
+
+ new CleanCSS({ sourceMap: true }).minify('@import url(http://127.0.0.1/remote.css);', this.callback);
+ },
+ 'has mapping': function (errors, minified) {
+ assert.isDefined(minified.sourceMap);
+ },
+ teardown: function () {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.cleanAll();
+ }
+ },
+ 'absolute source map from external host via https': {
+ topic: function () {
+ this.reqMocks = nock('https://127.0.0.1')
+ .get('/remote.css')
+ .reply(200, '/*# sourceMappingURL=https://127.0.0.1/remote.css.map */')
+ .get('/remote.css.map')
+ .reply(200, inputMap);
+
+ new CleanCSS({ sourceMap: true }).minify('@import url(https://127.0.0.1/remote.css);', this.callback);
+ },
+ 'has mapping': function (errors, minified) {
+ assert.isDefined(minified.sourceMap);
+ },
+ teardown: function () {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.cleanAll();
+ }
+ },
+ 'relative source map from external host': {
+ topic: function () {
+ this.reqMocks = nock('http://127.0.0.1')
+ .get('/remote.css')
+ .reply(200, '/*# sourceMappingURL=remote.css.map */')
+ .get('/remote.css.map')
+ .reply(200, inputMap);
+
+ new CleanCSS({ sourceMap: true }).minify('@import url(http://127.0.0.1/remote.css);', this.callback);
+ },
+ 'has mapping': function (errors, minified) {
+ assert.isDefined(minified.sourceMap);
+ },
+ teardown: function () {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.cleanAll();
+ }
+ },
+ 'available via POST only': {
+ topic: function () {
+ this.reqMocks = nock('http://127.0.0.1')
+ .post('/remote.css')
+ .reply(200, '/*# sourceMappingURL=remote.css.map */')
+ .post('/remote.css.map')
+ .reply(200, inputMap);
+
+ new CleanCSS({ sourceMap: true, inliner: { request: { method: 'POST' } } })
+ .minify('@import url(http://127.0.0.1/remote.css);', this.callback);
+ },
+ 'has mapping': function (errors, minified) {
+ assert.isDefined(minified.sourceMap);
+ },
+ teardown: function () {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.cleanAll();
+ }
+ }
+ })
.export(module);