diff --git a/bin/next-export b/bin/next-export
index a98f6ae7a7..c9ac30b368 100755
--- a/bin/next-export
+++ b/bin/next-export
@@ -57,8 +57,6 @@ const options = {
outdir: argv.outdir ? resolve(argv.outdir) : resolve(dir, 'out')
}
-exportApp(dir, options)
- .catch((err) => {
- console.error(err)
- process.exit(1)
- })
+exportApp(dir, options).catch((err) => {
+ printAndExit(err)
+})
diff --git a/lib/constants.js b/lib/constants.js
index ec595fa6da..19cb799242 100644
--- a/lib/constants.js
+++ b/lib/constants.js
@@ -3,3 +3,4 @@ export const PHASE_PRODUCTION_BUILD = 'phase-production-build'
export const PHASE_PRODUCTION_SERVER = 'phase-production-server'
export const PHASE_DEVELOPMENT_SERVER = 'phase-development-server'
export const PAGES_MANIFEST = 'pages-manifest.json'
+export const BUILD_MANIFEST = 'build-manifest.json'
diff --git a/package.json b/package.json
index 6b098e238b..d372fad240 100644
--- a/package.json
+++ b/package.json
@@ -134,7 +134,6 @@
"node-fetch": "1.7.3",
"node-notifier": "5.1.2",
"nyc": "11.2.1",
- "portfinder": "1.0.13",
"react": "16.2.0",
"react-dom": "16.2.0",
"rimraf": "2.6.2",
diff --git a/readme.md b/readme.md
index 3994141bf2..0505d64e50 100644
--- a/readme.md
+++ b/readme.md
@@ -943,9 +943,9 @@ import flush from 'styled-jsx/server'
export default class MyDocument extends Document {
static getInitialProps({ renderPage }) {
- const { html, head, errorHtml, chunks } = renderPage()
+ const { html, head, errorHtml, chunks, buildManifest } = renderPage()
const styles = flush()
- return { html, head, errorHtml, chunks, styles }
+ return { html, head, errorHtml, chunks, styles, buildManifest }
}
render() {
diff --git a/server/build/plugins/build-manifest-plugin.js b/server/build/plugins/build-manifest-plugin.js
new file mode 100644
index 0000000000..be2e5fee26
--- /dev/null
+++ b/server/build/plugins/build-manifest-plugin.js
@@ -0,0 +1,46 @@
+// @flow
+import { RawSource } from 'webpack-sources'
+import {BUILD_MANIFEST} from '../../../lib/constants'
+
+// This plugin creates a build-manifest.json for all assets that are being output
+// It has a mapping of "entry" filename to real filename. Because the real filename can be hashed in production
+export default class BuildManifestPlugin {
+ apply (compiler: any) {
+ compiler.plugin('emit', (compilation, callback) => {
+ const {chunks} = compilation
+ const assetMap = {pages: {}, css: []}
+
+ for (const chunk of chunks) {
+ if (!chunk.name || !chunk.files) {
+ continue
+ }
+
+ const files = []
+
+ for (const file of chunk.files) {
+ if (/\.map$/.test(file)) {
+ continue
+ }
+
+ if (/\.hot-update\.js$/.test(file)) {
+ continue
+ }
+
+ if (/\.css$/.exec(file)) {
+ assetMap.css.push(file)
+ continue
+ }
+
+ files.push(file)
+ }
+
+ if (files.length > 0) {
+ assetMap[chunk.name] = files
+ }
+ }
+
+ compilation.assets[BUILD_MANIFEST] = new RawSource(JSON.stringify(assetMap))
+ callback()
+ })
+ }
+}
diff --git a/server/build/webpack.js b/server/build/webpack.js
index ebc466a2d7..344ee32c99 100644
--- a/server/build/webpack.js
+++ b/server/build/webpack.js
@@ -12,6 +12,7 @@ import NextJsSsrImportPlugin from './plugins/nextjs-ssr-import'
import DynamicChunksPlugin from './plugins/dynamic-chunks-plugin'
import UnlinkFilePlugin from './plugins/unlink-file-plugin'
import PagesManifestPlugin from './plugins/pages-manifest-plugin'
+import BuildManifestPlugin from './plugins/build-manifest-plugin'
const presetItem = createConfigItem(require('./babel/preset'), {type: 'preset'})
const hotLoaderItem = createConfigItem(require('react-hot-loader/babel'), {type: 'plugin'})
@@ -259,6 +260,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
}),
!dev && new webpack.optimize.ModuleConcatenationPlugin(),
isServer && new PagesManifestPlugin(),
+ !isServer && new BuildManifestPlugin(),
!isServer && new PagesPlugin(),
!isServer && new DynamicChunksPlugin(),
isServer && new NextJsSsrImportPlugin(),
@@ -266,7 +268,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
// In production we move common modules into the existing main.js bundle
!isServer && new webpack.optimize.CommonsChunkPlugin({
name: 'main.js',
- filename: 'main.js',
+ filename: dev ? 'static/commons/main.js' : 'static/commons/main-[chunkhash].js',
minChunks (module, count) {
// React and React DOM are used everywhere in Next.js. So they should always be common. Even in development mode, to speed up compilation.
if (module.resource && module.resource.includes(`${sep}react-dom${sep}`) && count >= 0) {
@@ -297,8 +299,8 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
}),
// We use a manifest file in development to speed up HMR
dev && !isServer && new webpack.optimize.CommonsChunkPlugin({
- name: 'manifest',
- filename: 'manifest.js'
+ name: 'manifest.js',
+ filename: dev ? 'static/commons/manifest.js' : 'static/commons/manifest-[chunkhash].js'
})
].filter(Boolean)
}
diff --git a/server/document.js b/server/document.js
index 3ab9eda757..48fcdbc5fb 100644
--- a/server/document.js
+++ b/server/document.js
@@ -10,9 +10,9 @@ const Fragment = React.Fragment || function Fragment ({ children }) {
export default class Document extends Component {
static getInitialProps ({ renderPage }) {
- const { html, head, errorHtml, chunks } = renderPage()
+ const { html, head, errorHtml, chunks, buildManifest } = renderPage()
const styles = flush()
- return { html, head, errorHtml, chunks, styles }
+ return { html, head, errorHtml, chunks, styles, buildManifest }
}
static childContextTypes = {
@@ -40,32 +40,33 @@ export class Head extends Component {
}
getChunkPreloadLink (filename) {
- const { __NEXT_DATA__ } = this.context._documentProps
+ const { __NEXT_DATA__, buildManifest } = this.context._documentProps
let { assetPrefix, buildId } = __NEXT_DATA__
- const hash = buildId
- return (
- {
+ return
- )
+ })
}
getPreloadMainLinks () {
const { dev } = this.context._documentProps
if (dev) {
return [
- this.getChunkPreloadLink('manifest.js'),
- this.getChunkPreloadLink('main.js')
+ ...this.getChunkPreloadLink('manifest.js'),
+ ...this.getChunkPreloadLink('main.js')
]
}
// In the production mode, we have a single asset with all the JS content.
return [
- this.getChunkPreloadLink('main.js')
+ ...this.getChunkPreloadLink('main.js')
]
}
@@ -125,31 +126,32 @@ export class NextScript extends Component {
}
getChunkScript (filename, additionalProps = {}) {
- const { __NEXT_DATA__ } = this.context._documentProps
+ const { __NEXT_DATA__, buildManifest } = this.context._documentProps
let { assetPrefix, buildId } = __NEXT_DATA__
- const hash = buildId
- return (
+ const files = buildManifest[filename]
+
+ return files.map((file) => (
- )
+ ))
}
getScripts () {
const { dev } = this.context._documentProps
if (dev) {
return [
- this.getChunkScript('manifest.js'),
- this.getChunkScript('main.js')
+ ...this.getChunkScript('manifest.js'),
+ ...this.getChunkScript('main.js')
]
}
// In the production mode, we have a single asset with all the JS content.
// So, we can load the script with async
- return [this.getChunkScript('main.js', { async: true })]
+ return [...this.getChunkScript('main.js', { async: true })]
}
getDynamicChunks () {
diff --git a/server/export.js b/server/export.js
index c2ed256468..89967165fa 100644
--- a/server/export.js
+++ b/server/export.js
@@ -19,10 +19,7 @@ export default async function (dir, options, configuration) {
log(`> using build directory: ${nextDir}`)
if (!existsSync(nextDir)) {
- console.error(
- `Build directory ${nextDir} does not exist. Make sure you run "next build" before running "next start" or "next export".`
- )
- process.exit(1)
+ throw new Error(`Build directory ${nextDir} does not exist. Make sure you run "next build" before running "next start" or "next export".`)
}
const buildId = readFileSync(join(nextDir, 'BUILD_ID'), 'utf8')
@@ -53,12 +50,6 @@ export default async function (dir, options, configuration) {
)
}
- // Copy main.js
- await cp(
- join(nextDir, 'main.js'),
- join(outDir, '_next', buildId, 'main.js')
- )
-
// Copy .next/static directory
if (existsSync(join(nextDir, 'static'))) {
log(' copying "static build" directory')
diff --git a/server/index.js b/server/index.js
index be7f4881a4..1d993c0084 100644
--- a/server/index.js
+++ b/server/index.js
@@ -162,57 +162,6 @@ export default class Server {
await this.serveStatic(req, res, p)
},
- '/_next/:buildId/manifest.js': async (req, res, params) => {
- if (!this.dev) return this.send404(res)
-
- this.handleBuildId(params.buildId, res)
- const p = join(this.dir, this.dist, 'manifest.js')
- await this.serveStatic(req, res, p)
- },
-
- '/_next/:buildId/manifest.js.map': async (req, res, params) => {
- if (!this.dev) return this.send404(res)
-
- this.handleBuildId(params.buildId, res)
- const p = join(this.dir, this.dist, 'manifest.js.map')
- await this.serveStatic(req, res, p)
- },
-
- '/_next/:buildId/main.js': async (req, res, params) => {
- if (this.dev) {
- this.handleBuildId(params.buildId, res)
- const p = join(this.dir, this.dist, 'main.js')
- await this.serveStatic(req, res, p)
- } else {
- const buildId = params.buildId
- if (!this.handleBuildId(buildId, res)) {
- const error = new Error('INVALID_BUILD_ID')
- const customFields = { buildIdMismatched: true }
-
- return await renderScriptError(req, res, '/_error', error, customFields, this.renderOpts)
- }
-
- const p = join(this.dir, this.dist, 'main.js')
- await this.serveStatic(req, res, p)
- }
- },
-
- '/_next/:buildId/main.js.map': async (req, res, params) => {
- if (this.dev) {
- this.handleBuildId(params.buildId, res)
- const p = join(this.dir, this.dist, 'main.js.map')
- await this.serveStatic(req, res, p)
- } else {
- const buildId = params.buildId
- if (!this.handleBuildId(buildId, res)) {
- return await this.render404(req, res)
- }
-
- const p = join(this.dir, this.dist, 'main.js.map')
- await this.serveStatic(req, res, p)
- }
- },
-
'/_next/:buildId/page/:path*.js.map': async (req, res, params) => {
const paths = params.path || ['']
const page = `/${paths.join('/')}`
@@ -279,6 +228,11 @@ export default class Server {
},
'/_next/static/:path*': async (req, res, params) => {
+ // The commons folder holds commonschunk files
+ // In development they don't have a hash, and shouldn't be cached by the browser.
+ if (this.dev && params.path[0] === 'commons') {
+ res.setHeader('Cache-Control', 'no-store, must-revalidate')
+ }
const p = join(this.dir, this.dist, 'static', ...(params.path || []))
await this.serveStatic(req, res, p)
},
diff --git a/server/render.js b/server/render.js
index 3f700657ea..aa98a5f858 100644
--- a/server/render.js
+++ b/server/render.js
@@ -12,6 +12,7 @@ import Head, { defaultHead } from '../lib/head'
import App from '../lib/app'
import ErrorDebug from '../lib/error-debug'
import { flushChunks } from '../lib/dynamic'
+import { BUILD_MANIFEST } from '../lib/constants'
const logger = console
@@ -54,6 +55,7 @@ async function doRender (req, res, pathname, query, {
}
const documentPath = join(dir, dist, 'dist', 'bundles', 'pages', '_document')
+ const buildManifest = require(join(dir, dist, BUILD_MANIFEST))
let [Component, Document] = await Promise.all([
requirePage(page, {dir, dist}),
@@ -94,7 +96,7 @@ async function doRender (req, res, pathname, query, {
}
const chunks = loadChunks({ dev, dir, dist, availableChunks })
- return { html, head, errorHtml, chunks }
+ return { html, head, errorHtml, chunks, buildManifest }
}
const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })
@@ -117,6 +119,7 @@ async function doRender (req, res, pathname, query, {
dev,
dir,
staticMarkup,
+ buildManifest,
...docProps
})
diff --git a/test/integration/dist-dir/test/index.test.js b/test/integration/dist-dir/test/index.test.js
index b0c9e7a2d6..2e7b99e1f4 100644
--- a/test/integration/dist-dir/test/index.test.js
+++ b/test/integration/dist-dir/test/index.test.js
@@ -39,11 +39,10 @@ describe('Production Usage', () => {
describe('File locations', () => {
it('should build the app within the given `dist` directory', () => {
- expect(existsSync(join(__dirname, '/../dist/main.js'))).toBeTruthy()
+ expect(existsSync(join(__dirname, '/../dist/BUILD_ID'))).toBeTruthy()
})
-
it('should not build the app within the default `.next` directory', () => {
- expect(existsSync(join(__dirname, '/../.next/main.js'))).toBeFalsy()
+ expect(existsSync(join(__dirname, '/../.next/BUILD_ID'))).toBeFalsy()
})
})
})
diff --git a/test/integration/static/.gitignore b/test/integration/static/.gitignore
new file mode 100644
index 0000000000..3ec8dc5141
--- /dev/null
+++ b/test/integration/static/.gitignore
@@ -0,0 +1 @@
+.next-dev
diff --git a/test/integration/static/next.config.js b/test/integration/static/next.config.js
index 96c6be7eaa..fc2c1d2b55 100644
--- a/test/integration/static/next.config.js
+++ b/test/integration/static/next.config.js
@@ -1,17 +1,22 @@
-module.exports = {
- exportPathMap: function () {
- return {
- '/': { page: '/' },
- '/about': { page: '/about' },
- '/asset': { page: '/asset' },
- '/button-link': { page: '/button-link' },
- '/get-initial-props-with-no-query': { page: '/get-initial-props-with-no-query' },
- '/counter': { page: '/counter' },
- '/dynamic-imports': { page: '/dynamic-imports' },
- '/dynamic': { page: '/dynamic', query: { text: 'cool dynamic text' } },
- '/dynamic/one': { page: '/dynamic', query: { text: 'next export is nice' } },
- '/dynamic/two': { page: '/dynamic', query: { text: 'zeit is awesome' } },
- '/file-name.md': { page: '/dynamic', query: { text: 'this file has an extension' } }
+const {PHASE_DEVELOPMENT_SERVER} = require('next/constants')
+
+module.exports = (phase) => {
+ return {
+ distDir: phase === PHASE_DEVELOPMENT_SERVER ? '.next-dev' : '.next',
+ exportPathMap: function () {
+ return {
+ '/': { page: '/' },
+ '/about': { page: '/about' },
+ '/asset': { page: '/asset' },
+ '/button-link': { page: '/button-link' },
+ '/get-initial-props-with-no-query': { page: '/get-initial-props-with-no-query' },
+ '/counter': { page: '/counter' },
+ '/dynamic-imports': { page: '/dynamic-imports' },
+ '/dynamic': { page: '/dynamic', query: { text: 'cool dynamic text' } },
+ '/dynamic/one': { page: '/dynamic', query: { text: 'next export is nice' } },
+ '/dynamic/two': { page: '/dynamic', query: { text: 'zeit is awesome' } },
+ '/file-name.md': { page: '/dynamic', query: { text: 'this file has an extension' } }
+ }
}
}
}
diff --git a/test/integration/static/test/index.test.js b/test/integration/static/test/index.test.js
index 5ea24f5eb4..3fea134be7 100644
--- a/test/integration/static/test/index.test.js
+++ b/test/integration/static/test/index.test.js
@@ -31,13 +31,13 @@ describe('Static Export', () => {
context.server = await startStaticServer(join(appDir, 'out'))
context.port = context.server.address().port
- devContext.appPort = await findPort()
- devContext.server = await launchApp(join(__dirname, '../'), devContext.appPort, true)
+ devContext.port = await findPort()
+ devContext.server = await launchApp(join(__dirname, '../'), devContext.port, true)
// pre-build all pages at the start
await Promise.all([
- renderViaHTTP(devContext.appPort, '/'),
- renderViaHTTP(devContext.appPort, '/dynamic/one')
+ renderViaHTTP(devContext.port, '/'),
+ renderViaHTTP(devContext.port, '/dynamic/one')
])
})
afterAll(() => {
@@ -47,5 +47,5 @@ describe('Static Export', () => {
ssr(context)
browser(context)
- dev(context)
+ dev(devContext)
})
diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js
index d916e95364..174b55b5e9 100644
--- a/test/lib/next-test-utils.js
+++ b/test/lib/next-test-utils.js
@@ -3,7 +3,7 @@ import qs from 'querystring'
import http from 'http'
import express from 'express'
import path from 'path'
-import portfinder from 'portfinder'
+import getPort from 'get-port'
import { spawn } from 'child_process'
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'
import fkill from 'fkill'
@@ -63,8 +63,7 @@ export function fetchViaHTTP (appPort, pathname, query) {
}
export function findPort () {
- portfinder.basePort = 20000 + Math.ceil(Math.random() * 10000)
- return portfinder.getPortPromise()
+ return getPort()
}
// Launch the app in dev mode.
diff --git a/test/lib/next-webdriver.js b/test/lib/next-webdriver.js
index 9ccbd6ad3d..55c2ccd2b1 100644
--- a/test/lib/next-webdriver.js
+++ b/test/lib/next-webdriver.js
@@ -1,6 +1,10 @@
import wd from 'wd'
export default async function (appPort, pathname) {
+ if (typeof appPort === 'undefined') {
+ throw new Error('appPort is undefined')
+ }
+
const url = `http://localhost:${appPort}${pathname}`
console.log(`> Start loading browser with url: ${url}`)
diff --git a/yarn.lock b/yarn.lock
index 08cebf89e2..3ad3f8a275 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -902,7 +902,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
-any-promise@^1.0.0, any-promise@^1.1.0:
+any-promise@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
@@ -1083,7 +1083,7 @@ async@2.0.1:
dependencies:
lodash "^4.8.0"
-async@^1.4.0, async@^1.5.2:
+async@^1.4.0:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@@ -3440,10 +3440,6 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
-glob-promise@3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-3.3.0.tgz#d1eb3625c4e6dcbb9b96eeae4425d5a3b135fed2"
-
glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
@@ -3675,11 +3671,7 @@ hoek@4.x.x:
version "4.2.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
-hoist-non-react-statics@2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
-
-hoist-non-react-statics@^2.5.0:
+hoist-non-react-statics@2.5.0, hoist-non-react-statics@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
@@ -5163,7 +5155,7 @@ mkdirp@0.5.0:
dependencies:
minimist "0.0.8"
-mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
@@ -5200,14 +5192,6 @@ mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
-mz@2.7.0:
- version "2.7.0"
- resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
- dependencies:
- any-promise "^1.0.0"
- object-assign "^4.0.1"
- thenify-all "^1.0.0"
-
nan@^2.3.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
@@ -5790,14 +5774,6 @@ pluralize@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
-portfinder@1.0.13:
- version "1.0.13"
- resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
- dependencies:
- async "^1.5.2"
- debug "^2.2.0"
- mkdirp "0.5.x"
-
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -7358,18 +7334,6 @@ text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
-thenify-all@^1.0.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
- dependencies:
- thenify ">= 3.1.0 < 4"
-
-"thenify@>= 3.1.0 < 4":
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
- dependencies:
- any-promise "^1.0.0"
-
throat@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"