diff --git a/client/index.js b/client/index.js
index f14cb395..7ffe6605 100644
--- a/client/index.js
+++ b/client/index.js
@@ -21,6 +21,7 @@ const {
__NEXT_DATA__: {
props,
err,
+ page,
pathname,
query,
buildId,
@@ -76,7 +77,7 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
ErrorComponent = await pageLoader.loadPage('/_error')
try {
- Component = await pageLoader.loadPage(pathname)
+ Component = await pageLoader.loadPage(page)
} catch (err) {
console.error(stripAnsi(`${err.message}\n${err.stack}`))
Component = ErrorComponent
diff --git a/errors/powered-by-header-option-removed.md b/errors/powered-by-header-option-removed.md
deleted file mode 100644
index 216fbda8..00000000
--- a/errors/powered-by-header-option-removed.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# The poweredByHeader has been removed
-
-#### Why This Error Occurred
-
-Starting at Next.js version 5.0.0 the `poweredByHeader` option has been removed.
-
-#### Possible Ways to Fix It
-
-If you still want to remove `x-powered-by` you can use one of the custom-server examples.
-
-And then manually remove the header using `res.removeHeader('x-powered-by')`
-
-### Useful Links
-
-- [Custom Server documentation + examples](https://github.com/zeit/next.js#custom-server-and-routing)
diff --git a/examples/active-class-name/components/Link.js b/examples/active-class-name/components/Link.js
index 9ac3eaa9..eadf822e 100644
--- a/examples/active-class-name/components/Link.js
+++ b/examples/active-class-name/components/Link.js
@@ -5,9 +5,9 @@ import React, { Children } from 'react'
const ActiveLink = ({ router, children, ...props }) => {
const child = Children.only(children)
- let className = child.props.className || ''
+ let className = child.props.className || null
if (router.pathname === props.href && props.activeClassName) {
- className = `${className} ${props.activeClassName}`.trim()
+ className = `${className !== null ? className : ''} ${props.activeClassName}`.trim()
}
delete props.activeClassName
diff --git a/examples/custom-charset/package.json b/examples/custom-charset/package.json
new file mode 100644
index 00000000..d9a8a33c
--- /dev/null
+++ b/examples/custom-charset/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "custom-server",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "node server.js",
+ "build": "next build",
+ "start": "NODE_ENV=production node server.js"
+ },
+ "dependencies": {
+ "next": "latest",
+ "react": "^16.0.0",
+ "react-dom": "^16.0.0"
+ }
+}
diff --git a/examples/custom-charset/pages/README.md b/examples/custom-charset/pages/README.md
new file mode 100644
index 00000000..cc4798c6
--- /dev/null
+++ b/examples/custom-charset/pages/README.md
@@ -0,0 +1,34 @@
+[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/custom-server)
+
+# Custom server example
+
+## How to use
+
+### Download manually
+
+Download the example [or clone the repo](https://github.com/zeit/next.js):
+
+```bash
+curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/custom-charset
+cd custom-charset
+```
+
+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
+
+The HTTP/1.1 specification says - if charset is not set in the http header then the browser defaults use ISO-8859-1.
+For languages like Polish, Albanian, Hungarian, Czech, Slovak, Slovene, there will be broken characters encoding from SSR.
+
+You can overwrite Content-Type in getInitialProps. But if you want to handle it as a server side concern, you can use this as an simple example.
diff --git a/examples/custom-charset/pages/index.js b/examples/custom-charset/pages/index.js
new file mode 100644
index 00000000..0e0c34eb
--- /dev/null
+++ b/examples/custom-charset/pages/index.js
@@ -0,0 +1,3 @@
+import React from 'react'
+
+export default () =>
áéíóöúü
diff --git a/examples/custom-charset/server.js b/examples/custom-charset/server.js
new file mode 100644
index 00000000..bf8b0a43
--- /dev/null
+++ b/examples/custom-charset/server.js
@@ -0,0 +1,21 @@
+const { createServer } = require('http')
+const { parse } = require('url')
+const next = require('next')
+
+const port = parseInt(process.env.PORT, 10) || 3000
+const dev = process.env.NODE_ENV !== 'production'
+const app = next({ dev })
+const handle = app.getRequestHandler()
+
+app.prepare()
+.then(() => {
+ createServer((req, res) => {
+ const parsedUrl = parse(req.url, true)
+ res.setHeader('Content-Type', 'text/html; charset=iso-8859-2')
+ handle(req, res, parsedUrl)
+ })
+ .listen(port, (err) => {
+ if (err) throw err
+ console.log(`> Ready on http://localhost:${port}`)
+ })
+})
diff --git a/examples/ssr-caching/server.js b/examples/ssr-caching/server.js
index 11a1d98e..8f0ef4d8 100644
--- a/examples/ssr-caching/server.js
+++ b/examples/ssr-caching/server.js
@@ -45,26 +45,32 @@ function getCacheKey (req) {
return `${req.url}`
}
-function renderAndCache (req, res, pagePath, queryParams) {
+async function renderAndCache (req, res, pagePath, queryParams) {
const key = getCacheKey(req)
// If we have a page in the cache, let's serve it
if (ssrCache.has(key)) {
- console.log(`CACHE HIT: ${key}`)
+ res.setHeader('x-cache', 'HIT')
res.send(ssrCache.get(key))
return
}
- // If not let's render the page into HTML
- app.renderToHTML(req, res, pagePath, queryParams)
- .then((html) => {
- // Let's cache this page
- console.log(`CACHE MISS: ${key}`)
- ssrCache.set(key, html)
+ try {
+ // If not let's render the page into HTML
+ const html = await app.renderToHTML(req, res, pagePath, queryParams)
+ // Something is wrong with the request, let's skip the cache
+ if (res.statusCode !== 200) {
res.send(html)
- })
- .catch((err) => {
- app.renderError(err, req, res, pagePath, queryParams)
- })
+ return
+ }
+
+ // Let's cache this page
+ ssrCache.set(key, html)
+
+ res.setHeader('x-cache', 'MISS')
+ res.send(html)
+ } catch (err) {
+ app.renderError(err, req, res, pagePath, queryParams)
+ }
}
diff --git a/examples/svg-components/package.json b/examples/svg-components/package.json
index 65f3541d..46e131f1 100644
--- a/examples/svg-components/package.json
+++ b/examples/svg-components/package.json
@@ -8,7 +8,7 @@
},
"dependencies": {
"next": "latest",
- "react": "latest,
+ "react": "latest",
"react-dom": "latest"
},
"devDependencies": {
diff --git a/examples/with-apollo-auth/pages/index.js b/examples/with-apollo-auth/pages/index.js
index 8250bac4..eb1fd6f3 100644
--- a/examples/with-apollo-auth/pages/index.js
+++ b/examples/with-apollo-auth/pages/index.js
@@ -25,7 +25,7 @@ class Index extends React.Component {
// Force a reload of all the current queries now that the user is
// logged in, so we don't accidentally leave any state around.
- this.props.client.resetStore().then(() => {
+ this.props.client.cache.reset().then(() => {
// Redirect to a more useful page when signed out
redirect({}, '/signin')
})
diff --git a/examples/with-apollo-auth/test/shared-apollo.js b/examples/with-apollo-auth/test/shared-apollo.js
index 8b3684f4..adf1a2a8 100644
--- a/examples/with-apollo-auth/test/shared-apollo.js
+++ b/examples/with-apollo-auth/test/shared-apollo.js
@@ -18,7 +18,7 @@ const test = require('ava')
* Apollo that we can clear, etc
*/
-const apolloFilePath = require.resolve('../lib/init-apollo')
+const apolloFilePath = require.resolve('../lib/initApollo')
test.beforeEach(() => {
// Clean up the cache
diff --git a/examples/with-apollo/README.md b/examples/with-apollo/README.md
index fd83de0e..bbf1f857 100644
--- a/examples/with-apollo/README.md
+++ b/examples/with-apollo/README.md
@@ -47,5 +47,3 @@ In this simple example, we integrate Apollo seamlessly with Next by wrapping our
On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](http://dev.apollodata.com/react/server-side-rendering.html#getDataFromTree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized.
This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend.
-
-*Note: Apollo uses Redux internally; if you're interested in integrating the client with your existing Redux store check out the [`with-apollo-and-redux`](https://github.com/zeit/next.js/tree/master/examples/with-apollo-and-redux) example.*
diff --git a/examples/with-draft-js/README.md b/examples/with-draft-js/README.md
new file mode 100644
index 00000000..5a9c90ac
--- /dev/null
+++ b/examples/with-draft-js/README.md
@@ -0,0 +1,41 @@
+[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-draft-js)
+# DraftJS Medium editor inspiration
+
+## How to use
+
+### Using `create-next-app`
+
+Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
+
+```
+npm i -g create-next-app
+create-next-app --example with-draft-js
+```
+
+### Download manually
+
+Download the example [or clone the repo](https://github.com/zeit/next.js):
+
+```bash
+curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-draft-js
+cd with-draft-js
+```
+
+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
+
+Have you ever wanted to have an editor like medium.com in your Next.js app? DraftJS is avalaible for SSR, but some plugins like the toolbar are using `window`, which does not work when doing SSR.
+
+This example aims to provides a fully customizable example of the famous medium editor with DraftJS. The goal was to get it as customizable as possible, and fully working with Next.js without using the react-no-ssr package.
diff --git a/examples/with-draft-js/package.json b/examples/with-draft-js/package.json
new file mode 100644
index 00000000..fdb9eccd
--- /dev/null
+++ b/examples/with-draft-js/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "with-draft-js",
+ "version": "1.0.0",
+ "author": "Asten Mies",
+ "license": "ISC",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "draft-js": "0.10.5",
+ "next": "5.0.0",
+ "react": "16.2.0",
+ "react-dom": "16.2.0"
+ }
+}
diff --git a/examples/with-draft-js/pages/index.js b/examples/with-draft-js/pages/index.js
new file mode 100644
index 00000000..286b3305
--- /dev/null
+++ b/examples/with-draft-js/pages/index.js
@@ -0,0 +1,270 @@
+import React from 'react'
+import {
+ Editor,
+ EditorState,
+ RichUtils,
+ convertToRaw,
+ convertFromRaw
+ } from 'draft-js'
+
+export default class App extends React.Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ editorState: EditorState.createWithContent(convertFromRaw(initialData)),
+ showToolbar: false,
+ windowWidth: 0,
+ toolbarMeasures: {
+ w: 0,
+ h: 0
+ },
+ selectionMeasures: {
+ w: 0,
+ h: 0
+ },
+ selectionCoordinates: {
+ x: 0,
+ y: 0
+ },
+ toolbarCoordinates: {
+ x: 0,
+ y: 0
+ },
+ showRawData: false
+ }
+
+ this.focus = () => this.editor.focus()
+ this.onChange = (editorState) => this.setState({editorState})
+ }
+
+ onClickEditor = () => {
+ this.focus()
+ this.checkSelectedText()
+ }
+
+ // 1- Check if some text is selected
+ checkSelectedText = () => {
+ if (typeof window !== 'undefined') {
+ const text = window.getSelection().toString()
+ if (text !== '') {
+ // 1-a Define the selection coordinates
+ this.setSelectionXY()
+ } else {
+ // Hide the toolbar if nothing is selected
+ this.setState({
+ showToolbar: false
+ })
+ }
+ }
+ }
+
+ // 2- Identify the selection coordinates
+ setSelectionXY = () => {
+ var r = window.getSelection().getRangeAt(0).getBoundingClientRect()
+ var relative = document.body.parentNode.getBoundingClientRect()
+ // 2-a Set the selection coordinates in the state
+ this.setState({
+ selectionCoordinates: r,
+ windowWidth: relative.width,
+ selectionMeasures: {
+ w: r.width,
+ h: r.height
+ }
+ }, () => this.showToolbar())
+ }
+
+ // 3- Show the toolbar
+ showToolbar = () => {
+ this.setState({
+ showToolbar: true
+ }, () => this.measureToolbar())
+ }
+
+ // 4- The toolbar was hidden until now
+ measureToolbar = () => {
+ // 4-a Define the toolbar width and height, as it is now visible
+ this.setState({
+ toolbarMeasures: {
+ w: this.elemWidth,
+ h: this.elemHeight
+ }
+ }, () => this.setToolbarXY())
+ }
+
+ // 5- Now that we have all measures, define toolbar coordinates
+ setToolbarXY = () => {
+ let coordinates = {}
+
+ const { selectionMeasures, selectionCoordinates, toolbarMeasures, windowWidth } = this.state
+
+ const hiddenTop = selectionCoordinates.y < toolbarMeasures.h
+ const hiddenRight = windowWidth - selectionCoordinates.x < toolbarMeasures.w / 2
+ const hiddenLeft = selectionCoordinates.x < toolbarMeasures.w / 2
+
+ const normalX = selectionCoordinates.x - (toolbarMeasures.w / 2) + (selectionMeasures.w / 2)
+ const normalY = selectionCoordinates.y - toolbarMeasures.h
+
+ const invertedY = selectionCoordinates.y + selectionMeasures.h
+ const moveXToLeft = windowWidth - toolbarMeasures.w
+ const moveXToRight = 0
+
+ coordinates = {
+ x: normalX,
+ y: normalY
+ }
+
+ if (hiddenTop) {
+ coordinates.y = invertedY
+ }
+
+ if (hiddenRight) {
+ coordinates.x = moveXToLeft
+ }
+
+ if (hiddenLeft) {
+ coordinates.x = moveXToRight
+ }
+
+ this.setState({
+ toolbarCoordinates: coordinates
+ })
+ }
+
+ handleKeyCommand = (command) => {
+ const {editorState} = this.state
+ const newState = RichUtils.handleKeyCommand(editorState, command)
+ if (newState) {
+ this.onChange(newState)
+ return true
+ }
+ return false
+ }
+
+ toggleToolbar = (inlineStyle) => {
+ this.onChange(
+ RichUtils.toggleInlineStyle(
+ this.state.editorState,
+ inlineStyle
+ )
+ )
+ }
+
+ render () {
+ const {editorState} = this.state
+ // Make sure we're not on the ssr
+ if (typeof window !== 'undefined') {
+ // Let's stick the toolbar to the selection
+ // when the window is resized
+ window.addEventListener('resize', this.checkSelectedText)
+ }
+
+ const toolbarStyle = {
+ display: this.state.showToolbar ? 'block' : 'none',
+ backgroundColor: 'black',
+ color: 'white',
+ position: 'absolute',
+ left: this.state.toolbarCoordinates.x,
+ top: this.state.toolbarCoordinates.y,
+ zIndex: 999,
+ padding: 10
+ }
+ return (
+
+ )
+}
+
+const initialData = {'blocks': [{'key': '16d0k', 'text': 'You can edit this text.', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [{'offset': 0, 'length': 23, 'style': 'BOLD'}], 'entityRanges': [], 'data': {}}, {'key': '98peq', 'text': '', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [], 'data': {}}, {'key': 'ecmnc', 'text': 'Luke Skywalker has vanished. In his absence, the sinister FIRST ORDER has risen from the ashes of the Empire and will not rest until Skywalker, the last Jedi, has been destroyed.', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [{'offset': 0, 'length': 14, 'style': 'BOLD'}, {'offset': 133, 'length': 9, 'style': 'BOLD'}], 'entityRanges': [], 'data': {}}, {'key': 'fe2gn', 'text': '', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [], 'data': {}}, {'key': '4481k', 'text': 'With the support of the REPUBLIC, General Leia Organa leads a brave RESISTANCE. She is desperate to find her brother Luke and gain his help in restoring peace and justice to the galaxy.', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [{'offset': 34, 'length': 19, 'style': 'BOLD'}, {'offset': 117, 'length': 4, 'style': 'BOLD'}, {'offset': 68, 'length': 10, 'style': 'ANYCUSTOMSTYLE'}], 'entityRanges': [], 'data': {}}], 'entityMap': {}}
diff --git a/examples/with-firebase-authentication/README.md b/examples/with-firebase-authentication/README.md
index 4d275999..84f70244 100644
--- a/examples/with-firebase-authentication/README.md
+++ b/examples/with-firebase-authentication/README.md
@@ -23,10 +23,11 @@ cd with-firebase-authentication
```
Set up firebase:
-- create a project
-- get your service account credentials and client credentials and set both in firebaseCredentials.js
-- set your firebase database url in server.js
-- on the firebase Authentication console, select Google as your provider
+- Create a project at the [Firebase console](https://console.firebase.google.com/).
+- Get your account credentials from the Firebase console at *settings>service accounts*, where you can click on *generate new private key* and download the credentials as a json file. It will contain keys such as `project_id`, `client_email` and `client id`. Now copy them into your project in the `credentials/server.js` file.
+- Get your authentication credentials from the Firebase console under *authentication>users>web setup*. It will include keys like `apiKey`, `authDomain` and `databaseUrl` and it goes into your project in `credentials/client.js`.
+- Copy the `databaseUrl` key you got in the last step into `server.js` in the corresponding line.
+- Back at the Firebase web console, go to *authentication>signup method* and select *Google*.
Install it and run:
diff --git a/examples/with-firebase-authentication/package.json b/examples/with-firebase-authentication/package.json
index e2e632a3..28c3fcd8 100644
--- a/examples/with-firebase-authentication/package.json
+++ b/examples/with-firebase-authentication/package.json
@@ -10,8 +10,8 @@
"body-parser": "^1.17.1",
"express": "^4.14.0",
"express-session": "^1.15.2",
- "firebase": "^3.7.5",
- "firebase-admin": "^4.2.0",
+ "firebase": "^4.9.1",
+ "firebase-admin": "^5.8.2",
"isomorphic-unfetch": "2.0.0",
"next": "latest",
"react": "^16.0.0",
diff --git a/examples/with-glamor/pages/_document.js b/examples/with-glamor/pages/_document.js
index c6572651..525cd21e 100644
--- a/examples/with-glamor/pages/_document.js
+++ b/examples/with-glamor/pages/_document.js
@@ -4,7 +4,7 @@ import { renderStatic } from 'glamor/server'
export default class MyDocument extends Document {
static async getInitialProps ({ renderPage }) {
const page = renderPage()
- const styles = renderStatic(() => page.html)
+ const styles = renderStatic(() => page.html || page.errorHtml)
return { ...page, ...styles }
}
diff --git a/examples/with-glamorous/pages/_document.js b/examples/with-glamorous/pages/_document.js
index 1d7d07af..63644fd9 100644
--- a/examples/with-glamorous/pages/_document.js
+++ b/examples/with-glamorous/pages/_document.js
@@ -4,7 +4,7 @@ import { renderStatic } from 'glamor/server'
export default class MyDocument extends Document {
static async getInitialProps ({ renderPage }) {
const page = renderPage()
- const styles = renderStatic(() => page.html)
+ const styles = renderStatic(() => page.html || page.errorHtml)
return { ...page, ...styles }
}
diff --git a/examples/with-global-stylesheet/package.json b/examples/with-global-stylesheet/package.json
index 72738a39..4166dea9 100644
--- a/examples/with-global-stylesheet/package.json
+++ b/examples/with-global-stylesheet/package.json
@@ -22,8 +22,5 @@
"react": "^16.0.0",
"react-dom": "^16.0.0",
"sass-loader": "^6.0.6"
- },
- "devDependencies": {
- "now": "^8.3.10"
}
}
diff --git a/examples/with-mobx-state-tree/store.js b/examples/with-mobx-state-tree/store.js
index 773beb69..798e5c89 100644
--- a/examples/with-mobx-state-tree/store.js
+++ b/examples/with-mobx-state-tree/store.js
@@ -14,7 +14,7 @@ const Store = types
// mobx-state-tree doesn't allow anonymous callbacks changing data
// pass off to another action instead
self.update()
- })
+ }, 1000)
}
function update () {
diff --git a/examples/with-mobx/store.js b/examples/with-mobx/store.js
index ba88dce5..34d9abdc 100644
--- a/examples/with-mobx/store.js
+++ b/examples/with-mobx/store.js
@@ -14,7 +14,7 @@ class Store {
this.timer = setInterval(() => {
this.lastUpdate = Date.now()
this.light = true
- })
+ }, 1000)
}
stop = () => clearInterval(this.timer)
diff --git a/examples/with-next-css/README.md b/examples/with-next-css/README.md
new file mode 100644
index 00000000..0451a991
--- /dev/null
+++ b/examples/with-next-css/README.md
@@ -0,0 +1,40 @@
+[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-next-css)
+
+# next-css example
+
+## How to use
+
+### Using `create-next-app`
+
+Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
+
+```
+npm i -g create-next-app
+create-next-app --example with-next-css with-next-css-app
+```
+
+### Download manually
+
+Download the example [or clone the repo](https://github.com/zeit/next.js):
+
+```bash
+curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-next-css
+cd with-next-css
+```
+
+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 demonstrates how to use the [next-css plugin](https://github.com/zeit/next-plugins/tree/master/packages/next-css) It includes patterns for with and without CSS Modules, with PostCSS and with additional webpack configurations on top of the next-css plugin.
\ No newline at end of file
diff --git a/examples/with-next-css/next.config.js b/examples/with-next-css/next.config.js
new file mode 100644
index 00000000..b6316525
--- /dev/null
+++ b/examples/with-next-css/next.config.js
@@ -0,0 +1,16 @@
+const withCSS = require('@zeit/next-css')
+/* Without CSS Modules, with PostCSS */
+module.exports = withCSS()
+
+/* With CSS Modules */
+// module.exports = withCSS({ cssModules: true })
+
+/* With additional configuration on top of CSS Modules */
+/*
+module.exports = withCSS({
+ cssModules: true,
+ webpack: function (config) {
+ return config;
+ }
+});
+*/
diff --git a/examples/with-next-css/package.json b/examples/with-next-css/package.json
new file mode 100644
index 00000000..65d5f264
--- /dev/null
+++ b/examples/with-next-css/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "with-css-modules",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@zeit/next-css": "0.0.7",
+ "next": "5.0.0",
+ "react": "16.2.0",
+ "react-dom": "16.2.0"
+ }
+}
diff --git a/examples/with-next-css/pages/_document.js b/examples/with-next-css/pages/_document.js
new file mode 100644
index 00000000..512ea5e4
--- /dev/null
+++ b/examples/with-next-css/pages/_document.js
@@ -0,0 +1,23 @@
+/*
+In production the stylesheet is compiled to .next/static/style.css and served from /_next/static/style.css
+
+You have to include it into the page using either next/head or a custom _document.js, as is being done in this file.
+*/
+
+import Document, { Head, Main, NextScript } from 'next/document'
+
+export default class MyDocument extends Document {
+ render () {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/examples/with-next-css/pages/index.js b/examples/with-next-css/pages/index.js
new file mode 100644
index 00000000..1c8ec8c0
--- /dev/null
+++ b/examples/with-next-css/pages/index.js
@@ -0,0 +1,13 @@
+
+/* Without CSS Modules, maybe with PostCSS */
+
+import '../style.css'
+
+export default () =>
O Hai world!
+
+/* With CSS Modules */
+/*
+import css from "../style.css"
+
+export default () =>
Hello World, I am being styled using CSS Modules!
+*/
diff --git a/examples/with-next-css/style.css b/examples/with-next-css/style.css
new file mode 100644
index 00000000..ace395e6
--- /dev/null
+++ b/examples/with-next-css/style.css
@@ -0,0 +1,16 @@
+.example {
+ font-size: 50px;
+ color: papayawhip;
+}
+
+/* Post-CSS */
+/*
+:root {
+ --some-color: red;
+}
+
+.example {
+ color: var(--some-color);
+}
+
+*/
diff --git a/examples/with-next-sass/next.config.js b/examples/with-next-sass/next.config.js
new file mode 100644
index 00000000..ed73b137
--- /dev/null
+++ b/examples/with-next-sass/next.config.js
@@ -0,0 +1,2 @@
+const withSass = require('@zeit/next-sass')
+module.exports = withSass()
diff --git a/examples/with-next-sass/package.json b/examples/with-next-sass/package.json
new file mode 100644
index 00000000..c6c7a56f
--- /dev/null
+++ b/examples/with-next-sass/package.json
@@ -0,0 +1,14 @@
+{
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@zeit/next-sass": "0.0.9",
+ "next": "^5.0.0",
+ "node-sass": "^4.7.2",
+ "react": "^16.2.0",
+ "react-dom": "^16.2.0"
+ }
+}
diff --git a/examples/with-next-sass/pages/_document.js b/examples/with-next-sass/pages/_document.js
new file mode 100644
index 00000000..af0f233e
--- /dev/null
+++ b/examples/with-next-sass/pages/_document.js
@@ -0,0 +1,26 @@
+/*
+In production the stylesheet is compiled to .next/static/style.css.
+The file will be served from /_next/static/style.css
+You could include it into the page using either next/head or a custom _document.js.
+*/
+
+import Document, { Head, Main, NextScript } from 'next/document'
+
+export default class MyDocument extends Document {
+ render () {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/examples/with-next-sass/pages/index.js b/examples/with-next-sass/pages/index.js
new file mode 100644
index 00000000..64f64c0d
--- /dev/null
+++ b/examples/with-next-sass/pages/index.js
@@ -0,0 +1,6 @@
+import '../styles/style.scss'
+
+export default () =>
+
+ Hello World!
+
diff --git a/examples/with-next-sass/readme.md b/examples/with-next-sass/readme.md
new file mode 100644
index 00000000..b17cfe3f
--- /dev/null
+++ b/examples/with-next-sass/readme.md
@@ -0,0 +1,43 @@
+[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-jest)
+
+# Example app with next-sass
+
+## How to use
+
+### Using `create-next-app`
+
+Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
+
+```bash
+npm i -g create-next-app
+create-next-app --example with-next-sass with-next-sass-app
+```
+
+### Download manually
+
+Download the example [or clone the repo](https://github.com/zeit/next.js):
+
+```bash
+curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-next-sass
+cd with-next-sass
+```
+
+Install it and run:
+
+```bash
+npm install
+npm run build
+npm run start
+```
+
+The dev mode is also support via `npm run dev`
+
+## The idea behind the example
+
+This example features:
+
+* An app with next-sass
+
+This example uses next-sass without css-modules. The config can be found in `next.config.js`, change `withSass()` to `withSass({cssModules: true})` if you use css-modules. Then in the code, you import the stylesheet as `import style '../styles/style.scss'` and use it like `
`.
+
+[Learn more](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)
diff --git a/examples/with-next-sass/styles/style.scss b/examples/with-next-sass/styles/style.scss
new file mode 100644
index 00000000..73cc2e0e
--- /dev/null
+++ b/examples/with-next-sass/styles/style.scss
@@ -0,0 +1,4 @@
+$color: #2ecc71;
+.example {
+ background-color: $color;
+}
diff --git a/examples/with-polyfills/next.config.js b/examples/with-polyfills/next.config.js
index eca79708..67527712 100644
--- a/examples/with-polyfills/next.config.js
+++ b/examples/with-polyfills/next.config.js
@@ -3,7 +3,11 @@ module.exports = {
const originalEntry = cfg.entry
cfg.entry = async () => {
const entries = await originalEntry()
- entries['main.js'].unshift('./client/polyfills.js')
+
+ if (entries['main.js']) {
+ entries['main.js'].unshift('./client/polyfills.js')
+ }
+
return entries
}
diff --git a/examples/with-react-i18next/package.json b/examples/with-react-i18next/package.json
index a427dfcd..f69b9713 100644
--- a/examples/with-react-i18next/package.json
+++ b/examples/with-react-i18next/package.json
@@ -11,15 +11,15 @@
"author": "",
"license": "MIT",
"dependencies": {
- "express": "4.15.3",
- "i18next": "8.4.2",
- "i18next-browser-languagedetector": "2.0.0",
- "i18next-express-middleware": "1.0.5",
+ "express": "4.16.2",
+ "i18next": "10.4.1",
+ "i18next-browser-languagedetector": "2.1.0",
+ "i18next-express-middleware": "1.0.10",
"i18next-node-fs-backend": "1.0.0",
- "i18next-xhr-backend": "1.4.2",
- "next": "latest",
+ "i18next-xhr-backend": "1.5.1",
+ "next": "5.0.0",
"react": "16.2.0",
"react-dom": "16.2.0",
- "react-i18next": "4.6.3"
+ "react-i18next": "7.3.6"
}
}
diff --git a/examples/with-react-i18next/server.js b/examples/with-react-i18next/server.js
index d3b62e80..5f2686e8 100644
--- a/examples/with-react-i18next/server.js
+++ b/examples/with-react-i18next/server.js
@@ -2,7 +2,6 @@ const express = require('express')
const path = require('path')
const next = require('next')
-const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
@@ -17,6 +16,7 @@ i18n
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init({
+ fallbackLng: 'en',
preload: ['en', 'de'], // preload all langages
ns: ['common', 'home', 'page2'], // need to preload all the namespaces
backend: {
@@ -41,9 +41,9 @@ i18n
// use next.js
server.get('*', (req, res) => handle(req, res))
- server.listen(port, (err) => {
+ server.listen(3000, (err) => {
if (err) throw err
- console.log(`> Ready on http://localhost:${port}`)
+ console.log('> Ready on http://localhost:3000')
})
})
})
diff --git a/examples/with-react-native-web/.babelrc b/examples/with-react-native-web/.babelrc
new file mode 100644
index 00000000..e39892d3
--- /dev/null
+++ b/examples/with-react-native-web/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["next/babel"],
+ "plugins": ["react-native-web"]
+}
diff --git a/examples/with-react-native-web/README.md b/examples/with-react-native-web/README.md
new file mode 100644
index 00000000..1e4f14f3
--- /dev/null
+++ b/examples/with-react-native-web/README.md
@@ -0,0 +1,42 @@
+[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-react-native-web)
+
+## How to use
+
+### Using `create-next-app`
+
+Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
+
+```
+npm i -g create-next-app
+create-next-app --example with-react-native-web
+```
+
+### Download manually
+
+Download the example [or clone the repo](https://github.com/zeit/next.js):
+
+```bash
+curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-react-native-web
+cd with-react-native-web
+```
+
+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 how to use [react-native-web](https://github.com/necolas/react-native-web) to bring the platform-agnostic Components and APIs of React Native to the web.
+
+> **High-quality user interfaces**: React Native for Web makes it easy to create fast, adaptive web UIs in JavaScript. It provides native-like interactions, support for multiple input modes (touch, mouse, keyboard), optimized vendor-prefixed styles, built-in support for RTL layout, built-in accessibility, and integrates with React Dev Tools.
+>
+> **Write once, render anywhere**: React Native for Web interoperates with existing React DOM components and is compatible with the majority of the React Native API. You can develop new components for native and web without rewriting existing code. React Native for Web can also render to HTML and critical CSS on the server using Node.js.
diff --git a/examples/with-react-native-web/package.json b/examples/with-react-native-web/package.json
new file mode 100644
index 00000000..bb9d6f82
--- /dev/null
+++ b/examples/with-react-native-web/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "with-react-native-web",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "next": "latest",
+ "react": "^16.0.0",
+ "react-dom": "^16.0.0",
+ "react-native-web": "^0.4.0"
+ },
+ "devDependencies": {
+ "babel-plugin-react-native-web": "^0.4.0"
+ }
+}
diff --git a/examples/with-react-native-web/pages/_document.js b/examples/with-react-native-web/pages/_document.js
new file mode 100644
index 00000000..0d99939f
--- /dev/null
+++ b/examples/with-react-native-web/pages/_document.js
@@ -0,0 +1,45 @@
+import Document, { Head, Main, NextScript } from 'next/document'
+import React from 'react'
+import { AppRegistry } from 'react-native-web'
+
+let index = 0
+
+// Force Next-generated DOM elements to fill their parent's height.
+// Not required for using of react-native-web, but helps normalize
+// layout for top-level wrapping elements.
+const normalizeNextElements = `
+ body > div:first-child,
+ #__next {
+ height: 100%;
+ }
+`
+
+export default class MyDocument extends Document {
+ static async getInitialProps ({ renderPage }) {
+ AppRegistry.registerComponent('Main', () => Main)
+ const { getStyleElement } = AppRegistry.getApplication('Main')
+ const page = renderPage()
+ const styles = [
+ ,
+ getStyleElement()
+ ]
+ return { ...page, styles }
+ }
+
+ render () {
+ return (
+
+
+ react-native-web
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/examples/with-react-native-web/pages/index.js b/examples/with-react-native-web/pages/index.js
new file mode 100644
index 00000000..fcc6d041
--- /dev/null
+++ b/examples/with-react-native-web/pages/index.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import { StyleSheet, Text, View } from 'react-native'
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ height: '100%',
+ justifyContent: 'center'
+ },
+ text: {
+ alignItems: 'center',
+ fontSize: 24
+ }
+})
+
+export default props => (
+
+ Welcome to Next.js!
+
+)
diff --git a/examples/with-reasonml/README.md b/examples/with-reasonml/README.md
index 65ff6a02..60953681 100644
--- a/examples/with-reasonml/README.md
+++ b/examples/with-reasonml/README.md
@@ -40,7 +40,7 @@ Run BuckleScript build system `bsb -w` and `next -w` separately. For the sake
of simple convention, `npm run dev` run both `bsb` and `next` concurrently.
However, this doesn't offer the full [colorful and very, very, veeeery nice
error
-output](https://reasonml.github.io/community/blog/#way-way-waaaay-nicer-error-messages)
+output](https://reasonml.github.io/blog/2017/08/25/way-nicer-error-messages.html)
experience that ReasonML can offer, don't miss it!
## The idea behind the example
diff --git a/examples/with-redux-observable/redux/epics.js b/examples/with-redux-observable/redux/epics.js
index 3dd962af..f2444321 100644
--- a/examples/with-redux-observable/redux/epics.js
+++ b/examples/with-redux-observable/redux/epics.js
@@ -1,6 +1,6 @@
import { interval } from 'rxjs/observable/interval'
import { of } from 'rxjs/observable/of'
-import { takeUntil, mergeMap, catchError } from 'rxjs/operators'
+import { takeUntil, mergeMap, catchError, map } from 'rxjs/operators'
import { combineEpics, ofType } from 'redux-observable'
import ajax from 'universal-rx-request' // because standard AjaxObservable only works in browser
@@ -13,11 +13,9 @@ export const fetchUserEpic = (action$, store) =>
mergeMap(action => {
return interval(3000).pipe(
mergeMap(x =>
- of(
- actions.fetchCharacter({
- isServer: store.getState().isServer
- })
- )
+ actions.fetchCharacter({
+ isServer: store.getState().isServer
+ })
),
takeUntil(action$.ofType(types.STOP_FETCHING_CHARACTERS))
)
@@ -31,12 +29,10 @@ export const fetchCharacterEpic = (action$, store) =>
ajax({
url: `https://swapi.co/api/people/${store.getState().nextCharacterId}`
}).pipe(
- mergeMap(response =>
- of(
- actions.fetchCharacterSuccess(
- response.body,
- store.getState().isServer
- )
+ map(response =>
+ actions.fetchCharacterSuccess(
+ response.body,
+ store.getState().isServer
)
),
catchError(error =>
diff --git a/examples/with-redux-saga/README.md b/examples/with-redux-saga/README.md
index 033d25e8..bde6b9e0 100644
--- a/examples/with-redux-saga/README.md
+++ b/examples/with-redux-saga/README.md
@@ -61,7 +61,7 @@ The second example, under `components/add-count.js`, shows a simple add counter
## What changed with next-redux-saga
-The digital clock is updated every 800ms using the `runClockSaga` found in `saga.js`.
+The digital clock is updated every second using the `runClockSaga` found in `saga.js`.
All pages are also being wrapped by `next-redux-saga` using a helper function from `store.js`:
diff --git a/examples/with-redux-saga/pages/index.js b/examples/with-redux-saga/pages/index.js
index fd14ef55..c9ac10cf 100644
--- a/examples/with-redux-saga/pages/index.js
+++ b/examples/with-redux-saga/pages/index.js
@@ -1,11 +1,12 @@
import React from 'react'
-import {increment, loadData, startClock} from '../actions'
+import {increment, loadData, startClock, tickClock} from '../actions'
import {withReduxSaga} from '../store'
import Page from '../components/page'
class Counter extends React.Component {
- static async getInitialProps ({store}) {
+ static async getInitialProps ({store, isServer}) {
+ store.dispatch(tickClock(isServer))
store.dispatch(increment())
if (!store.getState().placeholderData) {
store.dispatch(loadData())
diff --git a/examples/with-redux-saga/pages/other.js b/examples/with-redux-saga/pages/other.js
index 8eace75a..13484093 100644
--- a/examples/with-redux-saga/pages/other.js
+++ b/examples/with-redux-saga/pages/other.js
@@ -1,11 +1,12 @@
import React from 'react'
-import {increment, startClock} from '../actions'
+import {increment, startClock, tickClock} from '../actions'
import {withReduxSaga} from '../store'
import Page from '../components/page'
class Counter extends React.Component {
- static async getInitialProps ({store}) {
+ static async getInitialProps ({store, isServer}) {
+ store.dispatch(tickClock(isServer))
store.dispatch(increment())
}
diff --git a/examples/with-redux-saga/saga.js b/examples/with-redux-saga/saga.js
index b917c3af..e6d471d6 100644
--- a/examples/with-redux-saga/saga.js
+++ b/examples/with-redux-saga/saga.js
@@ -13,7 +13,7 @@ function * runClockSaga () {
yield take(actionTypes.START_CLOCK)
while (true) {
yield put(tickClock(false))
- yield call(delay, 800)
+ yield call(delay, 1000)
}
}
diff --git a/examples/with-redux/store.js b/examples/with-redux/store.js
index 8291b0f4..7ad2c03f 100644
--- a/examples/with-redux/store.js
+++ b/examples/with-redux/store.js
@@ -32,7 +32,7 @@ export const serverRenderClock = (isServer) => dispatch => {
}
export const startClock = () => dispatch => {
- return setInterval(() => dispatch({ type: actionTypes.TICK, light: true, ts: Date.now() }), 800)
+ return setInterval(() => dispatch({ type: actionTypes.TICK, light: true, ts: Date.now() }), 1000)
}
export const addCount = () => dispatch => {
diff --git a/examples/with-typescript/.gitignore b/examples/with-typescript/.gitignore
deleted file mode 100644
index a6c7c285..00000000
--- a/examples/with-typescript/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.js
diff --git a/examples/with-typescript/README.md b/examples/with-typescript/README.md
index 3ccfe2b1..bb1a408e 100644
--- a/examples/with-typescript/README.md
+++ b/examples/with-typescript/README.md
@@ -31,4 +31,3 @@ npm install
npm run dev
```
-Output JS files are aside the related TypeScript ones.
diff --git a/examples/with-typescript/next.config.js b/examples/with-typescript/next.config.js
new file mode 100644
index 00000000..d8b638bd
--- /dev/null
+++ b/examples/with-typescript/next.config.js
@@ -0,0 +1,2 @@
+const withTypescript = require('@zeit/next-typescript')
+module.exports = withTypescript()
diff --git a/examples/with-typescript/package.json b/examples/with-typescript/package.json
index 5311bd17..cf30b7f6 100644
--- a/examples/with-typescript/package.json
+++ b/examples/with-typescript/package.json
@@ -1,22 +1,20 @@
{
- "name": "with-typescript",
+ "name": "with-typescript-plugin",
"version": "1.0.0",
"scripts": {
- "dev": "concurrently \"tsc --pretty --watch\" \"next\"",
- "prebuild": "tsc",
+ "dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
- "next": "latest",
- "react": "^16.1.0",
- "react-dom": "^16.1.0"
+ "next": "^5.0.0",
+ "react": "^16.2.0",
+ "react-dom": "^16.2.0"
},
"devDependencies": {
- "@types/next": "^2.4.5",
- "@types/react": "^16.0.22",
- "concurrently": "^3.5.0",
- "tslint": "^5.8.0",
- "typescript": "^2.6.1"
+ "@types/next": "^2.4.7",
+ "@types/react": "^16.0.36",
+ "@zeit/next-typescript": "0.0.8",
+ "typescript": "^2.7.1"
}
}
diff --git a/examples/with-typescript/tsconfig.json b/examples/with-typescript/tsconfig.json
index 804cc32c..41a1f0af 100644
--- a/examples/with-typescript/tsconfig.json
+++ b/examples/with-typescript/tsconfig.json
@@ -1,8 +1,26 @@
{
+ "compileOnSave": false,
"compilerOptions": {
- "jsx": "react-native",
- "module": "commonjs",
- "strict": true,
- "target": "es2017"
+ "target": "esnext",
+ "module": "esnext",
+ "jsx": "preserve",
+ "allowJs": true,
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "removeComments": false,
+ "preserveConstEnums": true,
+ "sourceMap": true,
+ "skipLibCheck": true,
+ "baseUrl": ".",
+ "typeRoots": [
+ "./node_modules/@types"
+ ],
+ "lib": [
+ "dom",
+ "es2015",
+ "es2016"
+ ]
}
}
diff --git a/examples/with-typescript/tslint.json b/examples/with-typescript/tslint.json
deleted file mode 100644
index 63f35ccc..00000000
--- a/examples/with-typescript/tslint.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "defaultSeverity": "error",
- "extends": ["tslint:recommended"],
- "jsRules": {},
- "rules": {
- "quotemark": [true, "single", "jsx-double"],
- "semicolon": [true, "never"]
- },
- "rulesDirectory": []
-}
diff --git a/examples/with-webpack-bundle-analyzer/next.config.js b/examples/with-webpack-bundle-analyzer/next.config.js
index ad085831..1bdbe095 100644
--- a/examples/with-webpack-bundle-analyzer/next.config.js
+++ b/examples/with-webpack-bundle-analyzer/next.config.js
@@ -2,11 +2,11 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const { ANALYZE } = process.env
module.exports = {
- webpack: function (config) {
+ webpack: function (config, { isServer }) {
if (ANALYZE) {
config.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'server',
- analyzerPort: 8888,
+ analyzerPort: isServer ? 8888 : 8889,
openAnalyzer: true
}))
}
diff --git a/examples/with-zones/blog/package.json b/examples/with-zones/blog/package.json
index fcd53769..72691fe9 100644
--- a/examples/with-zones/blog/package.json
+++ b/examples/with-zones/blog/package.json
@@ -6,7 +6,7 @@
"start": "next start -p 5000"
},
"dependencies": {
- "next": "zones",
+ "next": "latest",
"react": "^16.0.0",
"react-dom": "^16.0.0"
},
diff --git a/lib/router/with-router.js b/lib/router/with-router.js
index 1bca3f9d..91fceca8 100644
--- a/lib/router/with-router.js
+++ b/lib/router/with-router.js
@@ -11,7 +11,7 @@ export default function withRouter (ComposedComponent) {
router: PropTypes.object
}
- static displayName = `withRoute(${displayName})`
+ static displayName = `withRouter(${displayName})`
render () {
const props = {
diff --git a/package.json b/package.json
index 4636afcb..f639ecb0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "next",
- "version": "5.0.0",
+ "version": "5.0.1-canary.6",
"description": "Minimalistic framework for server-rendered React applications",
"main": "./dist/server/next.js",
"license": "MIT",
@@ -90,12 +90,12 @@
"pkg-up": "2.0.0",
"prop-types": "15.6.0",
"prop-types-exact": "1.1.1",
- "react-hot-loader": "4.0.0-beta.18",
+ "react-hot-loader": "4.0.0-beta.23",
"recursive-copy": "2.0.6",
"resolve": "1.5.0",
"send": "0.16.1",
"strip-ansi": "3.0.1",
- "styled-jsx": "2.2.3",
+ "styled-jsx": "2.2.5",
"touch": "3.1.0",
"uglifyjs-webpack-plugin": "1.1.6",
"unfetch": "3.0.0",
@@ -105,6 +105,7 @@
"webpack": "3.10.0",
"webpack-dev-middleware": "1.12.0",
"webpack-hot-middleware": "2.21.0",
+ "webpack-sources": "1.1.0",
"write-file-webpack-plugin": "4.2.0",
"xss-filters": "1.2.7"
},
diff --git a/readme.md b/readme.md
index 60352fcf..6c4c1816 100644
--- a/readme.md
+++ b/readme.md
@@ -169,9 +169,9 @@ To use more sophisticated CSS-in-JS solutions, you typically have to implement s
To support importing `.css` `.scss` or `.less` files you can use these modules, which configure sensible defaults for server rendered applications.
-- ![@zeit/next-css](https://github.com/zeit/next-plugins/tree/master/packages/next-css)
-- ![@zeit/next-sass](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)
-- ![@zeit/next-less](https://github.com/zeit/next-plugins/tree/master/packages/next-less)
+- [@zeit/next-css](https://github.com/zeit/next-plugins/tree/master/packages/next-css)
+- [@zeit/next-sass](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)
+- [@zeit/next-less](https://github.com/zeit/next-plugins/tree/master/packages/next-less)
### Static file serving (e.g.: images)
@@ -1037,6 +1037,17 @@ module.exports = {
This is development-only feature. If you want to cache SSR pages in production, please see [SSR-caching](https://github.com/zeit/next.js/tree/canary/examples/ssr-caching) example.
+#### Configuring extensions looked for when resolving pages in `pages`
+
+Aimed at modules like [`@zeit/next-typescript`](https://github.com/zeit/next-plugins/tree/master/packages/next-typescript), that add support for pages ending in `.ts`. `pageExtensions` allows you to configure the extensions looked for in the `pages` directory when resolving pages.
+
+```js
+// next.config.js
+module.exports = {
+ pageExtensions: ['jsx', 'js']
+}
+```
+
### Customizing webpack config
@@ -1077,6 +1088,21 @@ Some commonly asked for features are available as modules:
*Warning: The `webpack` function is executed twice, once for the server and once for the client. This allows you to distinguish between client and server configuration using the `isServer` property*
+Multiple configurations can be combined together with function composition. For example:
+
+```js
+const withTypescript = require('@zeit/next-typescript')
+const withSass = require('@zeit/next-sass')
+
+module.exports = withTypescript(withSass({
+ webpack(config, options) {
+ // Further custom configuration here
+ return config
+ }
+}))
+```
+
+
### Customizing babel config
@@ -1094,7 +1120,8 @@ Here's an example `.babelrc` file:
```json
{
- "presets": ["next/babel", "env"]
+ "presets": ["next/babel"],
+ "plugins": []
}
```
@@ -1267,6 +1294,7 @@ For the production deployment, you can use the [path alias](https://zeit.co/docs
- [Setting up 301 redirects](https://www.raygesualdo.com/posts/301-redirects-with-nextjs/)
- [Dealing with SSR and server only modules](https://arunoda.me/blog/ssr-and-server-only-modules)
+- [Building with React-Material-UI-Next-Express-Mongoose-Mongodb](https://github.com/builderbook/builderbook)
## FAQ
diff --git a/server/build/clean.js b/server/build/clean.js
deleted file mode 100644
index 4df79e9c..00000000
--- a/server/build/clean.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { resolve } from 'path'
-import del from 'del'
-import getConfig from '../config'
-
-export default function clean (dir) {
- const dist = getConfig(dir).distDir
- return del(resolve(dir, dist), { force: true })
-}
diff --git a/server/build/plugins/nextjs-ssr-import.js b/server/build/plugins/nextjs-ssr-import.js
index 1175eaf4..f9cc0b3b 100644
--- a/server/build/plugins/nextjs-ssr-import.js
+++ b/server/build/plugins/nextjs-ssr-import.js
@@ -1,22 +1,19 @@
-import { join } from 'path'
+import { join, resolve, relative, dirname } from 'path'
// This plugin modifies the require-ensure code generated by Webpack
// to work with Next.js SSR
export default class NextJsSsrImportPlugin {
- constructor ({ dir, dist }) {
- this.dir = dir
- this.dist = dist
- }
-
apply (compiler) {
compiler.plugin('compilation', (compilation) => {
- compilation.mainTemplate.plugin('require-ensure', (code) => {
+ compilation.mainTemplate.plugin('require-ensure', (code, chunk) => {
// Update to load chunks from our custom chunks directory
- const chunksDirPath = join(this.dir, this.dist, 'dist')
+ const outputPath = resolve('/')
+ const pagePath = join('/', dirname(chunk.name))
+ const relativePathToBaseDir = relative(pagePath, outputPath)
// Make sure even in windows, the path looks like in unix
// Node.js require system will convert it accordingly
- const chunksDirPathNormalized = chunksDirPath.replace(/\\/g, '/')
- let updatedCode = code.replace('require("./"', `require("${chunksDirPathNormalized}/"`)
+ const relativePathToBaseDirNormalized = relativePathToBaseDir.replace(/\\/g, '/')
+ let updatedCode = code.replace('require("./"', `require("${relativePathToBaseDirNormalized}/"`)
// Replace a promise equivalent which runs in the same loop
// If we didn't do this webpack's module loading process block us from
diff --git a/server/build/root-module-relative-path.js b/server/build/root-module-relative-path.js
deleted file mode 100644
index 54557a81..00000000
--- a/server/build/root-module-relative-path.js
+++ /dev/null
@@ -1,26 +0,0 @@
-// Next.js needs to use module.resolve to generate paths to modules it includes,
-// but those paths need to be relative to something so that they're portable
-// across directories and machines.
-//
-// This function returns paths relative to the top-level 'node_modules'
-// directory found in the path. If none is found, returns the complete path.
-
-import { sep } from 'path'
-
-const RELATIVE_START = `node_modules${sep}`
-
-// Pass in the module's `require` object since it's module-specific.
-export default (moduleRequire) => (path) => {
- // package.json removed because babel-runtime is resolved as
- // "babel-runtime/package"
- const absolutePath = moduleRequire.resolve(path)
- .replace(/[\\/]package\.json$/, '')
-
- const relativeStartIndex = absolutePath.indexOf(RELATIVE_START)
-
- if (relativeStartIndex === -1) {
- return absolutePath
- }
-
- return absolutePath.substring(relativeStartIndex + RELATIVE_START.length)
-}
diff --git a/server/build/webpack.js b/server/build/webpack.js
index 3084a37f..073b555c 100644
--- a/server/build/webpack.js
+++ b/server/build/webpack.js
@@ -71,7 +71,15 @@ function externalsConfig (dir, isServer) {
}
// Webpack itself has to be compiled because it doesn't always use module relative paths
- if (res.match(/node_modules[/\\].*\.js/) && !res.match(/node_modules[/\\]webpack/)) {
+ if (res.match(/node_modules[/\\]next[/\\]dist[/\\]pages/)) {
+ return callback()
+ }
+
+ if (res.match(/node_modules[/\\]webpack/)) {
+ return callback()
+ }
+
+ if (res.match(/node_modules[/\\].*\.js/)) {
return callback(null, `commonjs ${request}`)
}
@@ -107,7 +115,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
externals: externalsConfig(dir, isServer),
context: dir,
entry: async () => {
- const pages = await getPages(dir, {dev, isServer})
+ const pages = await getPages(dir, {dev, isServer, pageExtensions: config.pageExtensions.join('|')})
totalPages = Object.keys(pages).length
const mainJS = require.resolve(`../../client/next${dev ? '-dev' : ''}`)
const clientConfig = !isServer ? {
@@ -141,10 +149,15 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
],
alias: {
next: nextDir,
- // This bypasses React's check for production mode. Since we know it is in production this way.
- // This allows us to exclude React from being uglified. Saving multiple seconds per build.
- react: dev ? 'react/cjs/react.development.js' : 'react/cjs/react.production.min.js',
- 'react-dom': dev ? 'react-dom/cjs/react-dom.development.js' : 'react-dom/cjs/react-dom.production.min.js'
+ // React already does something similar to this.
+ // But if the user has react-devtools, it'll throw an error showing that
+ // we haven't done dead code elimination (via uglifyjs).
+ // We purposly do not uglify React code to save the build time.
+ // (But it didn't increase the overall build size)
+ // Here we are doing an exact match with '$'
+ // So, you can still require nested modules like `react-dom/server`
+ react$: dev ? 'react/cjs/react.development.js' : 'react/cjs/react.production.min.js',
+ 'react-dom$': dev ? 'react-dom/cjs/react-dom.development.js' : 'react-dom/cjs/react-dom.production.min.js'
}
},
resolveLoader: {
@@ -242,7 +255,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
!dev && new webpack.optimize.ModuleConcatenationPlugin(),
!isServer && new PagesPlugin(),
!isServer && new DynamicChunksPlugin(),
- isServer && new NextJsSsrImportPlugin({ dir, dist: config.distDir }),
+ isServer && new NextJsSsrImportPlugin(),
!isServer && new webpack.optimize.CommonsChunkPlugin({
name: `commons`,
filename: `commons.js`,
@@ -250,11 +263,11 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
// We need to move react-dom explicitly into common chunks.
// Otherwise, if some other page or module uses it, it might
// included in that bundle too.
- if (dev && module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) {
+ if (module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) {
return true
}
- if (dev && module.context && module.context.indexOf(`${sep}react-dom${sep}`) >= 0) {
+ if (module.context && module.context.indexOf(`${sep}react-dom${sep}`) >= 0) {
return true
}
@@ -301,7 +314,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
}
if (typeof config.webpack === 'function') {
- webpackConfig = config.webpack(webpackConfig, {dir, dev, isServer, buildId, config, defaultLoaders})
+ webpackConfig = config.webpack(webpackConfig, {dir, dev, isServer, buildId, config, defaultLoaders, totalPages})
}
return webpackConfig
diff --git a/server/build/webpack/utils.js b/server/build/webpack/utils.js
index 60e64909..d674e8a6 100644
--- a/server/build/webpack/utils.js
+++ b/server/build/webpack/utils.js
@@ -3,26 +3,28 @@ import glob from 'glob-promise'
const nextPagesDir = path.join(__dirname, '..', '..', '..', 'pages')
-export async function getPages (dir, {dev, isServer}) {
- const pageFiles = await getPagePaths(dir, {dev, isServer})
+export async function getPages (dir, {dev, isServer, pageExtensions}) {
+ const pageFiles = await getPagePaths(dir, {dev, isServer, pageExtensions})
- return getPageEntries(pageFiles, {isServer})
+ return getPageEntries(pageFiles, {isServer, pageExtensions})
}
-async function getPagePaths (dir, {dev, isServer}) {
+async function getPagePaths (dir, {dev, isServer, pageExtensions}) {
let pages
if (dev) {
- pages = await glob(isServer ? 'pages/+(_document|_error).+(js|jsx|ts|tsx)' : 'pages/_error.+(js|jsx|ts|tsx)', { cwd: dir })
+ // In development we only compile _document.js and _error.js when starting, since they're always needed. All other pages are compiled with on demand entries
+ pages = await glob(isServer ? `pages/+(_document|_error).+(${pageExtensions})` : `pages/_error.+(${pageExtensions})`, { cwd: dir })
} else {
- pages = await glob(isServer ? 'pages/**/*.+(js|jsx|ts|tsx)' : 'pages/**/!(_document)*.+(js|jsx|ts|tsx)', { cwd: dir })
+ // In production get all pages from the pages directory
+ pages = await glob(isServer ? `pages/**/*.+(${pageExtensions})` : `pages/**/!(_document)*.+(${pageExtensions})`, { cwd: dir })
}
return pages
}
// Convert page path into single entry
-export function createEntry (filePath, name) {
+export function createEntry (filePath, {name, pageExtensions} = {}) {
const parsedPath = path.parse(filePath)
let entryName = name || filePath
@@ -33,7 +35,9 @@ export function createEntry (filePath, name) {
}
// Makes sure supported extensions are stripped off. The outputted file should always be `.js`
- entryName = entryName.replace(/\.+(jsx|tsx|ts)/, '.js')
+ if (pageExtensions) {
+ entryName = entryName.replace(new RegExp(`\\.+(${pageExtensions})$`), '.js')
+ }
return {
name: path.join('bundles', entryName),
@@ -42,23 +46,23 @@ export function createEntry (filePath, name) {
}
// Convert page paths into entries
-export function getPageEntries (pagePaths, {isServer}) {
+export function getPageEntries (pagePaths, {isServer = false, pageExtensions} = {}) {
const entries = {}
for (const filePath of pagePaths) {
- const entry = createEntry(filePath)
+ const entry = createEntry(filePath, {pageExtensions})
entries[entry.name] = entry.files
}
const errorPagePath = path.join(nextPagesDir, '_error.js')
- const errorPageEntry = createEntry(errorPagePath, 'pages/_error.js') // default error.js
+ const errorPageEntry = createEntry(errorPagePath, {name: 'pages/_error.js'}) // default error.js
if (!entries[errorPageEntry.name]) {
entries[errorPageEntry.name] = errorPageEntry.files
}
if (isServer) {
const documentPagePath = path.join(nextPagesDir, '_document.js')
- const documentPageEntry = createEntry(documentPagePath, 'pages/_document.js')
+ const documentPageEntry = createEntry(documentPagePath, {name: 'pages/_document.js'}) // default _document.js
if (!entries[documentPageEntry.name]) {
entries[documentPageEntry.name] = documentPageEntry.files
}
diff --git a/server/config.js b/server/config.js
index 5aabc60b..7ed863c0 100644
--- a/server/config.js
+++ b/server/config.js
@@ -5,10 +5,12 @@ const cache = new Map()
const defaultConfig = {
webpack: null,
webpackDevMiddleware: null,
+ poweredByHeader: true,
distDir: '.next',
assetPrefix: '',
configOrigin: 'default',
- useFileSystemPublicRoutes: true
+ useFileSystemPublicRoutes: true,
+ pageExtensions: ['jsx', 'js'] // jsx before js because otherwise regex matching will match js first
}
export default function getConfig (dir, customConfig) {
@@ -32,9 +34,6 @@ function loadConfig (dir, customConfig) {
if (path && path.length) {
const userConfigModule = require(path)
userConfig = userConfigModule.default || userConfigModule
- if (userConfig.poweredByHeader === true || userConfig.poweredByHeader === false) {
- console.warn('> the `poweredByHeader` option has been removed https://err.sh/zeit/next.js/powered-by-header-option-removed')
- }
userConfig.configOrigin = 'next.config.js'
}
diff --git a/server/document.js b/server/document.js
index 49eea8a5..a68b7925 100644
--- a/server/document.js
+++ b/server/document.js
@@ -84,12 +84,12 @@ export class Head extends Component {
render () {
const { head, styles, __NEXT_DATA__ } = this.context._documentProps
- const { pathname, buildId, assetPrefix } = __NEXT_DATA__
+ const { page, pathname, buildId, assetPrefix } = __NEXT_DATA__
const pagePathname = getPagePathname(pathname)
return
{(head || []).map((h, i) => React.cloneElement(h, { key: h.key || i }))}
-
+ {page !== '/_error' && }
{this.getPreloadDynamicChunks()}
{this.getPreloadMainLinks()}
@@ -173,7 +173,7 @@ export class NextScript extends Component {
render () {
const { staticMarkup, __NEXT_DATA__, chunks } = this.context._documentProps
- const { pathname, buildId, assetPrefix } = __NEXT_DATA__
+ const { page, pathname, buildId, assetPrefix } = __NEXT_DATA__
const pagePathname = getPagePathname(pathname)
__NEXT_DATA__.chunks = chunks.names
@@ -193,9 +193,18 @@ export class NextScript extends Component {
__NEXT_REGISTER_CHUNK = function (chunkName, fn) {
__NEXT_LOADED_CHUNKS__.push({ chunkName: chunkName, fn: fn })
}
+
+ ${page === '_error' && `
+ __NEXT_REGISTER_PAGE(${htmlescape(pathname)}, function() {
+ var error = new Error('Page does not exist: ${htmlescape(pathname)}')
+ error.statusCode = 404
+
+ return { error: error }
+ })
+ `}
`
}} />}
-
+ {page !== '/_error' && }
{staticMarkup ? null : this.getDynamicChunks()}
{staticMarkup ? null : this.getScripts()}
diff --git a/server/export.js b/server/export.js
index 92f0aa43..ba05caec 100644
--- a/server/export.js
+++ b/server/export.js
@@ -49,6 +49,15 @@ export default async function (dir, options, configuration) {
)
}
+ // Copy .next/static directory
+ if (existsSync(join(nextDir, 'static'))) {
+ log(' copying "static build" directory')
+ await cp(
+ join(nextDir, 'static'),
+ join(outDir, '_next', 'static')
+ )
+ }
+
// Copy dynamic import chunks
if (existsSync(join(nextDir, 'chunks'))) {
log(' copying dynamic import chunks')
@@ -76,6 +85,7 @@ export default async function (dir, options, configuration) {
// Start the rendering process
const renderOpts = {
dir,
+ dist: config.distDir,
buildStats,
buildId,
nextExport: true,
diff --git a/server/hot-reloader.js b/server/hot-reloader.js
index fb4b92de..76ab8c7d 100644
--- a/server/hot-reloader.js
+++ b/server/hot-reloader.js
@@ -1,11 +1,10 @@
import { join, relative, sep } from 'path'
import WebpackDevMiddleware from 'webpack-dev-middleware'
import WebpackHotMiddleware from 'webpack-hot-middleware'
+import del from 'del'
import onDemandEntryHandler from './on-demand-entry-handler'
import webpack from 'webpack'
import getBaseWebpackConfig from './build/webpack'
-import clean from './build/clean'
-import getConfig from './config'
import UUID from 'uuid'
import {
IS_BUNDLED_PAGE,
@@ -13,7 +12,7 @@ import {
} from './utils'
export default class HotReloader {
- constructor (dir, { quiet, conf } = {}) {
+ constructor (dir, { quiet, config } = {}) {
this.dir = dir
this.quiet = quiet
this.middlewares = []
@@ -32,7 +31,7 @@ export default class HotReloader {
// it should be the same value.
this.buildId = UUID.v4()
- this.config = getConfig(dir, conf)
+ this.config = config
}
async run (req, res) {
@@ -55,8 +54,12 @@ export default class HotReloader {
}
}
+ async clean () {
+ return del(join(this.dir, this.config.distDir), { force: true })
+ }
+
async start () {
- await clean(this.dir)
+ await this.clean()
const configs = await Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config }),
@@ -86,7 +89,7 @@ export default class HotReloader {
async reload () {
this.stats = null
- await clean(this.dir)
+ await this.clean()
const configs = await Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config }),
@@ -225,6 +228,7 @@ export default class HotReloader {
dir: this.dir,
dev: true,
reload: this.reload.bind(this),
+ pageExtensions: this.config.pageExtensions,
...this.config.onDemandEntries
})
diff --git a/server/index.js b/server/index.js
index 633c3d83..1b1e546b 100644
--- a/server/index.js
+++ b/server/index.js
@@ -32,11 +32,12 @@ export default class Server {
this.dev = dev
this.quiet = quiet
this.router = new Router()
- this.hotReloader = dev ? this.getHotReloader(this.dir, { quiet, conf }) : null
this.http = null
this.config = getConfig(this.dir, conf)
this.dist = this.config.distDir
+ this.hotReloader = dev ? this.getHotReloader(this.dir, { quiet, config: this.config }) : null
+
if (dev) {
updateNotifier(pkg, 'next')
}
@@ -51,6 +52,7 @@ export default class Server {
dev,
staticMarkup,
dir: this.dir,
+ dist: this.dist,
hotReloader: this.hotReloader,
buildStats: this.buildStats,
buildId: this.buildId,
@@ -183,8 +185,7 @@ export default class Server {
}
}
- const dist = getConfig(this.dir).distDir
- const path = join(this.dir, dist, 'bundles', 'pages', `${page}.js.map`)
+ const path = join(this.dir, this.dist, 'bundles', 'pages', `${page}.js.map`)
await serveStatic(req, res, path)
},
@@ -241,7 +242,7 @@ export default class Server {
},
'/_next/static/:path*': async (req, res, params) => {
- const p = join(this.dist, 'static', ...(params.path || []))
+ const p = join(this.dir, this.dist, 'static', ...(params.path || []))
await this.serveStatic(req, res, p)
},
@@ -322,7 +323,9 @@ export default class Server {
return
}
- res.setHeader('X-Powered-By', `Next.js ${pkg.version}`)
+ if (this.config.poweredByHeader) {
+ res.setHeader('X-Powered-By', `Next.js ${pkg.version}`)
+ }
return sendHTML(req, res, html, req.method, this.renderOpts)
}
diff --git a/server/on-demand-entry-handler.js b/server/on-demand-entry-handler.js
index 0bda8df7..5bca16e0 100644
--- a/server/on-demand-entry-handler.js
+++ b/server/on-demand-entry-handler.js
@@ -1,9 +1,10 @@
import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'
import { EventEmitter } from 'events'
-import { join, relative } from 'path'
+import { join } from 'path'
import { parse } from 'url'
import touch from 'touch'
-import resolvePath from './resolve'
+import glob from 'glob-promise'
+import {normalizePagePath, pageNotFoundError} from './require'
import {createEntry} from './build/webpack/utils'
import { MATCH_ROUTE_NAME, IS_BUNDLED_PAGE } from './utils'
@@ -15,6 +16,7 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
dir,
dev,
reload,
+ pageExtensions,
maxInactiveAge = 1000 * 60,
pagesBufferLength = 2
}) {
@@ -37,7 +39,7 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
const allEntries = Object.keys(entries).map((page) => {
const { name, entry } = entries[page]
entries[page].status = BUILDING
- return addEntry(compilation, this.context, name, entry)
+ return addEntry(compilation, compiler.context, name, entry)
})
Promise.all(allEntries)
@@ -139,10 +141,26 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
async ensurePage (page) {
await this.waitUntilReloaded()
page = normalizePage(page)
+ let normalizedPagePath
+ try {
+ normalizedPagePath = normalizePagePath(page)
+ } catch (err) {
+ console.error(err)
+ throw pageNotFoundError(normalizedPagePath)
+ }
- const pagePath = join(dir, 'pages', page)
- const pathname = await resolvePath(pagePath)
- const {name, files} = createEntry(relative(dir, pathname))
+ const extensions = pageExtensions.join('|')
+ const paths = await glob(`pages/{${normalizedPagePath}/index,${normalizedPagePath}}.+(${extensions})`, {cwd: dir})
+
+ if (paths.length === 0) {
+ throw pageNotFoundError(normalizedPagePath)
+ }
+
+ const relativePathToPage = paths[0]
+
+ const pathname = join(dir, relativePathToPage)
+
+ const {name, files} = createEntry(relativePathToPage, {pageExtensions: extensions})
await new Promise((resolve, reject) => {
const entryInfo = entries[page]
diff --git a/server/render.js b/server/render.js
index 8684c1fa..3d306f26 100644
--- a/server/render.js
+++ b/server/render.js
@@ -4,8 +4,7 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import send from 'send'
import generateETag from 'etag'
import fresh from 'fresh'
-import requireModule from './require'
-import getConfig from './config'
+import requirePage from './require'
import { Router } from '../lib/router'
import { loadGetInitialProps, isResSent } from '../lib/utils'
import { getAvailableChunks } from './utils'
@@ -30,7 +29,7 @@ export async function renderError (err, req, res, pathname, query, opts) {
}
export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) {
- return doRender(req, res, pathname, query, { ...opts, err, page: '_error' })
+ return doRender(req, res, pathname, query, { ...opts, err, page: '/_error' })
}
async function doRender (req, res, pathname, query, {
@@ -41,6 +40,7 @@ async function doRender (req, res, pathname, query, {
hotReloader,
assetPrefix,
availableChunks,
+ dist,
dir = process.cwd(),
dev = false,
staticMarkup = false,
@@ -48,17 +48,14 @@ async function doRender (req, res, pathname, query, {
} = {}) {
page = page || pathname
- await ensurePage(page, { dir, hotReloader })
+ if (hotReloader) { // In dev mode we use on demand entries to compile the page before rendering
+ await ensurePage(page, { dir, hotReloader })
+ }
- const dist = getConfig(dir).distDir
-
- const pagePath = join(dir, dist, 'dist', 'bundles', 'pages', page)
const documentPath = join(dir, dist, 'dist', 'bundles', 'pages', '_document')
- let [Component, Document] = await Promise.all([
- requireModule(pagePath),
- requireModule(documentPath)
- ])
+ let Component = requirePage(page, {dir, dist})
+ let Document = require(documentPath)
Component = Component.default || Component
Document = Document.default || Document
const asPath = req.url
@@ -105,7 +102,8 @@ async function doRender (req, res, pathname, query, {
const doc = createElement(Document, {
__NEXT_DATA__: {
props,
- pathname,
+ page, // the rendered page
+ pathname, // the requested path
query,
buildId,
buildStats,
@@ -224,8 +222,7 @@ export function serveStatic (req, res, path) {
}
async function ensurePage (page, { dir, hotReloader }) {
- if (!hotReloader) return
- if (page === '_error' || page === '_document') return
+ if (page === '/_error') return
await hotReloader.ensurePage(page)
}
diff --git a/server/require.js b/server/require.js
index 3ca13f32..a677f870 100644
--- a/server/require.js
+++ b/server/require.js
@@ -1,6 +1,63 @@
-import resolve from './resolve'
+import {join, parse, normalize, sep} from 'path'
-export default async function requireModule (path) {
- const f = await resolve(path)
- return require(f)
+export function pageNotFoundError (page) {
+ const err = new Error(`Cannot find module for page: ${page}`)
+ err.code = 'ENOENT'
+ return err
+}
+
+export function normalizePagePath (page) {
+ // If the page is `/` we need to append `/index`, otherwise the returned directory root will be bundles instead of pages
+ if (page === '/') {
+ page = '/index'
+ }
+
+ // Resolve on anything that doesn't start with `/`
+ if (page[0] !== '/') {
+ page = `/${page}`
+ }
+
+ // Windows compatibility
+ if (sep !== '/') {
+ page = page.replace(/\//g, sep)
+ }
+
+ // Throw when using ../ etc in the pathname
+ const resolvedPage = normalize(page)
+ if (page !== resolvedPage) {
+ throw new Error('Requested and resolved page mismatch')
+ }
+
+ return page
+}
+
+export function getPagePath (page, {dir, dist}) {
+ const pageBundlesPath = join(dir, dist, 'dist', 'bundles', 'pages')
+
+ try {
+ page = normalizePagePath(page)
+ } catch (err) {
+ console.error(err)
+ throw pageNotFoundError(page)
+ }
+
+ const pagePath = join(pageBundlesPath, page) // Path to the page that is to be loaded
+
+ // Don't allow wandering outside of the bundles directory
+ const pathDir = parse(pagePath).dir
+ if (pathDir.indexOf(pageBundlesPath) !== 0) {
+ console.error('Resolved page path goes outside of bundles path')
+ throw pageNotFoundError(page)
+ }
+
+ return pagePath
+}
+
+export default function requirePage (page, {dir, dist}) {
+ const pagePath = getPagePath(page, {dir, dist})
+ try {
+ return require(pagePath)
+ } catch (err) {
+ throw pageNotFoundError(page)
+ }
}
diff --git a/server/resolve.js b/server/resolve.js
deleted file mode 100644
index d33fee65..00000000
--- a/server/resolve.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import { join, sep, parse } from 'path'
-import fs from 'mz/fs'
-import glob from 'glob-promise'
-
-export default async function resolve (id) {
- const paths = getPaths(id)
- for (const p of paths) {
- if (await isFile(p)) {
- return p
- }
- }
-
- const err = new Error(`Cannot find module ${id}`)
- err.code = 'ENOENT'
- throw err
-}
-
-export function resolveFromList (id, files) {
- const paths = getPaths(id)
- const set = new Set(files)
- for (const p of paths) {
- if (set.has(p)) return p
- }
-}
-
-function getPaths (id) {
- const i = sep === '/' ? id : id.replace(/\//g, sep)
-
- if (i.slice(-3) === '.js') return [i]
- if (i.slice(-4) === '.jsx') return [i]
- if (i.slice(-4) === '.tsx') return [i]
- if (i.slice(-3) === '.ts') return [i]
- if (i.slice(-5) === '.json') return [i]
-
- if (i[i.length - 1] === sep) {
- return [
- i + 'index.js',
- i + 'index.jsx',
- i + 'index.ts',
- i + 'index.tsx',
- i + 'index.json'
- ]
- }
-
- return [
- i + '.js',
- join(i, 'index.js'),
- i + '.jsx',
- join(i, 'index.jsx'),
- i + '.tsx',
- join(i, 'index.tsx'),
- i + '.ts',
- join(i, 'index.ts'),
- i + '.json',
- join(i, 'index.json')
- ]
-}
-
-async function isFile (p) {
- let stat
- try {
- stat = await fs.stat(p)
- } catch (err) {
- if (err.code === 'ENOENT') return false
- throw err
- }
-
- // We need the path to be case sensitive
- const realpath = await getTrueFilePath(p)
- if (p !== realpath) return false
-
- return stat.isFile() || stat.isFIFO()
-}
-
-// This is based on the stackoverflow answer: http://stackoverflow.com/a/33139702/457224
-// We assume we'll get properly normalized path names as p
-async function getTrueFilePath (p) {
- let fsPathNormalized = p
- // OSX: HFS+ stores filenames in NFD (decomposed normal form) Unicode format,
- // so we must ensure that the input path is in that format first.
- if (process.platform === 'darwin') fsPathNormalized = fsPathNormalized.normalize('NFD')
-
- // !! Windows: Curiously, the drive component mustn't be part of a glob,
- // !! otherwise glob.sync() will invariably match nothing.
- // !! Thus, we remove the drive component and instead pass it in as the 'cwd'
- // !! (working dir.) property below.
- var pathRoot = parse(fsPathNormalized).root
- var noDrivePath = fsPathNormalized.slice(Math.max(pathRoot.length - 1, 0))
-
- // Perform case-insensitive globbing (on Windows, relative to the drive /
- // network share) and return the 1st match, if any.
- // Fortunately, glob() with nocase case-corrects the input even if it is
- // a *literal* path.
- const result = await glob(noDrivePath, { nocase: true, cwd: pathRoot })
- return result[0]
-}
diff --git a/test/integration/basic/test/client-navigation.js b/test/integration/basic/test/client-navigation.js
index d0f5685d..7fbec21b 100644
--- a/test/integration/basic/test/client-navigation.js
+++ b/test/integration/basic/test/client-navigation.js
@@ -370,16 +370,8 @@ export default (context, render) => {
browser.close()
})
- it('should work with dir/index page ', async () => {
- const browser = await webdriver(context.appPort, '/nested-cdm/index')
- const text = await browser.elementByCss('p').text()
-
- expect(text).toBe('ComponentDidMount executed on client.')
- browser.close()
- })
-
it('should work with dir/ page ', async () => {
- const browser = await webdriver(context.appPort, '/nested-cdm/')
+ const browser = await webdriver(context.appPort, '/nested-cdm')
const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.')
@@ -426,7 +418,7 @@ export default (context, render) => {
describe('with asPath', () => {
describe('inside getInitialProps', () => {
it('should show the correct asPath with a Link with as prop', async () => {
- const browser = await webdriver(context.appPort, '/nav/')
+ const browser = await webdriver(context.appPort, '/nav')
const asPath = await browser
.elementByCss('#as-path-link').click()
.waitForElementByCss('.as-path-content')
@@ -437,7 +429,7 @@ export default (context, render) => {
})
it('should show the correct asPath with a Link without the as prop', async () => {
- const browser = await webdriver(context.appPort, '/nav/')
+ const browser = await webdriver(context.appPort, '/nav')
const asPath = await browser
.elementByCss('#as-path-link-no-as').click()
.waitForElementByCss('.as-path-content')
@@ -450,7 +442,7 @@ export default (context, render) => {
describe('with next/router', () => {
it('should show the correct asPath', async () => {
- const browser = await webdriver(context.appPort, '/nav/')
+ const browser = await webdriver(context.appPort, '/nav')
const asPath = await browser
.elementByCss('#as-path-using-router-link').click()
.waitForElementByCss('.as-path-content')
@@ -461,5 +453,31 @@ export default (context, render) => {
})
})
})
+
+ describe('with 404 pages', () => {
+ it('should 404 on not existent page', async () => {
+ const browser = await webdriver(context.appPort, '/non-existent')
+ expect(await browser.elementByCss('h1').text()).toBe('404')
+ expect(await browser.elementByCss('h2').text()).toBe('This page could not be found.')
+ browser.close()
+ })
+
+ it('should 404 for /', async () => {
+ const browser = await webdriver(context.appPort, '/nav/about/')
+ expect(await browser.elementByCss('h1').text()).toBe('404')
+ expect(await browser.elementByCss('h2').text()).toBe('This page could not be found.')
+ browser.close()
+ })
+
+ it('should should not contain a page script in a 404 page', async () => {
+ const browser = await webdriver(context.appPort, '/non-existent')
+ const scripts = await browser.elementsByCss('script[src]')
+ for (const script of scripts) {
+ const src = await script.getAttribute('src')
+ expect(src.includes('/non-existent')).toBeFalsy()
+ }
+ browser.close()
+ })
+ })
})
}
diff --git a/test/integration/basic/test/rendering.js b/test/integration/basic/test/rendering.js
index 08c87f76..90cf64a4 100644
--- a/test/integration/basic/test/rendering.js
+++ b/test/integration/basic/test/rendering.js
@@ -106,10 +106,28 @@ export default function ({ app }, suiteName, render, fetch) {
expect($('.as-path-content').text()).toBe('/nav/as-path?aa=10')
})
- test('error 404', async () => {
- const $ = await get$('/non-existent')
- expect($('h1').text()).toBe('404')
- expect($('h2').text()).toBe('This page could not be found.')
+ describe('404', () => {
+ it('should 404 on not existent page', async () => {
+ const $ = await get$('/non-existent')
+ expect($('h1').text()).toBe('404')
+ expect($('h2').text()).toBe('This page could not be found.')
+ })
+
+ it('should 404 for /', async () => {
+ const $ = await get$('/nav/about/')
+ expect($('h1').text()).toBe('404')
+ expect($('h2').text()).toBe('This page could not be found.')
+ })
+
+ it('should should not contain a page script in a 404 page', async () => {
+ const $ = await get$('/non-existent')
+ $('script[src]').each((index, element) => {
+ const src = $(element).attr('src')
+ if (src.includes('/non-existent')) {
+ throw new Error('Page includes page script')
+ }
+ })
+ })
})
describe('with the HOC based router', () => {
diff --git a/test/integration/production/pages/counter.js b/test/integration/production/pages/counter.js
new file mode 100644
index 00000000..38b7c7bc
--- /dev/null
+++ b/test/integration/production/pages/counter.js
@@ -0,0 +1,33 @@
+import Link from 'next/link'
+import { Component } from 'react'
+import Router from 'next/router'
+
+let counter = 0
+
+export default class extends Component {
+ increase () {
+ counter++
+ this.forceUpdate()
+ }
+
+ visitQueryStringPage () {
+ const href = { pathname: '/nav/querystring', query: { id: 10 } }
+ const as = { pathname: '/nav/querystring/10', hash: '10' }
+ Router.push(href, as)
+ }
+
+ render () {
+ return (
+