Skip to content

Commit

Permalink
Use native brotli when available and make iltorb & node-zopfli-es opt…
Browse files Browse the repository at this point in the history
…ional (#36)

* chore: Zopfli and brotli optional with fallback

* fixup: replace console.warn with process.emitWarning

* update readme

* fixup simplify zopfli fallback
  • Loading branch information
zetlen authored and Alorel committed Apr 29, 2019
1 parent 3018a26 commit 40a3717
Show file tree
Hide file tree
Showing 7 changed files with 562 additions and 183 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,36 @@ toolchain](https://github.com/nodejs/node-gyp#installation).
npm install shrink-ray-current
```

## Peer Dependencies

The brotli and zopfli algorithms rely on Node modules with native bindings.
Some environments may not be able to build these dependencies, but
`shrink-ray` tries to run even when they are absent; it just falls back to
gzip.

Therefore, the `iltorb` and `node-zopfli-es` modules are listed as
`peerDependencies` in `package.json`. This is for two reasons:

- `shrink-ray` will install successfully without them and fall back to gzip
- Projects using `shrink-ray` can specify their own version ranges of these
dependencies for maximum control

Add them manually to your `package.json` as `optionalDependencies`:

```js
"optionalDependencies": {
"iltorb": "~2.0.0",
"node-zopfli-es": "~1.0.3"
}
```

Then, run `npm install` again.

_(Node `>=11.8` has Brotli compression built in, but `shrink-ray` supports
prior versions of Node as well. If your version of Node is `>=11.8`, the
`iltorb` module is not necessary at runtime; however, you'll still see a
warning at install time.)_

# API

```js
Expand Down
62 changes: 62 additions & 0 deletions brotli-compat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module.exports = getBrotliModule;

function getBrotliModule() {
const zlib = require('zlib');

if (typeof zlib.createBrotliCompress === 'function') {
/**
* Map an options object for `iltorb` into an options object for `brotli`.
*/
const ILTORB_OPTION_NAMES_TO_BROTLI_PARAM_NAMES = {
mode: zlib.constants.BROTLI_PARAM_MODE,
quality: zlib.constants.BROTLI_PARAM_QUALITY,
lgwin: zlib.constants.BROTLI_PARAM_LGWIN,
lgblock: zlib.constants.BROTLI_PARAM_LGBLOCK,
disable_literal_context_modeling:
zlib.constants.BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING,
large_window: zlib.constants.BROTLI_PARAM_LARGE_WINDOW
};
const iltorbOptionsToNodeZlibBrotliOpts = iltorbOpts => {
if (!iltorbOpts) return iltorbOpts;
const params = {};
Object.keys(iltorbOpts).forEach(key => {
if (ILTORB_OPTION_NAMES_TO_BROTLI_PARAM_NAMES.hasOwnProperty(key)) {
params[ILTORB_OPTION_NAMES_TO_BROTLI_PARAM_NAMES[key]] = iltorbOpts[
key
];
}
});
return { params };
}

/**
* Replicate the 'iltorb' interface for backwards compatibility.
*/
return {
compressStream: opts => zlib.createBrotliCompress(
iltorbOptionsToNodeZlibBrotliOpts(opts)
),
decompressStream: opts => zlib.createBrotliDecompress(
iltorbOptionsToNodeZlibBrotliOpts(opts)
)
};

}

// If we get here, then our NodeJS does not support brotli natively.
try {
return require('iltorb');
} catch (e) {
process.emitWarning('Module "iltorb" was unavailable.',
{
type: 'MISSING_MODULE',
code: 'BROTLI_COMPAT',
detail: 'Brotli compression unavailable; will fall back to gzip.'
}
);
}

// Return a signal value instead of throwing an exception, so the code in the
// index file doesn't have to try/catch again.
return false;
}
28 changes: 23 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const bytes = require('bytes');
const compressible = require('compressible');
const debug = require('debug')('compression');
const Duplex = require('stream').Duplex;
const iltorb = require('iltorb');
const lruCache = require('lru-cache');
const multipipe = require('multipipe');
const onHeaders = require('on-headers');
Expand All @@ -24,7 +23,24 @@ const util = require('util');
const vary = require('vary');
const Writable = require('stream').Writable;
const zlib = require('zlib');
const zopfli = require('node-zopfli-es');

/**
* Optional dependencies handling. If some binary dependencies cannot build in
* this environment, or are incompatible with this version of Node, the rest of
* the module should work!
* Known dependency issues:
* - node-zopfli-es is not compatible with Node <8.11.
* - iltorb is not required for Node >= 11.8, whose zlib has brotli built in.
*/

const brotliCompat = require('./brotli-compat');
const zopfliCompat = require('./zopfli-compat');

// These are factory functions because they dynamically require dependencies
// and may log errors.
// They need to be tested, so they shouldn't have side effects on load.
const brotli = brotliCompat();
const zopfli = zopfliCompat();

/**
* Module exports.
Expand Down Expand Up @@ -206,7 +222,9 @@ function compression(options) {
// instead enforce server preference. also, server-sent events (mime type of
// text/event-stream) require flush functionality, so skip brotli in that
// case.
const method = (contentType !== 'text/event-stream' && accept.encoding('br')) ||
// lastly, if brotli is unavailable or unsupported on this platform,
// the object will be falsy.
const method = (brotli && contentType !== 'text/event-stream' && accept.encoding('br')) ||
accept.encoding('gzip') ||
accept.encoding('deflate') ||
accept.encoding('identity');
Expand Down Expand Up @@ -236,7 +254,7 @@ function compression(options) {
debug('%s compression', method);
switch (method) {
case 'br':
stream = iltorb.compressStream(brotliOpts);
stream = brotli.compressStream(brotliOpts);
break;
case 'gzip':
stream = zlib.createGzip(zlibOpts);
Expand Down Expand Up @@ -483,6 +501,6 @@ function getBestQualityReencoder(coding) {
const PassThrough = require('stream').PassThrough;
return new PassThrough();
case 'br':
return multipipe(iltorb.decompressStream(), iltorb.compressStream());
return multipipe(brotli.decompressStream(), brotli.compressStream());
}
}
Loading

0 comments on commit 40a3717

Please sign in to comment.