From a2c153687d67cbf0a1c6a0015520f28dea25e658 Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Thu, 23 Mar 2017 20:34:42 +0100 Subject: [PATCH] Fixes #755 - adds custom handling of remote requests. Why: * To allow flexibility in a way of fetching remote resources, for example requests via CONNECT proxies, an external request library can be used, see example in readme; * instead of reinventing a wheel a better approach is to use an external library based on users' choice. --- History.md | 1 + README.md | 29 +++++++++++++ lib/clean.js | 2 + lib/options/fetch.js | 7 ++++ lib/reader/apply-source-maps.js | 4 +- lib/reader/load-original-sources.js | 4 +- lib/reader/read-sources.js | 4 +- test/protocol-imports-test.js | 65 +++++++++++++++++++++++++++++ 8 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 lib/options/fetch.js diff --git a/History.md b/History.md index a8f3deee..784fa444 100644 --- a/History.md +++ b/History.md @@ -7,6 +7,7 @@ * Fixed issue [#893](https://github.com/jakubpawlowicz/clean-css/issues/893) - `inline: false` as alias to `inline: 'none'`. * Fixed issue [#890](https://github.com/jakubpawlowicz/clean-css/issues/890) - adds toggle to disable empty tokens removal. * Fixed issue [#886](https://github.com/jakubpawlowicz/clean-css/issues/886) - better multi pseudo class / element merging. +* Fixed issue [#755](https://github.com/jakubpawlowicz/clean-css/issues/755) - adds custom handling of remote requests. [4.0.10 / 2017-03-22](https://github.com/jakubpawlowicz/clean-css/compare/v4.0.9...v4.0.10) ================== diff --git a/README.md b/README.md index 7f9dbee7..f4e04bc5 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ According to [tests](http://goalsmashers.github.io/css-minification-benchmark/) * [What's coming in version 4.1](#whats-coming-in-version-41) * [Constructor options](#constructor-options) * [Compatibility modes](#compatibility-modes) + * [Fetch option](#fetch-option) * [Formatting options](#formatting-options) * [Inlining options](#inlining-options) * [Optimization levels](#optimization-levels) @@ -103,12 +104,14 @@ Once released clean-css 4.1 will introduce the following changes / features: * `compatibility: { selectors: { mergeLimit: } }` flag in compatibility settings controlling maximum number of selectors in a single rule; * `minify` method improved signature accepting a list of hashes for a predictable traversal; * `selectorsSortingMethod` level 1 optimization allows `false` or `'none'` for disabling selector sorting; +* `fetch` option controlling a function for handling remote requests; ## Constructor options clean-css constructor accepts a hash as a parameter with the following options available: * `compatibility` - controls compatibility mode used; defaults to `ie10+`; see [compatibility modes](#compatibility-modes) for examples; +* `fetch` - controls a function for handling remote requests; see [fetch option](#fetch-option) for examples (since 4.1.0-pre); * `format` - controls output CSS formatting; defaults to `false`; see [formatting options](#formatting-options) for examples; * `inline` - controls `@import` inlining rules; defaults to `'local'`; see [inlining options](#inlining-options) for examples; * `inlineRequest` - controls extra options for inlining remote `@import` rules, can be any of [HTTP(S) request options](https://nodejs.org/api/http.html#http_http_request_options_callback); @@ -183,6 +186,32 @@ new CleanCSS({ }) ``` +## Fetch option + +The `fetch` option accepts a function which handles remote resource fetching, e.g. + +```js +var request = require('request'); +var source = '@import url(http://example.com/path/to/stylesheet.css);'; +new CleanCSS({ + fetch: function (uri, inlineRequest, inlineTimeout, callback) { + request(uri, function (error, response, body) { + if (error) { + callback(error, null); + } else if (response && response.statusCode != 200) { + callback(response.statusCode, null); + } else { + callback(null, body); + } + }); + } +}).minify(source); +``` + +This option provides a convenient way of overriding the default fetching logic if it doesn't support a particular feature, say CONNECT proxies. + +Unless given, the default [loadRemoteResource](https://github.com/jakubpawlowicz/clean-css/blob/master/lib/reader/load-remote-resource.js) logic is used. + ## Formatting options The `format` option accept the following options: diff --git a/lib/clean.js b/lib/clean.js index f8268777..62308efe 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -11,6 +11,7 @@ var level2Optimize = require('./optimizer/level-2/optimize'); var validator = require('./optimizer/validator'); var compatibilityFrom = require('./options/compatibility'); +var fetchFrom = require('./options/fetch'); var formatFrom = require('./options/format').formatFrom; var inlineFrom = require('./options/inline'); var inlineRequestFrom = require('./options/inline-request'); @@ -31,6 +32,7 @@ var CleanCSS = module.exports = function CleanCSS(options) { this.options = { compatibility: compatibilityFrom(options.compatibility), + fetch: fetchFrom(options.fetch), format: formatFrom(options.format), inline: inlineFrom(options.inline), inlineRequest: inlineRequestFrom(options.inlineRequest), diff --git a/lib/options/fetch.js b/lib/options/fetch.js new file mode 100644 index 00000000..0aaad786 --- /dev/null +++ b/lib/options/fetch.js @@ -0,0 +1,7 @@ +var loadRemoteResource = require('../reader/load-remote-resource'); + +function fetchFrom(callback) { + return callback || loadRemoteResource; +} + +module.exports = fetchFrom; diff --git a/lib/reader/apply-source-maps.js b/lib/reader/apply-source-maps.js index d2f87250..7c5a9282 100644 --- a/lib/reader/apply-source-maps.js +++ b/lib/reader/apply-source-maps.js @@ -2,7 +2,6 @@ var fs = require('fs'); var path = require('path'); var isAllowedResource = require('./is-allowed-resource'); -var loadRemoteResource = require('./load-remote-resource'); var matchDataUri = require('./match-data-uri'); var rebaseLocalMap = require('./rebase-local-map'); var rebaseRemoteMap = require('./rebase-remote-map'); @@ -17,6 +16,7 @@ var MAP_MARKER_PATTERN = /^\/\*# sourceMappingURL=(\S+) \*\/$/; function applySourceMaps(tokens, context, callback) { var applyContext = { callback: callback, + fetch: context.options.fetch, index: 0, inline: context.options.inline, inlineRequest: context.options.inlineRequest, @@ -151,7 +151,7 @@ function loadInputSourceMapFromRemoteUri(uri, applyContext, whenLoaded) { return whenLoaded(null); } - loadRemoteResource(uri, applyContext.inlineRequest, applyContext.inlineTimeout, function (error, body) { + applyContext.fetch(uri, applyContext.inlineRequest, applyContext.inlineTimeout, function (error, body) { if (error) { applyContext.warnings.push('Missing source map at "' + uri + '" - ' + error); return whenLoaded(null); diff --git a/lib/reader/load-original-sources.js b/lib/reader/load-original-sources.js index dbe2cad0..465035d6 100644 --- a/lib/reader/load-original-sources.js +++ b/lib/reader/load-original-sources.js @@ -2,7 +2,6 @@ var fs = require('fs'); var path = require('path'); var isAllowedResource = require('./is-allowed-resource'); -var loadRemoteResource = require('./load-remote-resource'); var hasProtocol = require('../utils/has-protocol'); var isRemoteResource = require('../utils/is-remote-resource'); @@ -10,6 +9,7 @@ var isRemoteResource = require('../utils/is-remote-resource'); function loadOriginalSources(context, callback) { var loadContext = { callback: callback, + fetch: context.options.fetch, index: 0, inline: context.options.inline, inlineRequest: context.options.inlineRequest, @@ -99,7 +99,7 @@ function loadOriginalSourceFromRemoteUri(uri, loadContext, whenLoaded) { return whenLoaded(null); } - loadRemoteResource(uri, loadContext.inlineRequest, loadContext.inlineTimeout, function (error, content) { + loadContext.fetch(uri, loadContext.inlineRequest, loadContext.inlineTimeout, function (error, content) { if (error) { loadContext.warnings.push('Missing original source at "' + uri + '" - ' + error); } diff --git a/lib/reader/read-sources.js b/lib/reader/read-sources.js index ed8c5a43..c9173ed6 100644 --- a/lib/reader/read-sources.js +++ b/lib/reader/read-sources.js @@ -5,7 +5,6 @@ var applySourceMaps = require('./apply-source-maps'); var extractImportUrlAndMedia = require('./extract-import-url-and-media'); var isAllowedResource = require('./is-allowed-resource'); var loadOriginalSources = require('./load-original-sources'); -var loadRemoteResource = require('./load-remote-resource'); var normalizePath = require('./normalize-path'); var rebase = require('./rebase'); var rebaseLocalMap = require('./rebase-local-map'); @@ -163,6 +162,7 @@ function inline(tokens, externalContext, parentInlinerContext, callback) { callback: callback, errors: externalContext.errors, externalContext: externalContext, + fetch: externalContext.options.fetch, inlinedStylesheets: parentInlinerContext.inlinedStylesheets || externalContext.inlinedStylesheets, inline: parentInlinerContext.inline, inlineRequest: externalContext.options.inlineRequest, @@ -278,7 +278,7 @@ function inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) { return isLoaded ? whenLoaded(null, inlinerContext.externalContext.sourcesContent[uri]) : - loadRemoteResource(uri, inlinerContext.inlineRequest, inlinerContext.inlineTimeout, whenLoaded); + inlinerContext.fetch(uri, inlinerContext.inlineRequest, inlinerContext.inlineTimeout, whenLoaded); } function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) { diff --git a/test/protocol-imports-test.js b/test/protocol-imports-test.js index 468c2ea2..20d49cde 100644 --- a/test/protocol-imports-test.js +++ b/test/protocol-imports-test.js @@ -898,4 +898,69 @@ vows.describe('protocol imports').addBatch({ nock.cleanAll(); } } +}).addBatch({ + 'custom fetch callback with no request': { + topic: function () { + new CleanCSS({ + fetch: function (uri, inlineRequest, inlineTimeout, callback) { + if (uri == 'http://localhost:12345/custom.css') { + callback(null, 'a{color:red}'); + } + }, + inline: 'all' + }).minify('@import url(http://localhost:12345/custom.css);', this.callback); + }, + 'should process @import': function (errors, minified) { + assert.equal(minified.styles, 'a{color:red}'); + } + }, + 'custom fetch callback with real request': { + topic: function () { + var self = this; + nock.enableNetConnect(); + + this.server = http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/css'}); + res.end('a{color:red}'); + }); + this.server.listen(port, function () { + new CleanCSS({ + fetch: function (uri, inlineRequest, inlineTimeout, callback) { + if (uri == 'http://localhost:' + port + '/custom.css') { + http.get(uri, function (res) { + var rawData = []; + + res.on('data', function (chunk) { rawData.push(chunk); }); + res.on('end', function () { + callback(null, rawData.join('')); + }); + }); + } + }, + inline: 'all' + }).minify('@import url(http://localhost:' + port + '/custom.css);', self.callback); + }); + + enableDestroy(this.server); + }, + 'gets right output': function (errors, minified) { + assert.equal(minified.styles, 'a{color:red}'); + }, + teardown: function () { + this.server.destroy(); + } + }, + 'custom fetch callback with error': { + topic: function () { + new CleanCSS({ + fetch: function (uri, inlineRequest, inlineTimeout, callback) { + callback('some error', null); + }, + inline: 'all' + }).minify('@import url(http://localhost:12345/custom.css);', this.callback); + }, + 'should raise error': function (errors, minified) { + assert.deepEqual(errors, ['Broken @import declaration of "http://localhost:12345/custom.css" - some error']); + } + } }).export(module); -- 2.34.1