Skip to content
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
30 changes: 30 additions & 0 deletions examples/using-static-hashing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/using-static-hashing)
# Example app utilizing next/static for immutable static files

## How to use

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

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

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

This example features:

* Optimized static file serving using `next/static`.
7 changes: 7 additions & 0 deletions examples/using-static-hashing/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
exportPathMap: function () {
return {
'/': { page: '/' }
}
}
}
17 changes: 17 additions & 0 deletions examples/using-static-hashing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "hello-world",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"export": "next export",
"start": "next start"
},
"dependencies": {
"next": "*",
"react": "^15.4.2",
"react-dom": "^15.4.2"
},
"author": "",
"license": "ISC"
}
39 changes: 39 additions & 0 deletions examples/using-static-hashing/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Component } from 'react'
import _static from 'next/static'

export default class Index extends Component {
state = {
white: true
}

render () {
const { white } = this.state
const logoSrc = white
? _static('white-logo.svg')
: _static('black-logo.svg')

return (
<div
onClick={() => this.setState({ white: !this.state.white })}
style={{ background: white ? 'white' : 'black' }}>
<img src={logoSrc} />
<style jsx>
{`
:global(body) {
margin: 0;
padding: 0;
}
div {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
transition: background 1s;
}
`}
</style>
</div>
)
}
}
17 changes: 17 additions & 0 deletions examples/using-static-hashing/static/black-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions examples/using-static-hashing/static/white-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions lib/static.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function _static () {
throw new Error('Please check you are using the next/babel preset and you are calling next/static with a string literal not a computed value')
}
86 changes: 86 additions & 0 deletions server/build/babel/plugins/handle-static.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const fs = require('fs')
const path = require('path')
const md5 = require('md5-file')

const dev = process.env.NODE_ENV !== 'production'
const moduleName = 'next/static'
const staticStats = {}
const staticStatsPath = process.env.__NEXT_STATIC_STATS_PATH__
const staticDir = process.env.__NEXT_STATIC_DIR__
Copy link
Contributor Author

Choose a reason for hiding this comment

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

need better way to tell babel plugin where the static dir is located

Copy link

Choose a reason for hiding this comment

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

@cloudkite I wonder if it would make sense for the static location to somehow be configured / set as part of config: https://github.com/zeit/next.js/blob/master/server/config.js

I know it's not configurable at the moment, but getConfig has access to dir and the result is cached / stored.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kochis that could work 👍 @arunoda what do you think of this approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kochis not sure when I'll get a chance to pick this up, feel free to jump in :)


function getHashedName (fileName) {
let hashedName = staticStats[fileName]
if (hashedName) return hashedName

const hash = md5.sync(path.join(staticDir, fileName))
const ext = path.extname(fileName)
const name = path.basename(fileName, ext)
hashedName = staticStats[fileName] = `${name}_${hash}${ext}`

// update assets.json
let stats = JSON.parse(fs.readFileSync(staticStatsPath, 'utf8'))
stats = Object.assign({}, stats, staticStats)
fs.writeFileSync(staticStatsPath, JSON.stringify(stats, null, 2))

return hashedName
}

module.exports = function ({ types: t }) {
const identifiers = new Set()
return {
visitor: {
ImportDeclaration (path) {
const defaultSpecifierPath = path.get('specifiers')[0]
if (
path.node.source.value !== moduleName ||
!t.isImportDefaultSpecifier(defaultSpecifierPath)
) {
return
}
const { node: { local: { name } } } = defaultSpecifierPath
const { referencePaths } = path.scope.getBinding(name)
referencePaths.forEach(reference => {
identifiers.add(reference)
})
},
VariableDeclarator (path) {
const { node } = path
if (!isRequireCall(node.init) || !t.isIdentifier(node.id)) {
return
}
const { id: { name } } = node
const binding = path.scope.getBinding(name)
if (binding) {
const { referencePaths } = binding
referencePaths.forEach(reference => {
identifiers.add(reference)
})
}
},
Program: {
exit () {
Array.from(identifiers).forEach(identifier => {
if (!t.isCallExpression(identifier.parent)) return
const [firstArg] = identifier.parent.arguments
if (!t.isStringLiteral(firstArg)) return

const replacement = !dev
? `/_next/static/${getHashedName(firstArg.value)}`
: `/static/${firstArg.value}`
identifier.parentPath.replaceWith(t.stringLiteral(replacement))
})
}
}
}
}

function isRequireCall (callExpression) {
return (
callExpression &&
callExpression.type === 'CallExpression' &&
callExpression.callee.name === 'require' &&
callExpression.arguments.length === 1 &&
callExpression.arguments[0].value === moduleName
)
}
}
1 change: 1 addition & 0 deletions server/build/babel/preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = {
plugins: [
require.resolve('babel-plugin-react-require'),
require.resolve('./plugins/handle-import'),
require.resolve('./plugins/handle-static'),
require.resolve('babel-plugin-transform-object-rest-spread'),
require.resolve('babel-plugin-transform-class-properties'),
require.resolve('babel-plugin-transform-runtime'),
Expand Down
14 changes: 14 additions & 0 deletions server/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import uuid from 'uuid'
import del from 'del'
import webpack from './webpack'
import replaceCurrentBuild from './replace'
import mkdirp from 'mkdirp-then'
import md5File from 'md5-file/promise'

export default async function build (dir, conf = null) {
const buildDir = join(tmpdir(), uuid.v4())
await writeStaticStats(dir, buildDir)
const compiler = await webpack(dir, { buildDir, conf })

try {
Expand Down Expand Up @@ -45,6 +47,18 @@ function runCompiler (compiler) {
})
}

async function writeStaticStats (dir, buildDir) {
await mkdirp(join(buildDir, '.next'))
const staticStatsPath = join(buildDir, '.next', 'static-stats.json')

// this is a hacky but not sure how to tell babel plugin about location of static folder
process.env.__NEXT_STATIC_STATS_PATH__ = staticStatsPath
process.env.__NEXT_STATIC_DIR__ = join(dir, 'static')

// write empty json
await fs.writeFile(staticStatsPath, JSON.stringify({}, null, 2))
}

async function writeBuildStats (dir) {
// Here we can't use hashes in webpack chunks.
// That's because the "app.js" is not tied to a chunk.
Expand Down
10 changes: 10 additions & 0 deletions server/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default async function (dir, options) {

const buildId = readFileSync(join(nextDir, 'BUILD_ID'), 'utf8')
const buildStats = require(join(nextDir, 'build-stats.json'))
const staticStats = require(join(nextDir, 'static-stats.json'))

// Initialize the output directory
const outDir = options.outdir
Expand All @@ -47,6 +48,15 @@ export default async function (dir, options) {
)
}

// Copy hashed static files
await mkdirp(join(outDir, '_next', 'static'))
for (const filename of Object.keys(staticStats)) {
await cp(
join(dir, 'static', filename),
join(outDir, '_next', 'static', staticStats[filename])
)
}

// Copy dynamic import chunks
if (existsSync(join(nextDir, 'chunks'))) {
log(' copying dynamic import chunks')
Expand Down
16 changes: 16 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export default class Server {
this.config = getConfig(this.dir, conf)
this.dist = this.config.distDir
this.buildStats = !dev ? require(join(this.dir, this.dist, 'build-stats.json')) : null
const staticStats = !dev ? require(join(this.dir, this.dist, 'static-stats.json')) : {}
this.staticStats = Object.entries(staticStats)
.reduce((obj, [key, value]) => ({ ...obj, [value]: key }), {})

this.buildId = !dev ? this.readBuildId() : '-'
this.renderOpts = {
dev,
Expand Down Expand Up @@ -187,6 +191,18 @@ export default class Server {
await renderScript(req, res, page, this.renderOpts)
},

'/_next/static/:name': async (req, res, params) => {
if (this.dev) return this.send404(res)
const filename = this.staticStats[params.name]
if (!filename) {
throw new Error(`Invalid Static Path: ${req.url}`)
}

res.setHeader('Cache-Control', 'max-age=365000000, immutable')
const p = join(this.dir, 'static', filename)
await this.serveStatic(req, res, p)
},

'/_next/:path+': async (req, res, params) => {
const p = join(__dirname, '..', 'client', ...(params.path || []))
await this.serveStatic(req, res, p)
Expand Down
1 change: 1 addition & 0 deletions static.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/lib/static')