Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for customizing compress encodings #657

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,43 @@ Here's an example `.babelrc` file:
}
```

### Customizing compression encodings

<p><details>
<summary><b>Examples</b></summary>
<ul><li><a href="./examples/with-custom-compress-encodings">With custom compress encodings</a></li></ul>
</details></p>

By default Next.js will compress your core JavaScript assets for `gzip` encoding while it builds(with `next build`) the app.
If you need to add support for more encoding, you could simply add it via `next.config.js`.

Here's an example config which adds supports for both [`br`](https://en.wikipedia.org/wiki/Brotli) and `gzip` encodings.

```js
var zlib = require('zlib')
var iltorb = require('iltorb')

module.exports = {
// Returns a map of compression streams for the types of encodings you want to support
// Here's a list of common encoding types: https://goo.gl/ke7zOK

// The first listed encoding has the higher priority over others.
// In this case, first it'll try to serve the `br` version if the browser supports it.
// Otherwise, it'll serve the gzipped version.
compress: {
br: function () {
return iltorb.compressStream()
},
gzip: function () {
// You can also return a promise which resolve a compression stream
return new Promise(function (resolve) {
resolve(zlib.createGzip())
})
}
}
}
```

## Production deployment

To deploy, instead of running `next`, you probably want to build ahead of time. Therefore, building and starting are separate commands:
Expand Down
29 changes: 29 additions & 0 deletions examples/with-custom-compress-encodings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Example app with custom compress encodings

## How to use

Download the example (or clone the repo)[https://github.com/zeit/next.js.git]:

```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-custom-compress-encodings
cd with-custom-compress-encodings
```

Install it and run:

```bash
npm install
npm run dev
```

Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))

```bash
now
```

## The idea behind the example

By default, Next.js compile your assets with `gzip` compression. But if you want to add support more encodings like `br` or `deflate` you can do it very easily.

This example shows how to add support for `br` and `gzip` compress encodings. For that it uses a config in `next.config.js`.
19 changes: 19 additions & 0 deletions examples/with-custom-compress-encodings/components/Counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'

let count = 0

export default class Counter extends React.Component {
add () {
count += 1
this.forceUpdate()
}

render () {
return (
<div>
<p>Count is: {count}</p>
<button onClick={() => this.add()}>Add</button>
</div>
)
}
}
19 changes: 19 additions & 0 deletions examples/with-custom-compress-encodings/components/Header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Link from 'next/link'

export default () => (
<div>
<Link href='/'>
<a style={styles.a} >Home</a>
</Link>

<Link href='/about'>
<a style={styles.a} >About</a>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be cool to use styled-jsx

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanna talk about this feature in this example.
Anyway, I don't mind using that too.

</Link>
</div>
)

const styles = {
a: {
marginRight: 10
}
}
22 changes: 22 additions & 0 deletions examples/with-custom-compress-encodings/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
var zlib = require('zlib')
var iltorb = require('iltorb')

module.exports = {
// Returns a map of compression streams for the types of encodings you want to support
// Here's a list of common encoding types: https://goo.gl/ke7zOK

// The first listed encoding has the higher priority over others.
// In this case, first it'll try to serve the `br` version if the browser supports it.
// Otherwise, it'll serve the gzipped version.
compress: {
br: function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about compress the syntax to something like:

  br: iltorb.compressStream,
  gzip: () => new Promise(resolve => resolve(zlib.createGzip()))
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't is the same as right now except it should be br: iltorb.compressStream()?

If you are looking at ES2015 features, I didn't use them because we don't transpile the next.config.js.

return iltorb.compressStream()
},
gzip: function () {
// You can also return a promise which resolve a compression stream
return new Promise(function (resolve) {
resolve(zlib.createGzip())
})
}
}
}
17 changes: 17 additions & 0 deletions examples/with-custom-compress-encodings/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "with-custom-compress-encodings",
"version": "1.0.0",
"description": "This example features:",
"main": "index.js",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"iltorb": "^1.0.13",
"next": "^2.0.0-beta"
},
"author": "",
"license": "ISC"
}
10 changes: 10 additions & 0 deletions examples/with-custom-compress-encodings/pages/about.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Header from '../components/Header'
import Counter from '../components/Counter'

export default () => (
<div>
<Header />
<p>This is the about page.</p>
<Counter />
</div>
)
10 changes: 10 additions & 0 deletions examples/with-custom-compress-encodings/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Header from '../components/Header'
import Counter from '../components/Counter'

export default () => (
<div>
<Header />
<p>HOME PAGE is here!</p>
<Counter />
</div>
)
57 changes: 57 additions & 0 deletions server/build/compress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import fs from 'fs'
import path from 'path'
import zlib from 'zlib'
import glob from 'glob-promise'
import getConfig from '../config'

export default async function compressAssets (dir) {
const nextDir = path.resolve(dir, '.next')
const config = getConfig(dir)

const coreAssets = [
path.join(nextDir, 'commons.js'),
path.join(nextDir, 'main.js')
]
const pages = await glob('bundles/pages/**/*.json', { cwd: nextDir })

const allAssets = [
...coreAssets,
...pages.map(page => path.join(nextDir, page))
]

while (true) {
// compress only 10 assets in parallel at a time.
const currentChunk = allAssets.splice(0, 10)
if (currentChunk.length === 0) break

await Promise.all(currentChunk.map((f) => compress(config, f)))
}
}

export function compress (config, filePath) {
const compressionMap = config.compress || {
gzip: () => zlib.createGzip()
}

const promises = Object.keys(compressionMap).map((type) => {
return new Promise(async (resolve, reject) => {
const input = fs.createReadStream(filePath)
const output = fs.createWriteStream(`${filePath}.${type}`)
// We accept stream resolved via a promise or not
const compression = await compressionMap[type](filePath)

// We need to handle errors like this.
// See: http://stackoverflow.com/a/22389498
input.on('error', reject)
compression.on('error', reject)

const stream = input.pipe(compression).pipe(output)

// Handle the final stream
stream.on('error', reject)
stream.on('finish', resolve)
})
})

return Promise.all(promises)
}
38 changes: 0 additions & 38 deletions server/build/gzip.js

This file was deleted.

4 changes: 2 additions & 2 deletions server/build/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import webpack from './webpack'
import clean from './clean'
import gzipAssets from './gzip'
import compressAssets from './compress'

export default async function build (dir) {
const [compiler] = await Promise.all([
Expand All @@ -9,7 +9,7 @@ export default async function build (dir) {
])

await runCompiler(compiler)
await gzipAssets(dir)
await compressAssets(dir)
}

function runCompiler (compiler) {
Expand Down
18 changes: 12 additions & 6 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
renderErrorJSON,
sendHTML,
serveStatic,
serveStaticWithGzip
serveStaticWithCompression
} from './render'
import Router from './router'
import HotReloader from './hot-reloader'
Expand All @@ -22,11 +22,17 @@ export default class Server {
this.dir = resolve(dir)
this.dev = dev
this.quiet = quiet
this.renderOpts = { dir: this.dir, dev, staticMarkup }
this.router = new Router()
this.hotReloader = dev ? new HotReloader(this.dir, { quiet }) : null
this.http = null
this.config = getConfig(this.dir)
this.supportedEncodings = Object.keys(this.config.compress || { gzip: true })
this.renderOpts = {
dir: this.dir,
supportedEncodings: this.supportedEncodings,
dev,
staticMarkup
}

this.defineRoutes()
}
Expand Down Expand Up @@ -62,12 +68,12 @@ export default class Server {

this.router.get('/_next/main.js', async (req, res, params) => {
const p = join(this.dir, '.next/main.js')
await serveStaticWithGzip(req, res, p)
await serveStaticWithCompression(req, res, p, this.supportedEncodings)
})

this.router.get('/_next/commons.js', async (req, res, params) => {
const p = join(this.dir, '.next/commons.js')
await serveStaticWithGzip(req, res, p)
await serveStaticWithCompression(req, res, p, this.supportedEncodings)
})

this.router.get('/_next/pages/:path*', async (req, res, params) => {
Expand Down Expand Up @@ -213,9 +219,9 @@ export default class Server {
return renderErrorJSON(err, req, res, this.renderOpts)
}

async serveStaticWithGzip (req, res, path) {
async serveStaticWithCompression (req, res, path) {
this._serveStatic(req, res, () => {
return serveStaticWithGzip(req, res, path)
return serveStaticWithCompression(req, res, path, this.supportedEncodings)
})
}

Expand Down
18 changes: 10 additions & 8 deletions server/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ async function doRender (req, res, pathname, query, {
return '<!DOCTYPE html>' + renderToStaticMarkup(doc)
}

export async function renderJSON (req, res, page, { dir = process.cwd() } = {}) {
export async function renderJSON (req, res, page, { dir = process.cwd(), supportedEncodings } = {}) {
const pagePath = await resolvePath(join(dir, '.next', 'bundles', 'pages', page))
return serveStaticWithGzip(req, res, pagePath)
return serveStaticWithCompression(req, res, pagePath, supportedEncodings)
}

export async function renderErrorJSON (err, req, res, { dir = process.cwd(), dev = false } = {}) {
Expand Down Expand Up @@ -141,16 +141,18 @@ function errorToJSON (err) {
return json
}

export async function serveStaticWithGzip (req, res, path) {
const encoding = accepts(req).encodings(['gzip'])
if (encoding !== 'gzip') {
export async function serveStaticWithCompression (req, res, path, supportedEncodings) {
const acceptingEncodings = accepts(req).encodings()
const encoding = supportedEncodings.find((e) => acceptingEncodings.indexOf(e) >= 0)

if (!encoding) {
return serveStatic(req, res, path)
}

try {
const gzipPath = `${path}.gz`
res.setHeader('Content-Encoding', 'gzip')
await serveStatic(req, res, gzipPath)
const compressedPath = `${path}.${encoding}`
res.setHeader('Content-Encoding', encoding)
await serveStatic(req, res, compressedPath)
} catch (ex) {
if (ex.code === 'ENOENT') {
res.removeHeader('Content-Encoding')
Expand Down
Loading