* Rewrote inliner to process data asynchronously.
* Supports 2xx responses, redirects, errors, and timeouts.
* Supports cyclical references.
* Supports protocol-less requests (defaults to HTTP).
* Supports overriding request options - see http://nodejs.org/api/http.html#http_http_request_options_callback
* Supports timeout in ./bin/cleancss via --timeout / -t switches.
* Supports inlining local resources only without a callback.
* Supports rebasing URLs in remote @imports.
* Always triggers a callback asynchronously.
* Adds an optional callback to minify method.
* Deprecates --selectors-merge-mode / selectorsMergeMode in favor to --compatibility / compatibility.
* Skips empty removal if advanced processing is enabled.
+* Fixed issue [#85](https://github.com/GoalSmashers/clean-css/issues/85) - resolving protocol `@import`s.
* Fixed issue [#160](https://github.com/GoalSmashers/clean-css/issues/160) - re-runs optimizer until a clean pass.
* Fixed issue [#161](https://github.com/GoalSmashers/clean-css/issues/161) - improves tokenizer performance.
* Fixed issue [#163](https://github.com/GoalSmashers/clean-css/issues/163) - round pixels to 2nd decimal place.
.option('--skip-advanced', 'Disable advanced optimizations - selector & property merging, reduction, etc.')
.option('--selectors-merge-mode [ie8|*]', 'DEPRECATED: Use --compatibility switch')
.option('-c, --compatibility [ie8]', 'Force compatibility mode')
+ .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)');
commands.on('--help', function() {
}
if (commands.debug)
cleanOptions.debug = true;
+if (commands.timeout)
+ cleanOptions.inliner = { timeout: parseFloat(commands.timeout) * 1000 };
if (commands.args.length > 0) {
var source = commands.args[0];
options.source = source;
fs.readFile(options.source, 'utf8', function(error, data) {
if (error)
throw error;
- output(minify(data));
+ minify(data);
});
} else {
var stdin = process.openStdin();
data += chunk;
});
stdin.on('end', function() {
- output(minify(data));
+ minify(data);
});
}
function minify(data) {
- var minifier = new CleanCSS(cleanOptions);
- var minified = minifier.minify(data);
-
- if (cleanOptions.debug) {
- console.error('Original: %d bytes', minifier.stats.originalSize);
- console.error('Minified: %d bytes', minifier.stats.minifiedSize);
- console.error('Efficiency: %d%', ~~(minifier.stats.efficiency * 10000) / 100.0);
- console.error('Time spent: %dms', minifier.stats.timeSpent);
- }
+ new CleanCSS(cleanOptions).minify(data, function(errors, minified) {
+ if (cleanOptions.debug) {
+ console.error('Original: %d bytes', this.stats.originalSize);
+ console.error('Minified: %d bytes', this.stats.minifiedSize);
+ console.error('Efficiency: %d%', ~~(this.stats.efficiency * 10000) / 100.0);
+ console.error('Time spent: %dms', this.stats.timeSpent);
+ }
- outputFeedback(minifier.errors, true);
- outputFeedback(minifier.warnings);
+ outputFeedback(this.errors, true);
+ outputFeedback(this.warnings);
- if (minifier.errors.length > 0)
- process.exit(1);
+ if (this.errors.length > 0)
+ process.exit(1);
- return minified;
+ output(minified);
+ });
}
function output(minified) {
* Copyright (C) 2011-2014 GoalSmashers.com
*/
+/* jshint latedef: false */
+
var ColorShortener = require('./colors/shortener');
var ColorHSLToHex = require('./colors/hsl-to-hex');
var ColorRGBToHex = require('./colors/rgb-to-hex');
};
CleanCSS.prototype.minify = function(data, callback) {
- var startedAt;
- var stats = this.stats;
var options = this.options;
- var context = this.context;
- var lineBreak = this.lineBreak;
if (Buffer.isBuffer(data))
data = data.toString();
if (options.debug) {
- startedAt = process.hrtime();
- stats.originalSize = data.length;
+ this.startedAt = process.hrtime();
+ this.stats.originalSize = data.length;
+ }
+
+ if (options.processImport) {
+ // inline all imports
+ var self = this;
+ var runner = callback ?
+ process.nextTick :
+ function(callback) { return callback(); };
+
+ return runner(function() {
+ return new ImportInliner(self.context, options.inliner).process(data, {
+ localOnly: !callback,
+ root: options.root || process.cwd(),
+ relativeTo: options.relativeTo,
+ whenDone: function(data) {
+ return minify.call(self, data, callback);
+ }
+ });
+ });
+ } else {
+ return minify.call(this, data, callback);
}
+};
+
+var minify = function(data, callback) {
+ var stats = this.stats;
+ var options = this.options;
+ var context = this.context;
+ var lineBreak = this.lineBreak;
+
+ var commentsProcessor = new CommentsProcessor(
+ 'keepSpecialComments' in options ? options.keepSpecialComments : '*',
+ options.keepBreaks,
+ lineBreak
+ );
+ var expressionsProcessor = new ExpressionsProcessor();
+ var freeTextProcessor = new FreeTextProcessor();
+ var urlsProcessor = new UrlsProcessor();
var replace = function() {
if (typeof arguments[0] == 'function')
};
}
- var commentsProcessor = new CommentsProcessor(
- 'keepSpecialComments' in options ? options.keepSpecialComments : '*',
- options.keepBreaks,
- lineBreak
- );
- var expressionsProcessor = new ExpressionsProcessor();
- var freeTextProcessor = new FreeTextProcessor();
- var urlsProcessor = new UrlsProcessor();
- var importInliner = new ImportInliner(context);
-
- if (options.processImport) {
- // inline all imports
- replace(function inlineImports() {
- data = importInliner.process(data, {
- root: options.root || process.cwd(),
- relativeTo: options.relativeTo
- });
- });
- }
-
replace(function escapeComments() {
data = commentsProcessor.escape(data);
});
data = data.trim();
if (options.debug) {
- var elapsed = process.hrtime(startedAt);
+ var elapsed = process.hrtime(this.startedAt);
stats.timeSpent = ~~(elapsed[0] * 1e3 + elapsed[1] / 1e6);
stats.efficiency = 1 - data.length / stats.originalSize;
stats.minifiedSize = data.length;
var path = require('path');
+var url = require('url');
module.exports = {
process: function(data, options) {
data;
},
- _rebased: function(url, options) {
- var specialUrl = url[0] == '/' ||
- url.substring(url.length - 4) == '.css' ||
- url.indexOf('data:') === 0 ||
- /^https?:\/\//.exec(url) !== null ||
- /__\w+__/.exec(url) !== null;
+ _rebased: function(resource, options) {
+ var specialUrl = resource[0] == '/' ||
+ resource.substring(resource.length - 4) == '.css' ||
+ resource.indexOf('data:') === 0 ||
+ /^https?:\/\//.exec(resource) !== null ||
+ /__\w+__/.exec(resource) !== null;
var rebased;
if (specialUrl)
- return url;
+ return resource;
+
+ if (/https?:\/\//.test(options.toBase))
+ return url.resolve(options.toBase, resource);
if (options.absolute) {
rebased = path
- .resolve(path.join(options.fromBase, url))
+ .resolve(path.join(options.fromBase, resource))
.replace(options.toBase, '');
} else {
- rebased = path.relative(options.toBase, path.join(options.fromBase, url));
+ rebased = path.relative(options.toBase, path.join(options.fromBase, resource));
}
return process.platform == 'win32' ?
var fs = require('fs');
var path = require('path');
+var http = require('http');
+var https = require('https');
+var url = require('url');
var UrlRewriter = require('../images/url-rewriter');
-module.exports = function Inliner(context) {
+var merge = function(source1, source2) {
+ var target = {};
+ for (var key1 in source1)
+ target[key1] = source1[key1];
+ for (var key2 in source2)
+ target[key2] = source2[key2];
+
+ return target;
+};
+
+module.exports = function Inliner(context, options) {
+ var defaultOptions = {
+ timeout: 5000,
+ request: {}
+ };
+ var inlinerOptions = merge(defaultOptions, options || {});
+
var process = function(data, options) {
- var tempData = [];
+ options._shared = options._shared || {
+ done: [],
+ left: []
+ };
+ var shared = options._shared;
+
var nextStart = 0;
var nextEnd = 0;
var cursor = 0;
nextEnd = data.indexOf(';', nextStart);
if (nextEnd == -1) {
- tempData.push('');
cursor = data.length;
+ data = '';
break;
}
- tempData.push(data.substring(cursor, nextStart));
- tempData.push(inlinedFile(data, nextStart, nextEnd, options));
- cursor = nextEnd + 1;
+ shared.done.push(data.substring(cursor, nextStart));
+ shared.left.unshift([data.substring(nextEnd + 1), options]);
+
+ return inline(data, nextStart, nextEnd, options);
}
- return tempData.length > 0 ?
- tempData.join('') + data.substring(cursor, data.length) :
- data;
+ // no @import matched in current data
+ shared.done.push(data);
+ return processNext(options);
+ };
+
+ var processNext = function(options) {
+ if (options._shared.left.length > 0)
+ return process.apply(null, options._shared.left.shift());
+ else
+ return options.whenDone(options._shared.done.join(''));
};
var commentScanner = function(data) {
return scanner;
};
- var inlinedFile = function(data, nextStart, nextEnd, options) {
+ var inline = function(data, nextStart, nextEnd, options) {
var strippedImport = data
.substring(data.indexOf(' ', nextStart) + 1, nextEnd)
.replace(/^url\(/, '')
.substring(importedFile.length + 1)
.trim();
- if (/^(http|https):\/\//.test(importedFile) || /^\/\//.test(importedFile))
- return '@import url(' + importedFile + ')' + (mediaQuery.length > 0 ? ' ' + mediaQuery : '') + ';';
+ var isRemote = options.isRemote ||
+ /^(http|https):\/\//.test(importedFile) ||
+ /^\/\//.test(importedFile);
+
+ if (options.localOnly && isRemote) {
+ context.warnings.push('Ignoring remote @import declaration of "' + importedFile + '" as no callback given.');
+ restoreImport(importedFile, mediaQuery, options);
+
+ return processNext(options);
+ }
+
+ var method = isRemote ? inlineRemoteResource : inlineLocalResource;
+ return method(importedFile, mediaQuery, options);
+ };
+
+ var inlineRemoteResource = function(importedFile, mediaQuery, options) {
+ var importedUrl = /^https?:\/\//.test(importedFile) ?
+ importedFile :
+ url.resolve(options.relativeTo, importedFile);
+
+ if (importedUrl.indexOf('//') === 0)
+ importedUrl = 'http:' + importedUrl;
+
+ if (options.visited.indexOf(importedUrl) > -1)
+ return processNext(options);
+
+ options.visited.push(importedUrl);
+
+ var get = importedUrl.indexOf('http://') === 0 ?
+ http.get :
+ https.get;
+
+ var timedOut = false;
+ var handleError = function(message) {
+ context.errors.push('Broken @import declaration of "' + importedUrl + '" - ' + message);
+ restoreImport(importedUrl, mediaQuery, options);
+
+ processNext(options);
+ };
+ var requestOptions = merge(url.parse(importedUrl), inlinerOptions.request);
+
+ get(requestOptions, function(res) {
+ if (res.statusCode < 200 || res.statusCode > 399) {
+ return handleError('error ' + res.statusCode);
+ } else if (res.statusCode > 299) {
+ var movedUrl = url.resolve(importedUrl, res.headers.location);
+ return inlineRemoteResource(movedUrl, mediaQuery, options);
+ }
+
+ var chunks = [];
+ var parsedUrl = url.parse(importedUrl);
+ res.on('data', function(chunk) {
+ chunks.push(chunk.toString());
+ });
+ res.on('end', function() {
+ var importedData = chunks.join('');
+ importedData = UrlRewriter.process(importedData, { toBase: importedUrl });
+
+ if (mediaQuery.length > 0)
+ importedData = '@media ' + mediaQuery + '{' + importedData + '}';
+
+ process(importedData, {
+ isRemote: true,
+ relativeTo: parsedUrl.protocol + '//' + parsedUrl.host,
+ _shared: options._shared,
+ whenDone: options.whenDone,
+ visited: options.visited
+ });
+ });
+ })
+ .on('error', function(res) {
+ handleError(res.message);
+ })
+ .on('timeout', function() {
+ // FIX: node 0.8 fires this event twice
+ if (timedOut)
+ return;
+
+ handleError('timeout');
+ timedOut = true;
+ })
+ .setTimeout(inlinerOptions.timeout);
+ };
+
+ var inlineLocalResource = function(importedFile, mediaQuery, options) {
var relativeTo = importedFile[0] == '/' ?
options.root :
options.relativeTo;
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
context.errors.push('Broken @import declaration of "' + importedFile + '"');
- return '';
+ return processNext(options);
}
- if (options.visited.indexOf(fullPath) != -1)
- return '';
+ if (options.visited.indexOf(fullPath) > -1)
+ return processNext(options);
+
options.visited.push(fullPath);
toBase: options._baseRelativeTo
});
- var inlinedData = process(importedData, {
+ if (mediaQuery.length > 0)
+ importedData = '@media ' + mediaQuery + '{' + importedData + '}';
+
+ return process(importedData, {
root: options.root,
relativeTo: importRelativeTo,
_baseRelativeTo: options.baseRelativeTo,
- visited: options.visited
+ _shared: options._shared,
+ visited: options.visited,
+ whenDone: options.whenDone,
+ localOnly: options.localOnly
});
- return mediaQuery.length > 0 ?
- '@media ' + mediaQuery + '{' + inlinedData + '}' :
- inlinedData;
+ };
+
+ var restoreImport = function(importedUrl, mediaQuery, options) {
+ var restoredImport = '@import url(' + importedUrl + ')' + (mediaQuery.length > 0 ? ' ' + mediaQuery : '') + ';';
+ options._shared.done.push(restoredImport);
};
// Inlines all imports taking care of repetitions, unknown files, and circular dependencies
},
"devDependencies": {
"jshint": "2.4.x",
+ "nock": "0.25.x",
"vows": "0.7.x"
},
"jshintConfig": {
return {
plain: fs.readFileSync(plainPath, 'utf-8'),
- minimized: fs.readFileSync(minPath, 'utf-8'),
+ preminified: fs.readFileSync(minPath, 'utf-8'),
root: path.dirname(plainPath)
};
}
};
context[testName]['minimizing ' + testName + '.css'] = function(data) {
- var processed = new CleanCSS({
+ new CleanCSS({
keepBreaks: true,
root: data.root
- }).minify(data.plain);
+ }).minify(data.plain, function(errors, minified) {
+ var minifiedTokens = minified.split(lineBreak);
+ var preminifiedTokens = data.preminified.split(lineBreak);
- var processedTokens = processed.split(lineBreak);
- var minimizedTokens = data.minimized.split(lineBreak);
-
- processedTokens.forEach(function(line, i) {
- assert.equal(line, minimizedTokens[i]);
+ minifiedTokens.forEach(function(line, i) {
+ assert.equal(line, preminifiedTokens[i]);
+ });
});
};
});
var cssData = require('fs').readFileSync(path.join(benchDir, 'complex.css'), 'utf8');
var start = process.hrtime();
-new CleanCSS({ benchmark: true, root: benchDir }).minify(cssData);
-
-var itTook = process.hrtime(start);
-console.log('complete minification: %d ms', 1000 * itTook[0] + itTook[1] / 1000000);
+new CleanCSS({ benchmark: true, root: benchDir }).minify(cssData, function() {
+ var itTook = process.hrtime(start);
+ console.log('complete minification: %d ms', 1000 * itTook[0] + itTook[1] / 1000000);
+});
var assert = require('assert');
var exec = require('child_process').exec;
var fs = require('fs');
+var http = require('http');
var isWindows = process.platform == 'win32';
var lineBreak = isWindows ? /\r\n/g : /\n/g;
return context;
};
+var unixOnlyContext = function(context) {
+ return isWindows ? {} : context;
+};
+
var readFile = function(filename) {
return fs.readFileSync(filename, 'utf-8').replace(lineBreak, '');
};
assert.include(stdout, 'url(../components/jquery-ui/images/next.gif)');
}
})
- }
+ },
+ 'timeout': unixOnlyContext({
+ topic: function() {
+ var self = this;
+ var source = '@import url(http://localhost:24682/timeout.css);';
+
+ this.server = http.createServer(function() {
+ setTimeout(function() {}, 1000);
+ });
+ this.server.listen('24682', function() {
+ exec('echo "' + source + '" | ./bin/cleancss --timeout 0.01', self.callback);
+ });
+ },
+ 'should raise warning': function(error, stdout, stderr) {
+ assert.include(stderr, 'Broken @import declaration of "http://localhost:24682/timeout.css" - timeout');
+ },
+ 'should output empty response': function(error, stdout) {
+ assert.equal(stdout, '');
+ },
+ teardown: function() {
+ this.server.close();
+ }
+ })
});
},
'if both root and output used reasons given': function(minifier) {
assert.doesNotThrow(function() {
- minifier.minify('@import url(/some/fake/file);');
+ minifier.minify('@import url(/some/fake/file);', function(errors) {
+ assert.equal(errors.length, 1);
+ assert.equal(errors[0], 'Broken @import declaration of "/some/fake/file"');
+ });
});
- assert.equal(minifier.errors.length, 1);
- assert.equal(minifier.errors[0], 'Broken @import declaration of "/some/fake/file"');
}
},
'buffer passed in': {
--- /dev/null
+/* jshint unused: false */
+
+var vows = require('vows');
+var assert = require('assert');
+var http = require('http');
+var nock = require('nock');
+var CleanCSS = require('../index');
+
+var port = 24682;
+
+if (process.platform == 'win32')
+ return;
+
+vows.describe('protocol imports').addBatch({
+ 'of a missing file': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/missing.css')
+ .reply(404);
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/missing.css);a{color:red}', this.callback);
+ },
+ 'should raise error': function(errors, minified) {
+ assert.equal(errors.length, 1);
+ },
+ 'should ignore @import': function(errors, minified) {
+ assert.equal(minified, '@import url(http://goalsmashers.com/missing.css);a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of an existing file': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/present.css')
+ .reply(200, 'p{font-size:13px}');
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/present.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'p{font-size:13px}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of an existing file via HTTPS': {
+ topic: function() {
+ this.reqMocks = nock('https://goalsmashers.com')
+ .get('/present.css')
+ .reply(200, 'p{font-size:13px}');
+
+ new CleanCSS().minify('@import url(https://goalsmashers.com/present.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'p{font-size:13px}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of an existing file with media': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/present.css')
+ .reply(200, 'p{font-size:13px}');
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/present.css) screen;a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, '@media screen{p{font-size:13px}}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of an existing file with dependencies': {
+ topic: function() {
+ this.reqMocks1 = nock('http://goalsmashers.com')
+ .get('/present.css')
+ .reply(200, '@import url(/vendor/reset.css);@import url(https://assets.goalsmashers.com/base.css);p{font-size:13px}')
+ .get('/vendor/reset.css')
+ .reply(200, 'body{margin:0}');
+ this.reqMocks2 = nock('https://assets.goalsmashers.com')
+ .get('/base.css')
+ .reply(200, 'div{padding:0}');
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/present.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'body{margin:0}div{padding:0}p{font-size:13px}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks1.isDone(), true);
+ assert.equal(this.reqMocks2.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of an existing file with relative dependencies': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/nested/present.css')
+ .reply(200, '@import url(../vendor/reset.css);p{font-size:13px}')
+ .get('/vendor/reset.css')
+ .reply(200, 'body{margin:0}');
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/nested/present.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'body{margin:0}p{font-size:13px}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of an existing file missing relative dependency': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/nested/present.css')
+ .reply(200, '@import url(../missing.css);p{font-size:13px}')
+ .get('/missing.css')
+ .reply(404);
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/nested/present.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.equal(errors.length, 1);
+ assert.equal(errors[0], 'Broken @import declaration of "http://goalsmashers.com/missing.css" - error 404');
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, '@import url(http://goalsmashers.com/missing.css);p{font-size:13px}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of an existing file with URLs to rebase': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/urls.css')
+ .reply(200, 'a{background:url(test.png)}');
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/urls.css);', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'a{background:url(http://goalsmashers.com/test.png)}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of an existing file with relative URLs to rebase': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/base.css')
+ .reply(200, '@import url(deeply/nested/urls.css);')
+ .get('/deeply/nested/urls.css')
+ .reply(200, 'a{background:url(../images/test.png)}');
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/base.css);', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'a{background:url(http://goalsmashers.com/deeply/images/test.png)}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of a non-resolvable domain': {
+ topic: function() {
+ new CleanCSS().minify('@import url(http://notdefined.goalsmashers.com/custom.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.equal(errors.length, 1);
+ assert.equal(errors[0], 'Broken @import declaration of "http://notdefined.goalsmashers.com/custom.css" - getaddrinfo ENOTFOUND');
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, '@import url(http://notdefined.goalsmashers.com/custom.css);a{color:red}');
+ }
+ },
+ 'of a 30x response with absolute URL': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/moved.css')
+ .reply(301, '', { 'Location': 'http://goalsmashers.com/present.css' })
+ .get('/present.css')
+ .reply(200, 'body{margin:0}');
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/moved.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'body{margin:0}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of a 30x response with relative URL': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/moved.css')
+ .reply(301, '', { 'Location': '/present.css' })
+ .get('/present.css')
+ .reply(200, 'body{margin:0}');
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/moved.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'body{margin:0}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of a timed out response': {
+ topic: function() {
+ var self = this;
+ var timeout = 100;
+ this.server = http.createServer(function(req, res) {
+ setTimeout(function() {}, timeout * 2);
+ });
+ this.server.listen(port, function() {
+ new CleanCSS({
+ inliner: {
+ timeout: timeout
+ }
+ }).minify('@import url(http://localhost:' + port + '/timeout.css);a{color:red}', self.callback);
+ });
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.equal(errors.length, 1);
+ assert.equal(errors[0], 'Broken @import declaration of "http://localhost:' + port + '/timeout.css" - timeout');
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, '@import url(http://localhost:' + port + '/timeout.css);a{color:red}');
+ },
+ teardown: function() {
+ this.server.close();
+ }
+ },
+ 'of a cyclical reference response': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/one.css')
+ .reply(200, '@import url(/two.css);div{padding:0}')
+ .get('/two.css')
+ .reply(200, '@import url(http://goalsmashers.com/two.css);body{margin:0}');
+
+ new CleanCSS().minify('@import url(http://goalsmashers.com/one.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'body{margin:0}div{padding:0}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of a resource without protocol': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/no-protocol.css')
+ .reply(200, 'div{padding:0}');
+
+ new CleanCSS().minify('@import url(//goalsmashers.com/no-protocol.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'div{padding:0}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of a resource available via POST only': {
+ topic: function() {
+ this.reqMocks = nock('http://goalsmashers.com')
+ .post('/computed.css')
+ .reply(200, 'div{padding:0}');
+
+ new CleanCSS({
+ inliner: {
+ request: {
+ method: 'POST'
+ }
+ }
+ }).minify('@import url(http://goalsmashers.com/computed.css);a{color:red}', this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'div{padding:0}a{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of a remote resource mixed with local ones': {
+ topic: function() {
+ var source = '@import url(http://goalsmashers.com/remote.css);@import url(test/data/partials/one.css);';
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/remote.css')
+ .reply(200, 'div{padding:0}');
+
+ new CleanCSS().minify(source, this.callback);
+ },
+ 'should not raise errors': function(errors, minified) {
+ assert.isNull(errors);
+ },
+ 'should process @import': function(errors, minified) {
+ assert.equal(minified, 'div{padding:0}.one{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), true);
+ nock.restore();
+ }
+ },
+ 'of a remote resource mixed with local ones but no callback': {
+ topic: function() {
+ var source = '@import url(http://goalsmashers.com/remote.css);@import url(test/data/partials/one.css);';
+ this.reqMocks = nock('http://goalsmashers.com')
+ .get('/remote.css')
+ .reply(200, 'div{padding:0}');
+
+ var minifier = new CleanCSS();
+ var minified = minifier.minify(source);
+ this.callback(null, minifier, minified);
+ },
+ 'should not raise errors': function(error, minifier) {
+ assert.isEmpty(minifier.errors);
+ },
+ 'should raise warnings': function(error, minifier) {
+ assert.equal(minifier.warnings.length, 1);
+ assert.match(minifier.warnings[0], /no callback given/);
+ },
+ 'should process @import': function(error, minifier, minified) {
+ assert.equal(minified, '@import url(http://goalsmashers.com/remote.css);.one{color:red}');
+ },
+ teardown: function() {
+ assert.equal(this.reqMocks.isDone(), false);
+ nock.restore();
+ }
+ }
+}).export(module);
"@import url(fake.css)",
''
],
- 'of a http file': "@import url(http://pro.goalsmashers.com/test.css);",
- 'of a https file': [
- "@import url('https://pro.goalsmashers.com/test.css');",
- "@import url(https://pro.goalsmashers.com/test.css);"
- ],
- 'of a remote file with media': "@import url(https://pro.goalsmashers.com/test.css) screen,tv;",
- 'of a url starting with //': [
- "@import url(//fonts.googleapis.com/css?family=Lato:400,700,400italic|Merriweather:400,700);",
- "@import url(//fonts.googleapis.com/css?family=Lato:400,700,400italic|Merriweather:400,700);"
- ],
- 'of a remote file via // url with media': "@import url(//pro.goalsmashers.com/test.css) screen,tv;",
'of a directory': [
"@import url(test/data/partials);",
''