[3.4.0 / 2015-xx-xx](https://github.com/jakubpawlowicz/clean-css/compare/v3.3.7...master)
==================
+* Adds an option for a fine-grained `@import` control.
* Adds unit compatibility switches to disable length optimizations.
* Adds inferring proxy settings from HTTP_PROXY environment variable.
* Adds support for Polymer / Web Components special selectors.
* Fixed issue [#612](https://github.com/jakubpawlowicz/clean-css/issues/612) - adds HTTP proxy support.
* Fixed issue [#618](https://github.com/jakubpawlowicz/clean-css/issues/618) - adds safer function validation.
* Fixed issue [#625](https://github.com/jakubpawlowicz/clean-css/issues/625) - adds length unit optimizations.
+* Fixed issue [#632](https://github.com/jakubpawlowicz/clean-css/issues/632) - adds disabling remote `import`s.
* Fixed issue [#635](https://github.com/jakubpawlowicz/clean-css/issues/635) - adds safer `0%` optimizations.
* Fixed issue [#644](https://github.com/jakubpawlowicz/clean-css/issues/644) - adds time unit optimizations.
* Fixed issue [#645](https://github.com/jakubpawlowicz/clean-css/issues/645) - adds bottom to top `media` merging.
--semantic-merging Enables unsafe mode by assuming BEM-like semantic stylesheets (warning, this may break your styling!)
--skip-advanced Disable advanced optimizations - ruleset reordering & merging
--skip-aggressive-merging Disable properties merging based on their order
+--skip-import-from [rules] Disable @import processing for specified rules
--skip-media-merging Disable @media merging
--skip-rebase Disable URLs rebasing
--skip-restructuring Disable restructuring optimizations
* `keepSpecialComments` - `*` for keeping all (default), `1` for keeping first one only, `0` for removing all
* `mediaMerging` - whether to merge `@media` at-rules (default is true)
* `processImport` - whether to process `@import` rules
+* `processImportFrom` - a list of `@import` rules, can be `['all']` (default), `['local']`, `['remote']`, or a blacklisted path e.g. `['!fonts.googleapis.com']`
* `rebase` - set to false to skip URL rebasing
* `relativeTo` - path to **resolve** relative `@import` rules and URLs
* `restructuring` - set to false to disable restructuring in advanced optimizations
.option('--semantic-merging', 'Enables unsafe mode by assuming BEM-like semantic stylesheets (warning, this may break your styling!)')
.option('--skip-advanced', 'Disable advanced optimizations - ruleset reordering & merging')
.option('--skip-aggressive-merging', 'Disable properties merging based on their order')
+ .option('--skip-import-from [rules]', 'Disable @import processing for specified rules', function (val) { return val.split(','); }, [])
.option('--skip-media-merging', 'Disable @media merging')
.option('--skip-rebase', 'Disable URLs rebasing')
.option('--skip-restructuring', 'Disable restructuring optimizations')
keepSpecialComments: commands.s0 ? 0 : (commands.s1 ? 1 : '*'),
mediaMerging: commands.skipMediaMerging ? false : true,
processImport: commands.skipImport ? false : true,
+ processImportFrom: processImportFrom(commands.skipImportFrom),
rebase: commands.skipRebase ? false : true,
restructuring: commands.skipRestructuring ? false : true,
root: commands.root,
});
}
+function processImportFrom(rules) {
+ if (rules.length === 0) {
+ return ['all'];
+ } else if (rules.length == 1 && rules[0] == 'all') {
+ return [];
+ } else {
+ return rules.map(function (rule) {
+ if (rule == 'local')
+ return 'remote';
+ else if (rule == 'remote')
+ return 'local';
+ else
+ return '!' + rule;
+ });
+ }
+}
+
function minify(data) {
new CleanCSS(options).minify(data, function (errors, minified) {
if (options.debug) {
keepSpecialComments: 'keepSpecialComments' in options ? options.keepSpecialComments : '*',
mediaMerging: undefined === options.mediaMerging ? true : !!options.mediaMerging,
processImport: undefined === options.processImport ? true : !!options.processImport,
+ processImportFrom: importOptionsFrom(options.processImportFrom),
rebase: undefined === options.rebase ? true : !!options.rebase,
relativeTo: options.relativeTo,
restructuring: undefined === options.restructuring ? true : !!options.restructuring,
);
};
+function importOptionsFrom(rules) {
+ return undefined === rules ? ['all'] : rules;
+}
+
function missingDirectory(filepath) {
return !fs.existsSync(filepath) && !/\.css$/.test(filepath);
}
return runner(function () {
return new ImportInliner(context).process(data, {
localOnly: context.localOnly,
+ imports: context.options.processImportFrom,
whenDone: runMinifier(callback, context)
});
});
var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//;
var REMOTE_RESOURCE = /^(https?:)?\/\//;
+var NO_PROTOCOL_RESOURCE = /^\/\//;
function ImportInliner (context) {
this.outerContext = context;
var isRemote = context.isRemote || REMOTE_RESOURCE.test(importedFile);
- if (context.localOnly && isRemote) {
+ if (isRemote && (context.localOnly || !allowedResource(importedFile, true, context.imports))) {
if (context.afterContent || hasContent(context.done.join('')))
context.warnings.push('Ignoring remote @import of "' + importedFile + '" as no callback given.');
else
return processNext(context);
}
+ if (!isRemote && !allowedResource(importedFile, false, context.imports)) {
+ if (context.afterImport)
+ context.warnings.push('Ignoring local @import of "' + importedFile + '" as after other inlined content.');
+ else
+ restoreImport(importedFile, mediaQuery, context);
+ return processNext(context);
+ }
+
if (!isRemote && context.afterContent) {
context.warnings.push('Ignoring local @import of "' + importedFile + '" as after other CSS content.');
return processNext(context);
return method(importedFile, mediaQuery, context);
}
+function allowedResource(importedFile, isRemote, rules) {
+ if (rules.length === 0)
+ return false;
+
+ if (isRemote && NO_PROTOCOL_RESOURCE.test(importedFile))
+ importedFile = 'http:' + importedFile;
+
+ var match = isRemote ?
+ url.parse(importedFile).host :
+ importedFile;
+ var allowed = true;
+
+ for (var i = 0; i < rules.length; i++) {
+ var rule = rules[i];
+
+ if (rule == 'all')
+ allowed = true;
+ else if (isRemote && rule == 'local')
+ allowed = false;
+ else if (isRemote && rule == 'remote')
+ allowed = true;
+ else if (!isRemote && rule == 'remote')
+ allowed = false;
+ else if (!isRemote && rule == 'local')
+ allowed = true;
+ else if (rule[0] == '!' && rule.substring(1) === match)
+ allowed = false;
+ }
+
+ return allowed;
+}
+
function inlineRemoteResource(importedFile, mediaQuery, context) {
var importedUrl = REMOTE_RESOURCE.test(importedFile) ?
importedFile :
url.resolve(context.relativeTo, importedFile);
var originalUrl = importedUrl;
- if (importedUrl.indexOf('//') === 0)
+ if (NO_PROTOCOL_RESOURCE.test(importedUrl))
importedUrl = 'http:' + importedUrl;
if (context.visited.indexOf(importedUrl) > -1)
if (mediaQuery.length > 0)
importedData = '@media ' + mediaQuery + '{' + importedData + '}';
+ context.afterImport = true;
+
var newContext = override(context, {
isRemote: true,
relativeTo: parsedUrl.protocol + '//' + parsedUrl.host + parsedUrl.pathname
if (mediaQuery.length > 0)
importedData = '@media ' + mediaQuery + '{' + importedData + '}';
+ context.afterImport = true;
+
var newContext = override(context, {
relativeTo: importRelativeTo
});
}
})
})
+ .addBatch({
+ 'disable all @import': pipedContext('@import url(http://127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);', '--skip-import-from all', {
+ 'should disable the remote import processing': function (error, stdout) {
+ assert.equal(stdout, '@import url(http://127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);');
+ }
+ }),
+ 'disable remote @import': pipedContext('@import url(http://127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);', '--skip-import-from remote', {
+ 'should disable the remote import processing': function (error, stdout) {
+ assert.equal(stdout, '@import url(http://127.0.0.1/remote.css);.one{color:red}');
+ }
+ }),
+ 'disable remote @import by host': pipedContext('@import url(http://127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);', '--skip-import-from 127.0.0.1', {
+ 'should disable the remote import processing': function (error, stdout) {
+ assert.equal(stdout, '@import url(http://127.0.0.1/remote.css);.one{color:red}');
+ }
+ })
+ })
.addBatch({
'relative image paths': {
'no root & output': binaryContext('./test/fixtures/partials-relative/base.css', {
nock.cleanAll();
}
},
+ 'of a remote resource mixed with local ones and disabled remote imports': {
+ topic: function () {
+ var source = '@import url(http://127.0.0.1/skipped.css);@import url(test/fixtures/partials/one.css);';
+ new CleanCSS({ processImportFrom: ['local'] }).minify(source, this.callback);
+ },
+ 'should not raise errors': function (error, minified) {
+ assert.isEmpty(minified.errors);
+ },
+ 'should not raise warnings': function (error, minified) {
+ assert.isEmpty(minified.warnings);
+ },
+ 'should keep imports': function (error, minified) {
+ assert.equal(minified.styles, '@import url(http://127.0.0.1/skipped.css);.one{color:red}');
+ }
+ },
'of a remote file that imports relative stylesheets': {
topic: function () {
var source = '@import url(http://127.0.0.1/test/folder/remote.css);';
delete process.env.http_proxy;
}
}
+}).addBatch({
+ 'allowed imports - not set': {
+ topic: function () {
+ var source = '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);';
+ this.reqMocks1 = nock('http://127.0.0.1')
+ .get('/remote.css')
+ .reply(200, 'div{border:0}');
+ this.reqMocks2 = nock('http://assets.127.0.0.1')
+ .get('/remote.css')
+ .reply(200, 'p{width:100%}');
+
+ new CleanCSS().minify(source, this.callback);
+ },
+ 'should not raise errors': function (error, minified) {
+ assert.isEmpty(minified.errors);
+ },
+ 'should not raise warnings': function (error, minified) {
+ assert.isEmpty(minified.warnings);
+ },
+ 'should process imports': function (error, minified) {
+ assert.equal(minified.styles, 'div{border:0}p{width:100%}.one{color:red}');
+ },
+ teardown: function () {
+ assert.isTrue(this.reqMocks1.isDone());
+ assert.isTrue(this.reqMocks2.isDone());
+ nock.cleanAll();
+ }
+ },
+ 'allowed imports - not set and disabled by processImport': {
+ topic: function () {
+ var source = '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);';
+ new CleanCSS({ processImport: false }).minify(source, this.callback);
+ },
+ 'should not raise errors': function (error, minified) {
+ assert.isEmpty(minified.errors);
+ },
+ 'should not raise warnings': function (error, minified) {
+ assert.isEmpty(minified.warnings);
+ },
+ 'should process imports': function (error, minified) {
+ assert.equal(minified.styles, '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);');
+ }
+ },
+ 'allowed imports - local': {
+ topic: function () {
+ var source = '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);';
+ new CleanCSS({ processImportFrom: ['local'] }).minify(source, this.callback);
+ },
+ 'should not raise errors': function (error, minified) {
+ assert.isEmpty(minified.errors);
+ },
+ 'should not raise warnings': function (error, minified) {
+ assert.isEmpty(minified.warnings);
+ },
+ 'should keeps imports': function (error, minified) {
+ assert.equal(minified.styles, '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);.one{color:red}');
+ }
+ },
+ 'allowed imports - remote': {
+ topic: function () {
+ var source = '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);';
+ this.reqMocks1 = nock('http://127.0.0.1')
+ .get('/remote.css')
+ .reply(200, 'div{border:0}');
+ this.reqMocks2 = nock('http://assets.127.0.0.1')
+ .get('/remote.css')
+ .reply(200, 'p{width:100%}');
+ new CleanCSS({ processImportFrom: ['remote'] }).minify(source, this.callback);
+ },
+ 'should not raise errors': function (error, minified) {
+ assert.isEmpty(minified.errors);
+ },
+ 'should not raise warnings': function (error, minified) {
+ assert.lengthOf(minified.warnings, 1);
+ },
+ 'should process imports': function (error, minified) {
+ assert.equal(minified.styles, 'div{border:0}p{width:100%}');
+ },
+ teardown: function () {
+ assert.isTrue(this.reqMocks1.isDone());
+ assert.isTrue(this.reqMocks2.isDone());
+ nock.cleanAll();
+ }
+ },
+ 'allowed imports - all': {
+ topic: function () {
+ var source = '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);';
+ this.reqMocks1 = nock('http://127.0.0.1')
+ .get('/remote.css')
+ .reply(200, 'div{border:0}');
+ this.reqMocks2 = nock('http://assets.127.0.0.1')
+ .get('/remote.css')
+ .reply(200, 'p{width:100%}');
+ new CleanCSS({ processImportFrom: ['all'] }).minify(source, this.callback);
+ },
+ 'should not raise errors': function (error, minified) {
+ assert.isEmpty(minified.errors);
+ },
+ 'should not raise warnings': function (error, minified) {
+ assert.isEmpty(minified.warnings);
+ },
+ 'should process imports': function (error, minified) {
+ assert.equal(minified.styles, 'div{border:0}p{width:100%}.one{color:red}');
+ },
+ teardown: function () {
+ assert.isTrue(this.reqMocks1.isDone());
+ assert.isTrue(this.reqMocks2.isDone());
+ nock.cleanAll();
+ }
+ },
+ 'allowed imports - blacklisted': {
+ topic: function () {
+ var source = '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);';
+ new CleanCSS({ processImportFrom: ['remote', 'local', '!assets.127.0.0.1', '!127.0.0.1', '!test/fixtures/partials/one.css'] }).minify(source, this.callback);
+ },
+ 'should not raise errors': function (error, minified) {
+ assert.isEmpty(minified.errors);
+ },
+ 'should raise a warning': function (error, minified) {
+ assert.isEmpty(minified.warnings);
+ },
+ 'should process first imports': function (error, minified) {
+ assert.equal(minified.styles, '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);');
+ }
+ },
+ 'allowed imports - blacklisted & no-protocol': {
+ topic: function () {
+ var source = '@import url(//127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);';
+ new CleanCSS({ processImportFrom: ['!127.0.0.1'] }).minify(source, this.callback);
+ },
+ 'should not raise errors': function (error, minified) {
+ assert.isEmpty(minified.errors);
+ },
+ 'should raise a warning': function (error, minified) {
+ assert.isEmpty(minified.warnings);
+ },
+ 'should process first imports': function (error, minified) {
+ assert.equal(minified.styles, '@import url(//127.0.0.1/remote.css);.one{color:red}');
+ }
+ }
}).export(module);