From a2f8bac6f9adceec08c47238348077d748556f8d Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Tue, 18 Aug 2015 07:45:52 +0100 Subject: [PATCH] Fixes #632 - adds disabling remote imports. So far we only had an option to skip inlining of all imports, but with this commit a fine-grained control is added, e.g. import from all but fonts.googleapis.com: API: `new CleanCSS({ processImportFrom: ['!fonts.googleapis.com'] })` CLI: `cleancss --skip-import-from fonts.googleapis.com` To skip all local imports: API: `new CleanCSS({ processImportFrom: ['remote'] })` CLI: `cleancss --skip-import-from local` To skip all remote and certain local imports: API: `new CleanCSS({ processImportFrom: ['local', '!path/to/file'] })` CLI: `cleancss --skip-import-from remote,path/to/file` --- History.md | 2 + README.md | 2 + bin/cleancss | 19 +++++ lib/clean.js | 6 ++ lib/imports/inliner.js | 49 ++++++++++- test/binary-test.js | 17 ++++ test/protocol-imports-test.js | 155 ++++++++++++++++++++++++++++++++++ 7 files changed, 248 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index c4db416a..6f5d1047 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,7 @@ [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. @@ -12,6 +13,7 @@ * 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. diff --git a/README.md b/README.md index d129319c..05e2504a 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ cleancss [options] source-file, [source-file, ...] --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 @@ -110,6 +111,7 @@ CleanCSS constructor accepts a hash as a parameter, i.e., * `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 diff --git a/bin/cleancss b/bin/cleancss index 0ff74f7c..5f95b7e2 100755 --- a/bin/cleancss +++ b/bin/cleancss @@ -28,6 +28,7 @@ commands .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') @@ -69,6 +70,7 @@ var options = { 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, @@ -103,6 +105,23 @@ if (commands.args.length > 0) { }); } +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) { diff --git a/lib/clean.js b/lib/clean.js index 1ef29cb1..8b8db74a 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -50,6 +50,7 @@ var CleanCSS = module.exports = function CleanCSS(options) { 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, @@ -70,6 +71,10 @@ var CleanCSS = module.exports = function CleanCSS(options) { ); }; +function importOptionsFrom(rules) { + return undefined === rules ? ['all'] : rules; +} + function missingDirectory(filepath) { return !fs.existsSync(filepath) && !/\.css$/.test(filepath); } @@ -114,6 +119,7 @@ CleanCSS.prototype.minify = function (data, callback) { return runner(function () { return new ImportInliner(context).process(data, { localOnly: context.localOnly, + imports: context.options.processImportFrom, whenDone: runMinifier(callback, context) }); }); diff --git a/lib/imports/inliner.js b/lib/imports/inliner.js index e4ffad33..9a586516 100644 --- a/lib/imports/inliner.js +++ b/lib/imports/inliner.js @@ -10,6 +10,7 @@ var override = require('../utils/object.js').override; var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//; var REMOTE_RESOURCE = /^(https?:)?\/\//; +var NO_PROTOCOL_RESOURCE = /^\/\//; function ImportInliner (context) { this.outerContext = context; @@ -193,7 +194,7 @@ function inline(data, nextStart, nextEnd, 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 @@ -202,6 +203,14 @@ function inline(data, nextStart, nextEnd, context) { 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); @@ -211,13 +220,45 @@ function inline(data, nextStart, nextEnd, 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) @@ -275,6 +316,8 @@ function inlineRemoteResource(importedFile, mediaQuery, context) { if (mediaQuery.length > 0) importedData = '@media ' + mediaQuery + '{' + importedData + '}'; + context.afterImport = true; + var newContext = override(context, { isRemote: true, relativeTo: parsedUrl.protocol + '//' + parsedUrl.host + parsedUrl.pathname @@ -333,6 +376,8 @@ function inlineLocalResource(importedFile, mediaQuery, context) { if (mediaQuery.length > 0) importedData = '@media ' + mediaQuery + '{' + importedData + '}'; + context.afterImport = true; + var newContext = override(context, { relativeTo: importRelativeTo }); diff --git a/test/binary-test.js b/test/binary-test.js index b60c9aca..b743354f 100644 --- a/test/binary-test.js +++ b/test/binary-test.js @@ -214,6 +214,23 @@ vows.describe('./bin/cleancss') } }) }) + .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', { diff --git a/test/protocol-imports-test.js b/test/protocol-imports-test.js index d20cf959..502a4c5d 100644 --- a/test/protocol-imports-test.js +++ b/test/protocol-imports-test.js @@ -459,6 +459,21 @@ vows.describe('protocol imports').addBatch({ 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);'; @@ -636,4 +651,144 @@ vows.describe('protocol imports').addBatch({ 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); -- 2.34.1