1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Compare commits

...

38 commits

Author SHA1 Message Date
Kegan Myers 50c1251b85
Merge branch 'canary' into canary 2019-02-18 02:25:07 -06:00
Kegan Myers 94460eee55 Add 'unified' SSR compilation target 2019-02-17 23:48:33 -06:00
Timon Borter f9fedaeba6 recreate stdChannel (or saga middleware). (#6330) 2019-02-17 20:57:59 +01:00
Tim Neutkens 9f2eb85de3 v8.0.2-canary.3 2019-02-17 20:48:43 +01:00
Tim Neutkens a1ccc19a1f
Pass through arguments of the next cli correctly (#6327)
Arguments that held the same name as one of the default commands were filtered out, causing issues.

For example `next build build` would get rid of the second `build` parameter.

Fixes #6263
2019-02-17 20:13:10 +01:00
Tim Neutkens dd9811b206
Fix recursive hydration of next/dynamic (#6326)
Fixes #5347

The main issue is that we were waiting only 1 level of dynamic imports, so the dynamic imports nested inside other dynamic import files were not awaited. This would cause either a flash of loading states or you wouldn't see the loading state (because of preload) but it would then show a hydration warning in development.

Thanks to @arthens for providing the reproduction that I modelled the tests after.
2019-02-17 19:52:00 +01:00
Ahmed Tarek 5cef35b811 Fix style importing (#6322) 2019-02-17 13:32:58 +01:00
Tim Neutkens 774af7fa0b v8.0.2-canary.2 2019-02-17 13:15:42 +01:00
Joshua Scott 46d870ab8a Fix url in docs (#6323) 2019-02-17 12:57:17 +01:00
Joe Haddad 7078f6567d Make build output friendlier (#6320)
Success:
![image](https://user-images.githubusercontent.com/616428/52907314-5e636480-322d-11e9-9420-b348663a3a7a.png)

Error:
![image](https://user-images.githubusercontent.com/616428/52907318-6c18ea00-322d-11e9-848d-e615d6af747d.png)

Warnings:
![image](https://user-images.githubusercontent.com/616428/52907353-2d376400-322e-11e9-9778-370f36912491.png)

---

We can still make build error output friendlier, but this is a good start.
2019-02-17 12:56:48 +01:00
Tim Neutkens 3882979236 v8.0.2-canary.1 2019-02-16 17:12:27 +01:00
Joe Haddad 56b1f81ace Fix development mode output (#6312)
* Remove usage of WebpackBar and Friendly Errors

* Add new clearConsole helper

* Add new simplified output for development mode

* Add an explicit bootstrapping mode

* Add missing returns

* Use existing output style

* Adjust first output to say Waiting on

* Only print URL if present
2019-02-16 17:09:49 +01:00
Tim Neutkens 036f5bf11b v8.0.2-canary.0 2019-02-16 16:41:30 +01:00
Tim Neutkens 708c537fc6 Merge branch 'master' into canary 2019-02-16 16:38:42 +01:00
JJ Kasper 5d779a0289 Add falling back to fetch based pinging for onDemandEntries (#6310)
After discussion, I added falling back to fetch based pinging when the WebSocket fails to connect. I also added an example of how to proxy the onDemandEntries WebSocket when using a custom server. Fixes: #6296
2019-02-15 22:22:21 +01:00
Connor Davis 1e5d0908d0 Block Certain Env Keys That Are Used Internally (#6260)
Closes: #6244 

This will block the following keys:
```
NODE_.+
__.+
```

There doesn't seem to be a way to simulate a failed build or else I'd add tests for it.
2019-02-15 17:49:40 +01:00
Timon Borter d2ef34429c push redux-saga to major release 1.0.1. (#6300) 2019-02-14 19:05:27 +01:00
Felix Mosheev 04ce3e7174 Use process.browser instead of env probing (#6286) 2019-02-14 19:05:08 +01:00
Joe Haddad 9bb8fbf535
Update webpack message formatter (#6299) 2019-02-14 11:13:35 -05:00
Tim Neutkens 4051ffcb01 [experimental] Rendering to AMP (#6218)
* Add initial AMP implementation

* Implement experimental feature flag

* Implement feedback from sbenz

* Add next/amp and `useAmp` hook

* Use /:path*/amp instead

* Add canonical

* Add amphtml tag

* Add ampEnabled for rel=“amphtml”

* Remove extra type
2019-02-14 10:22:57 -05:00
Joe Haddad 36946f9709
Remove lerna bootstrap (#6289) 2019-02-14 08:33:00 -05:00
Jonathan Reed 7dbe837ae4 fixes hashed statics readme (#6293)
# Description

* Fixes incorrect assertion of configuration file in the `with-hashed-statics` README as well as adds link to line for updating
2019-02-13 19:53:42 +01:00
Gary Meehan 126eb49867 Fix README links (#6284) 2019-02-13 10:53:04 -05:00
Tim Neutkens 7fd9cb440d v8.0.1 2019-02-13 09:31:49 +01:00
Tim Neutkens 77a5a6f91a v8.0.1-canary.0 2019-02-13 07:20:54 +01:00
Tim Neutkens 4fea345f5d Merge branch 'master' into canary
# Conflicts:
#	lerna.json
#	packages/next-server/package.json
#	packages/next/package.json
2019-02-13 07:19:58 +01:00
Truong Hoang Dung 10f41f5d47 Fix Docs (#6270)
Add options to customize webpack config section.
2019-02-13 07:08:41 +01:00
Joe Haddad f43e1a95f1
Set default Error status code to 404 (#6276)
* Set default `Error` status code to 404

This is an appropriate default behavior because:

1. When the server encounters an error, the `err` property is set.
2. When the client-side application crashes, the `err` property is set.

This means the "only" way to render the `/_error` page without an error
is when a page is not found (special condition).

Fixes #6243
Closes #5437

* Add new integration test for client side 404

* single quotes

* Remove unused variable

* Standard needs to go away

* Whoops

* Check for null status code in res and err

* Only check response for valid statusCode
2019-02-12 21:32:25 -05:00
Connor Davis 68db0992b6
v8.0.0-canary.24 2019-02-11 19:29:58 -06:00
Connor Davis bd249180c6
Fix Runtime Config in next export (#6258) 2019-02-11 19:28:47 -06:00
Joe Haddad 33b9ebc783 Add module as server fallback main field (#6256)
* Add `module` as server fallback main field

* Test that a module only package can be imported
2019-02-12 01:39:57 +01:00
Juan Olvera 23c9c0d624 Change anynymous functions to named functions on examples in the README.md file (#6255)
* convert export default anonymous functions into named functions

* change examples to function declaration and split export in classes

* change NextHead name to Head and rename component
2019-02-12 00:04:05 +01:00
Connor Davis e1056e32cf Add yarn check to test (#6257) 2019-02-11 23:26:42 +01:00
Spencer Elliott 4dd6094639 styled-components example: use a fragment for styles initial prop (#6252)
`initialProps.styles` is a React node, but not guaranteed to be an
array, so we can use a fragment to concatenate additional styles.

See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/32932#issuecomment-462372319
2019-02-11 20:48:03 +01:00
Jason Miller 734513b9be Apply babel to .mjs files (#6253) 2019-02-11 18:59:24 +01:00
Juan Olvera 80cb91ec87 Add setup to run example with cookie authentication locally (#6101)
* extract request login from auth

* add clarification that the monorepo is for deploy in Now only and fix typo

* Refactor HOC

- add authorization to HOC
- add displayName to HOC
- remove unnecessary `run`s in local routing
2019-02-11 14:17:43 +01:00
Fredrik Höglund 2ab1ae7f61 Updated examples for build-time env configuration for v8 (#6237)
* Updated examples for build-time env configuration for v8

* Add comment to build time config example with how to include entire .env
2019-02-11 14:15:06 +01:00
Resi Respati 3746d7d90b [with-typescript] Fixed incorrect query type (#6238) 2019-02-11 10:32:10 +01:00
106 changed files with 2356 additions and 752 deletions

View file

@ -9,9 +9,6 @@ jobs:
- run:
name: Installing dependencies
command: yarn install
- run:
name: Bootstrapping
command: yarn bootstrap
- run:
name: Linting
command: yarn lint
@ -31,4 +28,4 @@ workflows:
version: 2
build-and-deploy:
jobs:
- build
- build

View file

@ -20,6 +20,6 @@
before_cache: [
"rm -rf node_modules/.cache"
],
before_script: ["npm run bootstrap"],
before_script: [],
after_script: ["npm run coveralls"]
}

View file

@ -765,7 +765,7 @@ class MyLink extends React.Component {
const { router } = this.props
router.prefetch('/dynamic')
}
render() {
const { router } = this.props
return (
@ -773,7 +773,7 @@ class MyLink extends React.Component {
<a onClick={() => setTimeout(() => router.push('/dynamic'), 100)}>
A route transition will happen after 100ms
</a>
</div>
</div>
)
}
}
@ -1343,7 +1343,7 @@ module.exports = {
```js
// Example next.config.js for adding a loader that depends on babel-loader
// This source was taken from the @zeit/next-mdx plugin source:
// This source was taken from the @zeit/next-mdx plugin source:
// https://github.com/zeit/next-plugins/blob/master/packages/next-mdx
module.exports = {
webpack: (config, {}) => {
@ -1464,7 +1464,7 @@ module.exports = {
}
```
注意Next.js 运行时将会自动添加前缀,但是对于`/static`是没有效果的,如果你想这些静态资源也能使用 CDN你需要自己添加前缀。有一个方法可以判断你的环境来加前缀如 [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration)。
注意Next.js 运行时将会自动添加前缀,但是对于`/static`是没有效果的,如果你想这些静态资源也能使用 CDN你需要自己添加前缀。有一个方法可以判断你的环境来加前缀如 [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration-build-time)。
<a id="production-deployment" style="display: none"></a>
## 项目部署

View file

@ -19,10 +19,6 @@ steps:
yarn install
displayName: 'Install dependencies'
- script: |
yarn bootstrap
displayName: 'Lerna bootstrap'
- script: |
yarn test
displayName: 'Run tests'

View file

@ -3,10 +3,9 @@
Our Commitment to Open Source can be found [here](https://zeit.co/blog/oss)
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Install yarn: `npm install -g yarn`
3. Install the dependencies: `yarn`
4. Run `yarn run bootstrap`, which will link all repositories locally
5. Run `yarn run dev` to build and watch for code changes
1. Install yarn: `npm install -g yarn`
1. Install the dependencies: `yarn`
1. Run `yarn run dev` to build and watch for code changes
## To run tests

View file

@ -0,0 +1,9 @@
# The key "<your key>" under "env" in next.config.js is not allowed.
#### Why This Error Occurred
Next.js configures internal variables for replacement itself. These start with `__` or `NODE_`, for this reason they are not allowed as values for `env` in `next.config.js`
#### Possible Ways to Fix It
Rename the specified key so that it does not start with `__` or `NODE_`.

View file

@ -0,0 +1,29 @@
# onDemandEntries WebSocket unavailable
#### Why This Error Occurred
By default Next.js uses a random port to create a WebSocket to receive pings from the client letting it know to keep pages active. For some reason when the client tried to connect to this WebSocket the connection fails.
#### Possible Ways to Fix It
If you don't mind the fetch requests in your network console then you don't have to do anything as the fallback to fetch works fine. If you do, then depending on your set up you might need configure settings using the below config options from `next.config.js`:
```js
module.exports = {
onDemandEntries: {
// optionally configure a port for the onDemandEntries WebSocket, not needed by default
websocketPort: 3001,
// optionally configure a proxy path for the onDemandEntries WebSocket, not need by default
websocketProxyPath: '/hmr',
// optionally configure a proxy port for the onDemandEntries WebSocket, not need by default
websocketProxyPort: 7002,
},
}
```
If you are using a custom server with SSL configured, you might want to take a look at [the example](https://github.com/zeit/next.js/tree/canary/examples/custom-server-proxy-websocket) showing how to proxy the WebSocket connection through your custom server
### Useful Links
- [onDemandEntries config](https://github.com/zeit/next.js#configuring-the-ondemandentries)
- [Custom server proxying example](https://github.com/zeit/next.js/tree/canary/examples/custom-server-proxy-websocket)

View file

@ -0,0 +1,38 @@
# Custom server with Proxying onDemandEntries WebSocket
## How to use
### Using `create-next-app`
Execute [`create-next-app`](https://github.com/segmentio/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example:
```bash
npx create-next-app --example custom-server-proxy-websocket custom-server-proxy-websocket
# or
yarn create next-app --example custom-server-proxy-websocket custom-server-proxy-websocket
```
### Download manually
Download the example:
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/custom-server-proxy-websocket
cd custom-server-proxy-websocket
```
Install it and run:
```bash
npm install
npm run ssl
npm run dev
# or
yarn
yarn ssl
yarn dev
```
## The idea behind the example
The example shows how you can use SSL with a custom server and still use onDemandEntries WebSocket from Next.js using [node-http-proxy](https://github.com/nodejitsu/node-http-proxy#readme) and [ExpressJS](https://github.com/expressjs/express).

View file

@ -0,0 +1,7 @@
#!/bin/sh
# Generate self-signed certificate (only meant for testing don't use in production...)
# requires openssl be installed and in the $PATH
openssl genrsa -out localhost.key 2048
openssl req -new -x509 -key localhost.key -out localhost.cert -days 3650 -subj /CN=localhost

View file

@ -0,0 +1,6 @@
module.exports = {
onDemandEntries: {
websocketPort: 3001,
websocketProxyPort: 3000
}
}

View file

@ -0,0 +1,21 @@
{
"name": "custom-server-proxy-websocket",
"version": "1.0.0",
"main": "server.js",
"license": "MIT",
"scripts": {
"dev": "node server.js",
"build": "next build",
"ssl": "./genSSL.sh",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"express": "4.16.4",
"next": "8.0.1",
"react": "16.8.2",
"react-dom": "16.8.2"
},
"devDependencies": {
"http-proxy": "1.17.0"
}
}

View file

@ -0,0 +1,10 @@
import Link from 'next/link'
export default () => (
<div>
<h3>Another</h3>
<Link href='/'>
<a>Index</a>
</Link>
</div>
)

View file

@ -0,0 +1,10 @@
import Link from 'next/link'
export default () => (
<div>
<h3>Index</h3>
<Link href='/another'>
<a>Another</a>
</Link>
</div>
)

View file

@ -0,0 +1,43 @@
const express = require('express')
const Next = require('next')
const https = require('https')
const fs = require('fs')
const app = express()
const port = 3000
const isDev = process.env.NODE_ENV !== 'production'
const next = Next({ dev: isDev })
// Set up next
next.prepare()
// Set up next handler
app.use(next.getRequestHandler())
// Set up https.Server options with SSL
const options = {
key: fs.readFileSync('./localhost.key'),
cert: fs.readFileSync('./localhost.cert')
}
// Create http server using express app as requestHandler
const server = https.createServer(options, app)
// Set up proxying for Next's onDemandEntries WebSocket to allow
// using our SSL
if (isDev) {
const CreateProxyServer = require('http-proxy').createProxyServer
const proxy = CreateProxyServer({
target: {
host: 'localhost',
port: 3001
}
})
server.on('upgrade', (req, socket, head) => {
proxy.ws(req, socket, head)
})
}
server.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`)
})

View file

@ -1,4 +1,5 @@
[![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-cookie-auth)
# Example app utilizing cookie-based authentication
## How to use
@ -21,13 +22,33 @@ curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2
cd with-cookie-auth
```
Install it and run:
### Run locally
The repository is setup as a [monorepo](https://zeit.co/examples/monorepo/) so you can deploy it easily running `now` inside the project folder. However, you can't run it the same way locally (yet).
These files make it easier to run the application locally and aren't needed for production:
- `/api/index.js` runs the API server on port `3001` and imports the `login` and `profile` microservices.
- `/www/server.js` runs the Next.js app with a custom server proxying the authentication requests to the API server. We use this so we don't modify the logic on the application and we don't have to deal with CORS if we use domains while testing.
Install and run the API server:
```bash
cd api
npm install
npm run dev
```
Then run the Next.js app:
```bash
cd ../www
npm install
npm run dev
```
### Deploy
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
@ -40,8 +61,8 @@ In this example, we authenticate users and store a token in a cookie. The exampl
This example is backend agnostic and uses [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch) to do the API calls on the client and the server.
The repo includes a minimal passwordless backend built with [Micro](https://www.npmjs.com/package/micro) and it logs the user in with a GitHub username and saves the user id from the API call as token.
The repo includes a minimal passwordless backend built with [Micro](https://www.npmjs.com/package/micro) that logs the user in with a GitHub username and saves the user id from the API call as token.
Session is syncronized across tabs. If you logout your session gets logged out on all the windows as well. We use the HOC `withAuthSync` for this.
The helper function `auth` helps to retrieve the token across pages and redirects the user if no token was found.
The helper function `auth` helps to retrieve the token across pages and redirects the user if not token was found.

View file

@ -0,0 +1,20 @@
const { send } = require('micro')
const login = require('./login')
const profile = require('./profile')
const dev = async (req, res) => {
switch (req.url) {
case '/api/profile.js':
await profile(req, res)
break
case '/api/login.js':
await login(req, res)
break
default:
send(res, 404, '404. Not found.')
break
}
}
module.exports = dev

View file

@ -12,7 +12,7 @@
},
"scripts": {
"start": "micro",
"dev": "micro-dev"
"dev": "micro-dev . -p 3001"
},
"keywords": [],
"author": "",

View file

@ -1,7 +1,7 @@
{
"name": "with-cookie-auth",
"scripts": {
"dev": "next",
"dev": "node server.js",
"build": "next build",
"start": "next start"
},
@ -12,5 +12,8 @@
"next-cookies": "^1.0.4",
"react": "^16.7.0",
"react-dom": "^16.7.0"
},
"devDependencies": {
"http-proxy": "^1.17.0"
}
}

View file

@ -1,12 +1,15 @@
import { Component } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/layout'
import { login } from '../utils/auth'
class Login extends Component {
static getInitialProps ({ req }) {
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
const apiUrl = process.browser
? `https://${window.location.host}/api/login.js`
: `https://${req.headers.host}/api/login.js`
? `${protocol}://${window.location.host}/api/login.js`
: `${protocol}://${req.headers.host}/api/login.js`
return { apiUrl }
}
@ -27,9 +30,30 @@ class Login extends Component {
event.preventDefault()
const username = this.state.username
const url = this.props.apiUrl
login({ username, url }).catch(() =>
this.setState({ error: 'Login failed.' })
)
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
if (response.ok) {
const { token } = await response.json()
login({ token })
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
let error = new Error(response.statusText)
error.response = response
return Promise.reject(error)
}
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
throw new Error(error)
}
}
render () {

View file

@ -1,9 +1,10 @@
import Router from 'next/router'
import fetch from 'isomorphic-unfetch'
import nextCookie from 'next-cookies'
import Layout from '../components/layout'
import auth, { withAuthSync } from '../utils/auth'
import { withAuthSync } from '../utils/auth'
const Profile = withAuthSync(props => {
const Profile = props => {
const { name, login, bio, avatarUrl } = props.data
return (
@ -36,13 +37,15 @@ const Profile = withAuthSync(props => {
`}</style>
</Layout>
)
})
}
Profile.getInitialProps = async ctx => {
const token = auth(ctx)
const { token } = nextCookie(ctx)
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
const apiUrl = process.browser
? `https://${window.location.host}/api/profile.js`
: `https://${ctx.req.headers.host}/api/profile.js`
? `${protocol}://${window.location.host}/api/profile.js`
: `${protocol}://${ctx.req.headers.host}/api/profile.js`
const redirectOnError = () =>
process.browser
@ -70,4 +73,4 @@ Profile.getInitialProps = async ctx => {
}
}
export default Profile
export default withAuthSync(Profile)

View file

@ -0,0 +1,49 @@
const { createServer } = require('http')
const httpProxy = require('http-proxy')
const { parse } = require('url')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
const proxy = httpProxy.createProxyServer()
const target = 'http://localhost:3001'
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
const { pathname, query } = parsedUrl
switch (pathname) {
case '/':
app.render(req, res, '/', query)
break
case '/login':
app.render(req, res, '/login', query)
break
case '/api/login.js':
proxy.web(req, res, { target }, error => {
console.log('Error!', error)
})
break
case '/profile':
app.render(req, res, '/profile', query)
break
case '/api/profile.js':
proxy.web(req, res, { target }, error => console.log('Error!', error))
break
default:
handle(req, res, parsedUrl)
break
}
}).listen(3000, err => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})

View file

@ -2,33 +2,10 @@ import { Component } from 'react'
import Router from 'next/router'
import nextCookie from 'next-cookies'
import cookie from 'js-cookie'
import fetch from 'isomorphic-unfetch'
export const login = async ({ username, url }) => {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
if (response.ok) {
const { token } = await response.json()
cookie.set('token', token, { expires: 1 })
Router.push('/profile')
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
let error = new Error(response.statusText)
error.response = response
return Promise.reject(error)
}
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
throw new Error(error)
}
export const login = async ({ token }) => {
cookie.set('token', token, { expires: 1 })
Router.push('/profile')
}
export const logout = () => {
@ -38,13 +15,30 @@ export const logout = () => {
Router.push('/login')
}
export function withAuthSync (WrappedComponent) {
return class extends Component {
// Gets the display name of a JSX component for dev tools
const getDisplayName = Component =>
Component.displayName || Component.name || 'Component'
export const withAuthSync = WrappedComponent =>
class extends Component {
static displayName = `withAuthSync(${getDisplayName(WrappedComponent)})`
static async getInitialProps (ctx) {
const token = auth(ctx)
const componentProps =
WrappedComponent.getInitialProps &&
(await WrappedComponent.getInitialProps(ctx))
return { ...componentProps, token }
}
constructor (props) {
super(props)
this.syncLogout = this.syncLogout.bind(this)
}
componentDidMount () {
window.addEventListener('storage', this.syncLogout)
}
@ -65,9 +59,8 @@ export function withAuthSync (WrappedComponent) {
return <WrappedComponent {...this.props} />
}
}
}
export default ctx => {
export const auth = ctx => {
const { token } = nextCookie(ctx)
if (ctx.req && !token) {

View file

@ -46,6 +46,6 @@ This example shows how to inline env vars.
**Please note**:
* It is a bad practice to commit env vars to a repository. Thats why you should normally [gitignore](https://git-scm.com/docs/gitignore) your `.env` file.
* As soon as you are using an env var in your code it will be publicly available and exposed to the client.
* If you want to have more control of what is exposed to the client check out [tusbar/next-runtime-dotenv](https://github.com/tusbar/next-runtime-dotenv).
* Env vars are set (inlined) at build time. If you need to configure your app on rutime check out [examples/with-universal-configuration-runtime](../with-universal-configuration-runtime)
* In this example, as soon as you reference an env var in your code it will be automatically be publicly available and exposed to the client.
* If you want to have more centralized control of what is exposed to the client check out the example [with-universal-configuration-build-time](../with-universal-configuration-build-time).
* Env vars are set (inlined) at build time. If you need to configure your app on rutime check out [examples/with-universal-configuration-runtime](../with-universal-configuration-runtime).

View file

@ -41,4 +41,4 @@ now
This example shows how to import images, videos, etc. from `/static` and get the URL with a hash query allowing to use better cache without problems.
This example supports `.svg`, `.png` and `.txt` extensions, but it can be configured to support any possible extension changing the `extensions` array in the `.babelrc` file.
This example supports `.svg`, `.png` and `.txt` extensions, but it can be configured to support any possible extension changing the `extensions` array in the `next.config.js` [file](https://github.com/zeit/next.js/blob/canary/examples/with-hashed-statics/next.config.js#L4).

View file

@ -21,7 +21,7 @@ class MyMobxApp extends App {
constructor(props) {
super(props)
const isServer = typeof window === 'undefined'
const isServer = !process.browser;
this.mobxStore = isServer
? props.initialMobxState
: initializeStore(props.initialMobxState)

View file

@ -1,7 +1,7 @@
import { action, observable } from 'mobx'
import { useStaticRendering } from 'mobx-react'
const isServer = typeof window === 'undefined'
const isServer = !process.browser
useStaticRendering(isServer)
class Store {
@ -25,7 +25,8 @@ class Store {
}
let store = null
export function initializeStore(initialData) {
export function initializeStore (initialData) {
// Always make a new store if server, otherwise state is shared between requests
if (isServer) {
return new Store(isServer, initialData)

View file

@ -49,6 +49,6 @@ 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 `<div className={style.example}>`.
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 from '../styles/style.scss'` and use it like `<div className={style.example}>`.
[Learn more](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)

View file

@ -11,13 +11,13 @@
"es6-promise": "4.1.1",
"isomorphic-unfetch": "2.0.0",
"next": "latest",
"next-redux-saga": "3.0.0",
"next-redux-saga": "4.0.0",
"next-redux-wrapper": "2.0.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-redux": "5.0.7",
"redux": "4.0.0",
"redux-saga": "0.16.0"
"redux": "4.0.1",
"redux-saga": "1.0.1"
},
"devDependencies": {
"redux-devtools-extension": "2.13.2"

View file

@ -29,4 +29,4 @@ class MyApp extends App {
}
}
export default withRedux(createStore)(withReduxSaga({ async: true })(MyApp))
export default withRedux(createStore)(withReduxSaga(MyApp))

View file

@ -1,7 +1,6 @@
/* global fetch */
import { delay } from 'redux-saga'
import { all, call, put, take, takeLatest } from 'redux-saga/effects'
import { all, call, delay, put, take, takeLatest } from 'redux-saga/effects'
import es6promise from 'es6-promise'
import 'isomorphic-unfetch'
@ -13,7 +12,7 @@ function * runClockSaga () {
yield take(actionTypes.START_CLOCK)
while (true) {
yield put(tickClock(false))
yield call(delay, 1000)
yield delay(1000)
}
}

View file

@ -1,11 +1,9 @@
import { createStore, applyMiddleware } from 'redux'
import { applyMiddleware, createStore } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer, { exampleInitialState } from './reducer'
import rootSaga from './saga'
const sagaMiddleware = createSagaMiddleware()
const bindMiddleware = middleware => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension')
@ -15,17 +13,15 @@ const bindMiddleware = middleware => {
}
function configureStore (initialState = exampleInitialState) {
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
rootReducer,
initialState,
bindMiddleware([sagaMiddleware])
)
store.runSagaTask = () => {
store.sagaTask = sagaMiddleware.run(rootSaga)
}
store.sagaTask = sagaMiddleware.run(rootSaga)
store.runSagaTask()
return store
}

View file

@ -15,7 +15,7 @@ export default class MyDocument extends Document {
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: [...initialProps.styles, ...sheet.getStyleElement()]
styles: <>{initialProps.styles}{sheet.getStyleElement()}</>
}
} finally {
sheet.seal()

View file

@ -6,7 +6,7 @@ import { findData } from '../utils/sample-api'
import ListDetail from '../components/ListDetail';
type RequestQuery = {
id: number,
id: string,
}
type Props = {

View file

@ -7,6 +7,7 @@
"moduleResolution": "node",
"allowJs": true,
"noEmit": true,
"strict": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"noUnusedLocals": true,

View file

@ -1,6 +0,0 @@
const env = require('./env-config.js')
module.exports = {
presets: ['next/babel'],
plugins: [['transform-define', env]]
}

View file

@ -0,0 +1 @@
TEST=it works!

View file

@ -41,13 +41,13 @@ now
## The idea behind the example
This example shows how to use environment variables and customize one based on NODE_ENV for your application using [transform-define](https://github.com/FormidableLabs/babel-plugin-transform-define)
This example shows how to use environment variables and customize one based on NODE_ENV for your application using `dotenv`, a `.env`-file and `next.config.js`.
When you build your application the environment variable is transformed into a primitive (string or undefined) and can only be changed with a new build. This happens for both client-side and server-side. If the environment variable is used directly in your application it will only have an effect on the server side, not the client side.
To set the environment variables in runtime you can follow the example [with-universal-configuration-runtime]((https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration-runtime))
## Please note
## Caveats
- Because a babel plugin is used the output is cached in `node_modules/.cache` by `babel-loader`. When modifying the configuration you will have to manually clear this cache to make changes visible. Alternately, you may skip caching for `babel-loader` as shown [here](https://github.com/zeit/next.js/issues/1103#issuecomment-279529809).
- This example sets the environment configuration at build time, meaning the same build might not be used in e.g. both staging and production. For a solution which sets the environment at runtime, see [here](https://github.com/zeit/next.js/issues/1488#issuecomment-289108931).
- It is a bad practice to commit env vars to a repository. Thats why you should normally [gitignore](https://git-scm.com/docs/gitignore) your `.env` file.
- Any env var you expose in `next.config.js` will be publicly available and exposed to the client.
- This example sets the environment configuration at build time, meaning the same build might not be used in e.g. both staging and production. For a solution which sets the environment at runtime, see the example [with-universal-configuration-runtime](../with-universal-configuration-runtime).
- If you have many variables in `.env` and want to expose them without listing them all in `next.config.js`, see the example [with-dotenv](../with-dotenv). That example automatically exposes any variable that has been referenced in code, but keeps all other variables secret.

View file

@ -1,8 +0,0 @@
const prod = process.env.NODE_ENV === 'production'
module.exports = {
'process.env.BACKEND_URL': prod
? 'https://api.example.com'
: 'https://localhost:8080',
'process.env.VARIABLE_EXAMPLE': process.env.VARIABLE_EXAMPLE
}

View file

@ -0,0 +1,45 @@
const dotEnvResult = require('dotenv').config()
const prod = process.env.NODE_ENV === 'production'
if (dotEnvResult.error) {
throw dotEnvResult.error
}
module.exports = {
env: {
TEST: process.env.TEST,
BACKEND_URL: prod ? 'https://api.example.com' : 'https://localhost:8080'
}
}
/*
If you want to include every variable in .env automatically,
replace the above module.exports with the code below.
Warning: The below technique can be dangerous since it exposes every
variable in .env to the client. Even if you do not currently have
sensitive information there, it can be easy to forget about this when
you add variables in the future.
If you have many variables and want a safer way to conveniently expose
them, see the example "with-dotenv".
*/
/*
const parsedVariables = dotEnvResult.parsed || {}
const dotEnvVariables = {}
// We always want to use the values from process.env, since dotenv
// has already resolved these correctly in case of overrides
for (const key of Object.keys(parsedVariables)) {
dotEnvVariables[key] = process.env[key]
}
module.exports = {
env: {
...dotEnvVariables,
BACKEND_URL: prod ? 'https://api.example.com' : 'https://localhost:8080'
}
}
*/

View file

@ -12,7 +12,7 @@
"react-dom": "^16.7.0"
},
"devDependencies": {
"babel-plugin-transform-define": "^1.3.0"
"dotenv": "^6.2.0"
},
"license": "ISC"
}

View file

@ -1,6 +1,6 @@
export default () => (
<div>
<p>Environment variable process.env.VARIABLE_EXAMPLE is "{process.env.VARIABLE_EXAMPLE}"</p>
<p>Environment variable process.env.TEST is "{process.env.TEST}"</p>
<p>Custom environment variables process.env.BACKEND_URL is "{process.env.BACKEND_URL}"</p>
</div>
)

View file

@ -17,5 +17,5 @@
"registry": "https://registry.npmjs.org/"
}
},
"version": "8.0.0"
"version": "8.0.2-canary.3"
}

View file

@ -6,10 +6,9 @@
],
"scripts": {
"lerna": "lerna",
"bootstrap": "lerna bootstrap",
"dev": "lerna run build --stream --parallel",
"testonly": "jest",
"testall": "npm run testonly -- --coverage --forceExit --runInBand --reporters=default --reporters=jest-junit",
"testall": "yarn check && npm run testonly -- --coverage --forceExit --runInBand --reporters=default --reporters=jest-junit",
"pretest": "npm run lint",
"test": "npm run testall || npm run testall",
"coveralls": "cat ./test/coverage/lcov.info | coveralls",
@ -61,6 +60,7 @@
"@zeit/next-css": "1.0.2-canary.2",
"@zeit/next-sass": "1.0.2-canary.2",
"@zeit/next-typescript": "1.1.2-canary.0",
"amphtml-validator": "1.0.23",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "9.0.0",
"babel-jest": "23.6.0",
@ -68,6 +68,7 @@
"chromedriver": "2.42.0",
"clone": "2.1.1",
"coveralls": "3.0.2",
"eslint": "5.13.0",
"eslint-plugin-typescript": "0.14.0",
"express": "4.16.3",
"fkill": "5.1.0",
@ -83,9 +84,10 @@
"node-sass": "4.9.2",
"pre-commit": "1.2.2",
"prettier": "1.15.3",
"react": "16.6.3",
"react-dom": "16.6.3",
"react": "16.8.0",
"react-dom": "16.8.0",
"release": "5.0.3",
"request-promise-core": "1.1.1",
"rimraf": "2.6.2",
"standard": "11.0.1",
"taskr": "1.1.0",

View file

@ -0,0 +1 @@
module.exports = require('./dist/lib/amp')

View file

@ -0,0 +1,6 @@
import React from 'react'
import {IsAmpContext} from './amphtml-context'
export function useAmp() {
return React.useContext(IsAmpContext)
}

View file

@ -0,0 +1,3 @@
import * as React from 'react'
export const IsAmpContext: React.Context<any> = React.createContext(false)

View file

@ -25,7 +25,7 @@ import React from 'react'
import PropTypes from 'prop-types'
const ALL_INITIALIZERS = []
const READY_INITIALIZERS = new Map()
const READY_INITIALIZERS = []
let initialized = false
function load (loader) {
@ -138,11 +138,13 @@ function createLoadableComponent (loadFn, options) {
// Client only
if (!initialized && typeof window !== 'undefined' && typeof opts.webpack === 'function') {
const moduleIds = opts.webpack()
for (const moduleId of moduleIds) {
READY_INITIALIZERS.set(moduleId, () => {
return init()
})
}
READY_INITIALIZERS.push((ids) => {
for (const moduleId of moduleIds) {
if (ids.indexOf(moduleId) !== -1) {
return init()
}
}
})
}
return class LoadableComponent extends React.Component {
@ -273,17 +275,17 @@ function LoadableMap (opts) {
Loadable.Map = LoadableMap
function flushInitializers (initializers) {
function flushInitializers (initializers, ids) {
let promises = []
while (initializers.length) {
let init = initializers.pop()
promises.push(init())
promises.push(init(ids))
}
return Promise.all(promises).then(() => {
if (initializers.length) {
return flushInitializers(initializers)
return flushInitializers(initializers, ids)
}
})
}
@ -294,24 +296,14 @@ Loadable.preloadAll = () => {
})
}
Loadable.preloadReady = (webpackIds) => {
return new Promise((resolve, reject) => {
const initializers = webpackIds.reduce((allInitalizers, moduleId) => {
const initializer = READY_INITIALIZERS.get(moduleId)
if (!initializer) {
return allInitalizers
}
allInitalizers.push(initializer)
return allInitalizers
}, [])
initialized = true
// Make sure the object is cleared
READY_INITIALIZERS.clear()
Loadable.preloadReady = (ids) => {
return new Promise((resolve) => {
const res = () => {
initialized = true
return resolve()
}
// We always will resolve, errors should be handled within loading UIs.
flushInitializers(initializers).then(resolve, resolve)
flushInitializers(READY_INITIALIZERS, ids).then(res, res)
})
}

View file

@ -1,6 +1,6 @@
{
"name": "next-server",
"version": "8.0.0",
"version": "8.0.2-canary.3",
"main": "./index.js",
"license": "MIT",
"files": [
@ -12,7 +12,8 @@
"head.js",
"link.js",
"router.js",
"next-config.js"
"next-config.js",
"amp.js"
],
"scripts": {
"build": "taskr",

View file

@ -1,9 +1,10 @@
import findUp from 'find-up'
import {CONFIG_FILE} from 'next-server/constants'
const targets = ['server', 'serverless']
const targets = ['server', 'serverless', 'unified']
const defaultConfig = {
env: [],
webpack: null,
webpackDevMiddleware: null,
poweredByHeader: true,
@ -21,6 +22,9 @@ const defaultConfig = {
websocketPort: 0,
websocketProxyPath: '/',
websocketProxyPort: null
},
experimental: {
amp: false
}
}
@ -47,6 +51,12 @@ export default function loadConfig (phase, dir, customConfig) {
if (userConfig.target && !targets.includes(userConfig.target)) {
throw new Error(`Specified target is invalid. Provided: "${userConfig.target}" should be one of ${targets.join(', ')}`)
}
if (userConfig.experimental) {
userConfig.experimental = {
...defaultConfig.experimental,
...userConfig.experimental
}
}
if (userConfig.onDemandEntries) {
userConfig.onDemandEntries = {
...defaultConfig.onDemandEntries,

View file

@ -30,6 +30,7 @@ export default class Server {
distDir: string
buildId: string
renderOpts: {
ampEnabled: boolean,
staticMarkup: boolean,
buildId: string,
generateEtags: boolean,
@ -53,6 +54,7 @@ export default class Server {
this.buildId = this.readBuildId()
this.renderOpts = {
ampEnabled: this.nextConfig.experimental.amp,
staticMarkup,
buildId: this.buildId,
generateEtags,
@ -160,6 +162,29 @@ export default class Server {
]
if (this.nextConfig.useFileSystemPublicRoutes) {
if (this.nextConfig.experimental.amp) {
// It's very important to keep this route's param optional.
// (but it should support as many params as needed, separated by '/')
// Otherwise this will lead to a pretty simple DOS attack.
// See more: https://github.com/zeit/next.js/issues/2617
routes.push({
match: route('/:path*/amp'),
fn: async (req, res, params, parsedUrl) => {
let pathname
if (!params.path) {
pathname = '/'
} else {
pathname = '/' + params.path.join('/')
}
const { query } = parsedUrl
if (!pathname) {
throw new Error('pathname is undefined')
}
await this.renderToAMP(req, res, pathname, query, parsedUrl)
},
})
}
// It's very important to keep this route's param optional.
// (but it should support as many params as needed, separated by '/')
// Otherwise this will lead to a pretty simple DOS attack.
@ -223,6 +248,31 @@ export default class Server {
return
}
if (this.nextConfig.poweredByHeader) {
res.setHeader('X-Powered-By', 'Next.js ' + process.env.__NEXT_VERSION)
}
return this.sendHTML(req, res, html)
}
public async renderToAMP(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}, parsedUrl?: UrlWithParsedQuery): Promise<void> {
if (!this.nextConfig.experimental.amp) {
throw new Error('"experimental.amp" is not enabled in "next.config.js"')
}
const url: any = req.url
if (isInternalUrl(url)) {
return this.handleRequest(req, res, parsedUrl)
}
if (isBlockedPage(pathname)) {
return this.render404(req, res, parsedUrl)
}
const html = await this.renderToAMPHTML(req, res, pathname, query)
// Request was ended by the user
if (html === null) {
return
}
if (this.nextConfig.poweredByHeader) {
res.setHeader('X-Powered-By', 'Next.js ' + process.env.NEXT_VERSION)
}
@ -234,10 +284,17 @@ export default class Server {
return renderToHTML(req, res, pathname, query, {...result, ...opts})
}
public async renderToHTML(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}): Promise<string|null> {
public async renderToAMPHTML(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}): Promise<string|null> {
if (!this.nextConfig.experimental.amp) {
throw new Error('"experimental.amp" is not enabled in "next.config.js"')
}
return this.renderToHTML(req, res, pathname, query, {amphtml: true})
}
public async renderToHTML(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}, {amphtml}: {amphtml?: boolean} = {}): Promise<string|null> {
try {
// To make sure the try/catch is executed
const html = await this.renderToHTMLWithComponents(req, res, pathname, query, this.renderOpts)
const html = await this.renderToHTMLWithComponents(req, res, pathname, query, {...this.renderOpts, amphtml})
return html
} catch (err) {
if (err.code === 'ENOENT') {

View file

@ -1,4 +1,4 @@
import {IncomingMessage, ServerResponse} from 'http'
import { IncomingMessage, ServerResponse } from 'http'
import { ParsedUrlQuery } from 'querystring'
import React from 'react'
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
@ -7,15 +7,26 @@ import { loadGetInitialProps, isResSent } from '../lib/utils'
import Head, { defaultHead } from '../lib/head'
import Loadable from '../lib/loadable'
import LoadableCapture from '../lib/loadable-capture'
import {getDynamicImportBundles, Manifest as ReactLoadableManifest, ManifestItem} from './get-dynamic-import-bundles'
import {getPageFiles, BuildManifest} from './get-page-files'
import {
getDynamicImportBundles,
Manifest as ReactLoadableManifest,
ManifestItem,
} from './get-dynamic-import-bundles'
import { getPageFiles, BuildManifest } from './get-page-files'
import { IsAmpContext } from '../lib/amphtml-context'
type Enhancer = (Component: React.ComponentType) => React.ComponentType
type ComponentsEnhancer = {enhanceApp?: Enhancer, enhanceComponent?: Enhancer}|Enhancer
type ComponentsEnhancer =
| { enhanceApp?: Enhancer; enhanceComponent?: Enhancer }
| Enhancer
function enhanceComponents(options: ComponentsEnhancer, App: React.ComponentType, Component: React.ComponentType): {
function enhanceComponents(
options: ComponentsEnhancer,
App: React.ComponentType,
Component: React.ComponentType,
): {
App: React.ComponentType
Component: React.ComponentType,
} {
// For backwards compatibility
if (typeof options === 'function') {
@ -27,11 +38,16 @@ function enhanceComponents(options: ComponentsEnhancer, App: React.ComponentType
return {
App: options.enhanceApp ? options.enhanceApp(App) : App,
Component: options.enhanceComponent ? options.enhanceComponent(Component) : Component,
Component: options.enhanceComponent
? options.enhanceComponent(Component)
: Component,
}
}
function render(renderElementToString: (element: React.ReactElement<any>) => string, element: React.ReactElement<any>): {html: string, head: any} {
function render(
renderElementToString: (element: React.ReactElement<any>) => string,
element: React.ReactElement<any>,
): { html: string; head: any } {
let html
let head
@ -45,75 +61,101 @@ function render(renderElementToString: (element: React.ReactElement<any>) => str
}
type RenderOpts = {
staticMarkup: boolean,
buildId: string,
runtimeConfig?: {[key: string]: any},
assetPrefix?: string,
err?: Error|null,
nextExport?: boolean,
dev?: boolean,
buildManifest: BuildManifest,
reactLoadableManifest: ReactLoadableManifest,
Component: React.ComponentType,
Document: React.ComponentType,
App: React.ComponentType,
ErrorDebug?: React.ComponentType<{error: Error}>,
ampEnabled: boolean
staticMarkup: boolean
buildId: string
runtimeConfig?: { [key: string]: any }
assetPrefix?: string
err?: Error | null
nextExport?: boolean
dev?: boolean
amphtml?: boolean
buildManifest: BuildManifest
reactLoadableManifest: ReactLoadableManifest
Component: React.ComponentType
Document: React.ComponentType
App: React.ComponentType
ErrorDebug?: React.ComponentType<{ error: Error }>,
}
function renderDocument(Document: React.ComponentType, {
props,
docProps,
pathname,
query,
buildId,
assetPrefix,
runtimeConfig,
nextExport,
dynamicImportsIds,
err,
dev,
staticMarkup,
devFiles,
files,
dynamicImports,
}: RenderOpts & {
props: any,
docProps: any,
pathname: string,
query: ParsedUrlQuery,
dynamicImportsIds: string[],
dynamicImports: ManifestItem[],
files: string[]
devFiles: string[],
}): string {
return '<!DOCTYPE html>' + renderToStaticMarkup(
<Document
__NEXT_DATA__={{
props, // The result of getInitialProps
page: pathname, // The rendered page
query, // querystring parsed / passed by the user
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
nextExport, // If this is a page exported by `next export`
dynamicIds: dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
err: (err) ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
}}
staticMarkup={staticMarkup}
devFiles={devFiles}
files={files}
dynamicImports={dynamicImports}
assetPrefix={assetPrefix}
{...docProps}
/>,
function renderDocument(
Document: React.ComponentType,
{
ampEnabled = false,
props,
docProps,
pathname,
asPath,
query,
buildId,
assetPrefix,
runtimeConfig,
nextExport,
dynamicImportsIds,
err,
dev,
amphtml,
staticMarkup,
devFiles,
files,
dynamicImports,
}: RenderOpts & {
props: any
docProps: any
pathname: string
asPath: string | undefined
query: ParsedUrlQuery
amphtml: boolean
dynamicImportsIds: string[]
dynamicImports: ManifestItem[]
files: string[]
devFiles: string[],
},
): string {
return (
'<!DOCTYPE html>' +
renderToStaticMarkup(
<IsAmpContext.Provider value={amphtml}>
<Document
__NEXT_DATA__={{
props, // The result of getInitialProps
page: pathname, // The rendered page
query, // querystring parsed / passed by the user
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
nextExport, // If this is a page exported by `next export`
dynamicIds:
dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
}}
ampEnabled={ampEnabled}
asPath={encodeURI(asPath || '')}
amphtml={amphtml}
staticMarkup={staticMarkup}
devFiles={devFiles}
files={files}
dynamicImports={dynamicImports}
assetPrefix={assetPrefix}
{...docProps}
/>
</IsAmpContext.Provider>,
)
)
}
export async function renderToHTML(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery, renderOpts: RenderOpts): Promise<string|null> {
export async function renderToHTML(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: ParsedUrlQuery,
renderOpts: RenderOpts,
): Promise<string | null> {
const {
err,
dev = false,
staticMarkup = false,
amphtml = false,
App,
Document,
Component,
@ -127,22 +169,28 @@ export async function renderToHTML(req: IncomingMessage, res: ServerResponse, pa
if (dev) {
const { isValidElementType } = require('react-is')
if (!isValidElementType(Component)) {
throw new Error(`The default export is not a React Component in page: "${pathname}"`)
throw new Error(
`The default export is not a React Component in page: "${pathname}"`,
)
}
if (!isValidElementType(App)) {
throw new Error(`The default export is not a React Component in page: "/_app"`)
throw new Error(
`The default export is not a React Component in page: "/_app"`,
)
}
if (!isValidElementType(Document)) {
throw new Error(`The default export is not a React Component in page: "/_document"`)
throw new Error(
`The default export is not a React Component in page: "/_document"`,
)
}
}
const asPath = req.url
const ctx = { err, req, res, pathname, query, asPath }
const router = new Router(pathname, query, asPath)
const props = await loadGetInitialProps(App, {Component, router, ctx})
const props = await loadGetInitialProps(App, { Component, router, ctx })
// the response might be finished on the getInitialProps call
if (isResSent(res)) return null
@ -156,23 +204,35 @@ export async function renderToHTML(req: IncomingMessage, res: ServerResponse, pa
]
const reactLoadableModules: string[] = []
const renderPage = (options: ComponentsEnhancer = {}): {html: string, head: any} => {
const renderElementToString = staticMarkup ? renderToStaticMarkup : renderToString
const renderPage = (
options: ComponentsEnhancer = {},
): { html: string; head: any } => {
const renderElementToString = staticMarkup
? renderToStaticMarkup
: renderToString
if (err && ErrorDebug) {
return render(renderElementToString, <ErrorDebug error={err} />)
}
const {App: EnhancedApp, Component: EnhancedComponent} = enhanceComponents(options, App, Component)
const {
App: EnhancedApp,
Component: EnhancedComponent,
} = enhanceComponents(options, App, Component)
return render(renderElementToString,
<LoadableCapture report={(moduleName) => reactLoadableModules.push(moduleName)}>
<EnhancedApp
Component={EnhancedComponent}
router={router}
{...props}
/>
</LoadableCapture>,
return render(
renderElementToString,
<IsAmpContext.Provider value={amphtml}>
<LoadableCapture
report={(moduleName) => reactLoadableModules.push(moduleName)}
>
<EnhancedApp
Component={EnhancedComponent}
router={router}
{...props}
/>
</LoadableCapture>
</IsAmpContext.Provider>,
)
}
@ -180,14 +240,18 @@ export async function renderToHTML(req: IncomingMessage, res: ServerResponse, pa
// the response might be finished on the getInitialProps call
if (isResSent(res)) return null
const dynamicImports = [...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules)]
const dynamicImports = [
...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules),
]
const dynamicImportsIds: any = dynamicImports.map((bundle) => bundle.id)
return renderDocument(Document, {
...renderOpts,
props,
docProps,
asPath,
pathname,
amphtml,
query,
dynamicImportsIds,
dynamicImports,
@ -201,10 +265,17 @@ function errorToJSON(err: Error): Error {
return { name, message, stack }
}
function serializeError(dev: boolean|undefined, err: Error): Error & {statusCode?: number} {
function serializeError(
dev: boolean | undefined,
err: Error,
): Error & { statusCode?: number } {
if (dev) {
return errorToJSON(err)
}
return { name: 'Internal Server Error.', message: '500 - Internal Server Error.', statusCode: 500 }
return {
name: 'Internal Server Error.',
message: '500 - Internal Server Error.',
statusCode: 500,
}
}

View file

@ -4,7 +4,7 @@ import pathMatch from './lib/path-match'
export const route = pathMatch()
type Params = {[param: string]: string}
type Params = {[param: string]: any}
export type Route = {
match: (pathname: string|undefined) => false|Params,

View file

@ -35,7 +35,7 @@ try {
}
// update file's data
file.data = Buffer.from(result.outputText.replace(/process\.env\.NEXT_VERSION/, `"${require('./package.json').version}"`), 'utf8')
file.data = Buffer.from(result.outputText.replace(/process\.env\.__NEXT_VERSION/, `"${require('./package.json').version}"`), 'utf8')
})
}
} catch (err) {

View file

@ -115,7 +115,11 @@ After that, the file-system is the main API. Every `.js` file becomes a route th
Populate `./pages/index.js` inside your project:
```jsx
export default () => <div>Welcome to next.js!</div>
function Home() {
return <div>Welcome to next.js!</div>
}
export default Home
```
and then just run `npm run dev` and go to `http://localhost:3000`. To use another port, you can run `npm run dev -- -p <your port here>`.
@ -136,11 +140,15 @@ Every `import` you declare gets bundled and served with each page. That means pa
```jsx
import cowsay from 'cowsay-browser'
export default () => (
<pre>
{cowsay.say({ text: 'hi there!' })}
</pre>
)
function CowsayHi() {
return (
<pre>
{cowsay.say({ text: 'hi there!' })}
</pre>
)
}
export default CowsayHi
```
### CSS
@ -159,30 +167,34 @@ export default () => (
We bundle [styled-jsx](https://github.com/zeit/styled-jsx) to provide support for isolated scoped CSS. The aim is to support "shadow CSS" similar to Web Components, which unfortunately [do not support server-rendering and are JS-only](https://github.com/w3c/webcomponents/issues/71).
```jsx
export default () => (
<div>
Hello world
<p>scoped!</p>
<style jsx>{`
p {
color: blue;
}
div {
background: red;
}
@media (max-width: 600px) {
div {
background: blue;
function HelloWorld() {
return (
<div>
Hello world
<p>scoped!</p>
<style jsx>{`
p {
color: blue;
}
}
`}</style>
<style global jsx>{`
body {
background: black;
}
`}</style>
</div>
)
div {
background: red;
}
@media (max-width: 600px) {
div {
background: blue;
}
}
`}</style>
<style global jsx>{`
body {
background: black;
}
`}</style>
</div>
)
}
export default HelloWorld
```
Please see the [styled-jsx documentation](https://www.npmjs.com/package/styled-jsx) for more examples.
@ -208,10 +220,14 @@ Please see the [styled-jsx documentation](https://www.npmjs.com/package/styled-j
It's possible to use any existing CSS-in-JS solution. The simplest one is inline styles:
```jsx
export default () => <p style={{ color: 'red' }}>hi there</p>
function HiThere() {
return <p style={{ color: 'red' }}>hi there</p>
}
export default HiThere
```
To use more sophisticated CSS-in-JS solutions, you typically have to implement style flushing for server-side rendering. We enable this by allowing you to define your own [custom `<Document>`](#user-content-custom-document) component that wraps each page.
To use more sophisticated CSS-in-JS solutions, you typically have to implement style flushing for server-side rendering. We enable this by allowing you to define your own [custom `<Document>`](#custom-document) component that wraps each page.
#### Importing CSS / Sass / Less / Stylus files
@ -227,7 +243,11 @@ To support importing `.css`, `.scss`, `.less` or `.styl` files you can use these
Create a folder called `static` in your project root directory. From your code you can then reference those files with `/static/` URLs:
```jsx
export default () => <img src="/static/my-image.png" alt="my image" />
function MyImage() {
return <img src="/static/my-image.png" alt="my image" />
}
export default MyImage
```
_Note: Don't name the `static` directory anything else. The name is required and is the only directory that Next.js uses for serving static assets._
@ -249,15 +269,19 @@ We expose a built-in component for appending elements to the `<head>` of the pag
```jsx
import Head from 'next/head'
export default () => (
<div>
<Head>
<title>My page title</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<p>Hello world!</p>
</div>
)
function IndexPage() {
return (
<div>
<Head>
<title>My page title</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<p>Hello world!</p>
</div>
)
}
export default IndexPage
```
To avoid duplicate tags in your `<head>` you can use the `key` property, which will make sure the tag is only rendered once:
@ -265,18 +289,22 @@ To avoid duplicate tags in your `<head>` you can use the `key` property, which w
```jsx
import Head from 'next/head'
export default () => (
<div>
<Head>
<title>My page title</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" key="viewport" />
</Head>
<Head>
<meta name="viewport" content="initial-scale=1.2, width=device-width" key="viewport" />
</Head>
<p>Hello world!</p>
</div>
)
function IndexPage() {
return (
<div>
<Head>
<title>My page title</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" key="viewport" />
</Head>
<Head>
<meta name="viewport" content="initial-scale=1.2, width=device-width" key="viewport" />
</Head>
<p>Hello world!</p>
</div>
)
}
export default IndexPage
```
In this case only the second `<meta name="viewport" />` is rendered.
@ -301,7 +329,7 @@ When you need state, lifecycle hooks or **initial data population** you can expo
```jsx
import React from 'react'
export default class extends React.Component {
class HelloUA extends React.Component {
static async getInitialProps({ req }) {
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
return { userAgent }
@ -315,6 +343,8 @@ export default class extends React.Component {
)
}
}
export default HelloUA
```
Notice that to load data when the page loads, we use `getInitialProps` which is an [`async`](https://zeit.co/blog/async-and-await) static method. It can asynchronously fetch anything that resolves to a JavaScript plain `Object`, which populates `props`.
@ -335,10 +365,9 @@ _Note: `getInitialProps` can **not** be used in children components. Only in `pa
You can also define the `getInitialProps` lifecycle method for stateless components:
```jsx
const Page = ({ stars }) =>
<div>
Next stars: {stars}
</div>
function Page({ stars }) {
return <div>Next stars: {stars}</div>
}
Page.getInitialProps = async ({ req }) => {
const res = await fetch('https://api.github.com/repos/zeit/next.js')
@ -384,20 +413,28 @@ Consider these two pages:
// pages/index.js
import Link from 'next/link'
export default () => (
<div>
Click{' '}
<Link href="/about">
<a>here</a>
</Link>{' '}
to read more
</div>
)
function Home() {
return (
<div>
Click{' '}
<Link href="/about">
<a>here</a>
</Link>{' '}
to read more
</div>
)
}
export default Home
```
```jsx
// pages/about.js
export default () => <p>Welcome to About!</p>
function About() {
return <p>Welcome to About!</p>
}
export default About
```
**Custom routes (using props from URL)**
@ -414,7 +451,7 @@ Example:
2. You created the `pages/post.js`
```jsx
export default class extends React.Component {
class Post extends React.Component {
static async getInitialProps({query}) {
console.log('SLUG', query.slug)
return {}
@ -423,6 +460,8 @@ Example:
return <h1>My blog post</h1>
}
}
export default Post
```
3. You add the route to `express` (or any other server) on `server.js` file (this is only for SSR). This will route the url `/post/:slug` to `pages/post.js` and provide `slug` as part of query in getInitialProps.
@ -463,15 +502,19 @@ The component `<Link>` can also receive an URL object and it will automatically
// pages/index.js
import Link from 'next/link'
export default () => (
<div>
Click{' '}
<Link href={{ pathname: '/about', query: { name: 'Zeit' } }}>
<a>here</a>
</Link>{' '}
to read more
</div>
)
function Home() {
return (
<div>
Click{' '}
<Link href={{ pathname: '/about', query: { name: 'Zeit' } }}>
<a>here</a>
</Link>{' '}
to read more
</div>
)
}
export default Home
```
That will generate the URL string `/about?name=Zeit`, you can use every property as defined in the [Node.js URL module documentation](https://nodejs.org/api/url.html#url_url_strings_and_url_objects).
@ -484,15 +527,19 @@ The default behaviour for the `<Link>` component is to `push` a new url into the
// pages/index.js
import Link from 'next/link'
export default () => (
<div>
Click{' '}
<Link href="/about" replace>
<a>here</a>
</Link>{' '}
to read more
</div>
)
function Home() {
return (
<div>
Click{' '}
<Link href="/about" replace>
<a>here</a>
</Link>{' '}
to read more
</div>
)
}
export default Home
```
##### Using a component that supports `onClick`
@ -503,14 +550,18 @@ export default () => (
// pages/index.js
import Link from 'next/link'
export default () => (
<div>
Click{' '}
<Link href="/about">
<img src="/static/image.png" alt="image" />
</Link>
</div>
)
function Home() {
return (
<div>
Click{' '}
<Link href="/about">
<img src="/static/image.png" alt="image" />
</Link>
</div>
)
}
export default Home
```
##### Forcing the Link to expose `href` to its child
@ -523,13 +574,17 @@ If child is an `<a>` tag and doesn't have a href attribute we specify it so that
import Link from 'next/link'
import Unexpected_A from 'third-library'
export default ({ href, name }) => (
<Link href={href} passHref>
<Unexpected_A>
{name}
</Unexpected_A>
</Link>
)
function NavLink({ href, name }) {
return (
<Link href={href} passHref>
<Unexpected_A>
{name}
</Unexpected_A>
</Link>
)
}
export default NavLink
```
##### Disabling the scroll changes to top on page
@ -558,11 +613,15 @@ You can also do client-side page transitions using the `next/router`
```jsx
import Router from 'next/router'
export default () => (
<div>
Click <span onClick={() => Router.push('/about')}>here</span> to read more
</div>
)
function ReadMore() {
return (
<div>
Click <span onClick={() => Router.push('/about')}>here</span> to read more
</div>
)
}
export default ReadMore
```
#### Intercepting `popstate`
@ -615,11 +674,15 @@ const handler = () => {
})
}
export default () => (
<div>
Click <span onClick={handler}>here</span> to read more
</div>
)
function ReadMore() {
return (
<div>
Click <span onClick={handler}>here</span> to read more
</div>
)
}
export default ReadMore
```
This uses the same exact parameters as in the `<Link>` component.
@ -724,7 +787,7 @@ If you want to access the `router` object inside any component in your app, you
```jsx
import { withRouter } from 'next/router'
const ActiveLink = ({ children, router, href }) => {
function ActiveLink({ children, router, href }) {
const style = {
marginRight: 10,
color: router.pathname === href ? 'red' : 'black'
@ -773,28 +836,31 @@ You can add `prefetch` prop to any `<Link>` and Next.js will prefetch those page
```jsx
import Link from 'next/link'
// example header component
export default () => (
<nav>
<ul>
<li>
<Link prefetch href="/">
<a>Home</a>
</Link>
</li>
<li>
<Link prefetch href="/about">
<a>About</a>
</Link>
</li>
<li>
<Link prefetch href="/contact">
<a>Contact</a>
</Link>
</li>
</ul>
</nav>
)
function Header() {
return (
<nav>
<ul>
<li>
<Link prefetch href="/">
<a>Home</a>
</Link>
</li>
<li>
<Link prefetch href="/about">
<a>About</a>
</Link>
</li>
<li>
<Link prefetch href="/contact">
<a>Contact</a>
</Link>
</li>
</ul>
</nav>
)
}
export default Header
```
#### Imperatively
@ -804,15 +870,19 @@ Most prefetching needs are addressed by `<Link />`, but we also expose an impera
```jsx
import { withRouter } from 'next/router'
export default withRouter(({ router }) => (
<div>
<a onClick={() => setTimeout(() => router.push('/dynamic'), 100)}>
A route transition will happen after 100ms
</a>
{// but we can prefetch it!
router.prefetch('/dynamic')}
</div>
)
function MyLink({ router }) {
return (
<div>
<a onClick={() => setTimeout(() => router.push('/dynamic'), 100)}>
A route transition will happen after 100ms
</a>
{// but we can prefetch it!
router.prefetch('/dynamic')}
</div>
)
}
export default withRouter(MyLink)
```
The router instance should be only used inside the client side of your app though. In order to prevent any error regarding this subject, when rendering the Router on the server side, use the imperatively prefetch method in the `componentDidMount()` lifecycle method.
@ -1004,13 +1074,17 @@ import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() => import('../components/hello'))
export default () => (
<div>
<Header />
<DynamicComponent />
<p>HOME PAGE is here!</p>
</div>
)
function Home() {
return (
<div>
<Header />
<DynamicComponent />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
```
#### 2. With Custom Loading Component
@ -1022,13 +1096,17 @@ const DynamicComponentWithCustomLoading = dynamic(() => import('../components/he
loading: () => <p>...</p>
})
export default () => (
<div>
<Header />
<DynamicComponentWithCustomLoading />
<p>HOME PAGE is here!</p>
</div>
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithCustomLoading />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
```
#### 3. With No SSR
@ -1040,13 +1118,17 @@ const DynamicComponentWithNoSSR = dynamic(() => import('../components/hello3'),
ssr: false
})
export default () => (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
```
#### 4. With Multiple Modules At Once
@ -1073,7 +1155,11 @@ const HelloBundle = dynamic({
</div>
})
export default () => <HelloBundle title="Dynamic Bundle" />
function DynamicBundle() {
return <HelloBundle title="Dynamic Bundle" />
}
export default DynamicBundle
```
### Custom `<App>`
@ -1101,7 +1187,7 @@ To override, create the `./pages/_app.js` file and override the App class as sho
import React from 'react'
import App, { Container } from 'next/app'
export default class MyApp extends App {
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {}
@ -1122,6 +1208,8 @@ export default class MyApp extends App {
)
}
}
export default MyApp
```
### Custom `<Document>`
@ -1149,7 +1237,7 @@ Pages in `Next.js` skip the definition of the surrounding document's markup. For
// ./pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
@ -1169,6 +1257,8 @@ export default class MyDocument extends Document {
)
}
}
export default MyDocument
```
All of `<Head />`, `<Main />` and `<NextScript />` are required for page to be properly rendered.
@ -1188,7 +1278,7 @@ that need to wrap the application to properly work with server-rendering. 🚧
```js
import Document from 'next/document'
export default class MyDocument extends Document {
class MyDocument extends Document {
static async getInitialProps(ctx) {
const originalRenderPage = ctx.renderPage
@ -1205,6 +1295,8 @@ export default class MyDocument extends Document {
return initialProps
}
}
export default MyDocument
```
### Custom error handling
@ -1216,7 +1308,7 @@ export default class MyDocument extends Document {
```jsx
import React from 'react'
export default class Error extends React.Component {
class Error extends React.Component {
static getInitialProps({ res, err }) {
const statusCode = res ? res.statusCode : err ? err.statusCode : null;
return { statusCode }
@ -1232,6 +1324,8 @@ export default class Error extends React.Component {
)
}
}
export default Error
```
### Reusing the built-in error page
@ -1243,7 +1337,7 @@ import React from 'react'
import Error from 'next/error'
import fetch from 'isomorphic-unfetch'
export default class Page extends React.Component {
class Page extends React.Component {
static async getInitialProps() {
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const errorCode = res.statusCode > 200 ? res.statusCode : false
@ -1264,6 +1358,8 @@ export default class Page extends React.Component {
)
}
}
export default Page
```
> If you have created a custom error page you have to import your own `_error` component from `./_error` instead of `next/error`
@ -1479,7 +1575,7 @@ Example usage of `defaultLoaders.babel`:
// This source was taken from the @zeit/next-mdx plugin source:
// https://github.com/zeit/next-plugins/blob/master/packages/next-mdx
module.exports = {
webpack: (config, {}) => {
webpack: (config, options) => {
config.module.rules.push({
test: /\.mdx/,
use: [
@ -1577,19 +1673,21 @@ This will allow you to use `process.env.customKey` in your code. For example:
```jsx
// pages/index.js
export default function Index() {
function Index() {
return <h1>The value of customEnv is: {process.env.customEnv}</h1>
}
export default Index
```
#### Runtime configuration
> :warning: Note that this option is not available when using `target: 'serverless'`
> :warning: Generally you want to use build-time configuration to provide your configuration.
> :warning: Generally you want to use build-time configuration to provide your configuration.
The reason for this is that runtime configuration adds a small rendering / initialization overhead.
The `next/config` module gives your app access to the `publicRuntimeConfig` and `serverRuntimeConfig` stored in your `next.config.js`.
The `next/config` module gives your app access to the `publicRuntimeConfig` and `serverRuntimeConfig` stored in your `next.config.js`.
Place any server-only runtime config under a `serverRuntimeConfig` property.
@ -1617,9 +1715,15 @@ const {serverRuntimeConfig, publicRuntimeConfig} = getConfig()
console.log(serverRuntimeConfig.mySecret) // Will only be available on the server side
console.log(publicRuntimeConfig.staticFolder) // Will be available on both server and client
export default () => <div>
<img src={`${publicRuntimeConfig.staticFolder}/logo.png`} alt="logo" />
</div>
function MyImage() {
return (
<div>
<img src={`${publicRuntimeConfig.staticFolder}/logo.png`} alt="logo" />
</div>
)
}
export default MyImage
```
### Starting the server on alternative hostname
@ -1638,7 +1742,7 @@ module.exports = {
}
```
Note: Next.js will automatically use that prefix in the scripts it loads, but this has no effect whatsoever on `/static`. If you want to serve those assets over the CDN, you'll have to introduce the prefix yourself. One way of introducing a prefix that works inside your components and varies by environment is documented [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration).
Note: Next.js will automatically use that prefix in the scripts it loads, but this has no effect whatsoever on `/static`. If you want to serve those assets over the CDN, you'll have to introduce the prefix yourself. One way of introducing a prefix that works inside your components and varies by environment is documented [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration-build-time).
If your CDN is on a separate domain and you would like assets to be requested using a [CORS aware request](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) you can set a config option for that.

1
packages/next/amp.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('next-server/amp')

View file

@ -4,6 +4,7 @@ import arg from 'arg'
import { existsSync } from 'fs'
import startServer from '../server/lib/start-server'
import { printAndExit } from '../server/lib/utils'
import { startedDevelopmentServer } from '../build/output'
const args = arg({
// Types
@ -55,10 +56,12 @@ if (!existsSync(join(dir, 'pages'))) {
}
const port = args['--port'] || 3000
const appUrl = `http://${args['--hostname'] || 'localhost'}:${port}`
startedDevelopmentServer(appUrl)
startServer({dir, dev: true}, port, args['--hostname'])
.then(async (app) => {
// tslint:disable-next-line
console.log(`> Ready on http://${args['--hostname'] || 'localhost'}:${port}`)
await app.prepare()
})
.catch((err) => {

View file

@ -37,12 +37,12 @@ const args = arg({
// Version is inlined into the file using taskr build pipeline
if (args['--version']) {
// tslint:disable-next-line
console.log(`Next.js v${process.env.NEXT_VERSION}`)
console.log(`Next.js v${process.env.__NEXT_VERSION}`)
process.exit(0)
}
// Check if we are running `next <subcommand>` or `next`
const foundCommand = args._.find((cmd) => commands.includes(cmd))
const foundCommand = commands.includes(args._[0])
// Makes sure the `next <subcommand> --help` case is covered
// This help message is only showed for `next --help`
@ -73,8 +73,8 @@ if (args['--inspect']) {
nodeArguments.push('--inspect')
}
const command = foundCommand || defaultCommand
const forwardedArgs = args._.filter((arg) => arg !== command)
const command = foundCommand ? args._[0] : defaultCommand
const forwardedArgs = foundCommand ? args._.slice(1) : args._
// Make sure the `next <subcommand> --help` case is covered
if (args['--help']) {

View file

@ -1,11 +1,13 @@
import webpack from 'webpack'
export type CompilerResult = {
errors: Error[],
errors: Error[]
warnings: Error[]
}
export function runCompiler (config: webpack.Configuration[]): Promise<CompilerResult> {
export function runCompiler(
config: webpack.Configuration[]
): Promise<CompilerResult> {
return new Promise(async (resolve, reject) => {
const compiler = webpack(config)
compiler.run((err, multiStats: any) => {
@ -13,17 +15,25 @@ export function runCompiler (config: webpack.Configuration[]): Promise<CompilerR
return reject(err)
}
const result: CompilerResult = multiStats.stats.reduce((result: CompilerResult, stat: webpack.Stats): CompilerResult => {
if (stat.compilation.errors.length > 0) {
result.errors.push(...stat.compilation.errors)
}
const result: CompilerResult = multiStats.stats.reduce(
(result: CompilerResult, stat: webpack.Stats): CompilerResult => {
const { errors, warnings } = stat.toJson({
all: false,
warnings: true,
errors: true,
})
if (errors.length > 0) {
result.errors.push(...errors)
}
if (stat.compilation.warnings.length > 0) {
result.warnings.push(...stat.compilation.warnings)
}
if (warnings.length > 0) {
result.warnings.push(...warnings)
}
return result
}, {errors: [], warnings: []})
return result
},
{ errors: [], warnings: [] }
)
resolve(result)
})

View file

@ -2,6 +2,7 @@ import {join} from 'path'
import {stringify} from 'querystring'
import {PAGES_DIR_ALIAS, DOT_NEXT_ALIAS} from '../lib/constants'
import {ServerlessLoaderQuery} from './webpack/loaders/next-serverless-loader'
import {UnifiedLoaderQuery} from './webpack/loaders/next-unified-loader'
type PagesMapping = {
[page: string]: string
@ -30,7 +31,7 @@ type Entrypoints = {
server: WebpackEntrypoints
}
export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverless', buildId: string, config: any): Entrypoints {
export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverless'|'unified', buildId: string, config: any): Entrypoints {
const client: WebpackEntrypoints = {}
const server: WebpackEntrypoints = {}
@ -60,6 +61,22 @@ export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverl
client[bundlePath] = `next-client-pages-loader?${stringify({page, absolutePagePath})}!`
})
if(target === 'unified') {
const pagesArray: Array<String> = []
const absolutePagePaths: Array<String> = []
Object.keys(pages).forEach((page) => {
if(page === '/_document') {
return
}
pagesArray.push(page)
absolutePagePaths.push(pages[page])
});
const unifiedLoaderOptions: UnifiedLoaderQuery = {pages: pagesArray.join(','), absolutePagePaths: absolutePagePaths.join(','), ...defaultServerlessOptions};
server['index.js'] = `next-unified-loader?${stringify(unifiedLoaderOptions)}!`
}
return {
client,
server

View file

@ -3,36 +3,53 @@ import nanoid from 'nanoid'
import loadConfig from 'next-server/next-config'
import { PHASE_PRODUCTION_BUILD } from 'next-server/constants'
import getBaseWebpackConfig from './webpack-config'
import {generateBuildId} from './generate-build-id'
import {writeBuildId} from './write-build-id'
import {isWriteable} from './is-writeable'
import {runCompiler, CompilerResult} from './compiler'
import { generateBuildId } from './generate-build-id'
import { writeBuildId } from './write-build-id'
import { isWriteable } from './is-writeable'
import { runCompiler, CompilerResult } from './compiler'
import globModule from 'glob'
import {promisify} from 'util'
import {createPagesMapping, createEntrypoints} from './entries'
import { promisify } from 'util'
import { createPagesMapping, createEntrypoints } from './entries'
import formatWebpackMessages from '../client/dev-error-overlay/format-webpack-messages'
import chalk from 'chalk'
const glob = promisify(globModule)
function collectPages (directory: string, pageExtensions: string[]): Promise<string[]> {
return glob(`**/*.+(${pageExtensions.join('|')})`, {cwd: directory})
function collectPages(
directory: string,
pageExtensions: string[]
): Promise<string[]> {
return glob(`**/*.+(${pageExtensions.join('|')})`, { cwd: directory })
}
function printTreeView(list: string[]) {
list
.sort((a, b) => a > b ? 1 : -1)
.sort((a, b) => (a > b ? 1 : -1))
.forEach((item, i) => {
const corner = i === 0 ? list.length === 1 ? '─' : '┌' : i === list.length - 1 ? '└' : '├'
const corner =
i === 0
? list.length === 1
? '─'
: '┌'
: i === list.length - 1
? '└'
: '├'
console.log(` \x1b[90m${corner}\x1b[39m ${item}`)
})
console.log()
}
export default async function build (dir: string, conf = null): Promise<void> {
if (!await isWriteable(dir)) {
throw new Error('> Build directory is not writeable. https://err.sh/zeit/next.js/build-dir-not-writeable')
export default async function build(dir: string, conf = null): Promise<void> {
if (!(await isWriteable(dir))) {
throw new Error(
'> Build directory is not writeable. https://err.sh/zeit/next.js/build-dir-not-writeable'
)
}
console.log('Creating an optimized production build ...')
console.log()
const config = loadConfig(PHASE_PRODUCTION_BUILD, dir, conf)
const buildId = await generateBuildId(config.generateBuildId, nanoid)
const distDir = join(dir, config.distDir)
@ -42,34 +59,66 @@ export default async function build (dir: string, conf = null): Promise<void> {
const pages = createPagesMapping(pagePaths, config.pageExtensions)
const entrypoints = createEntrypoints(pages, config.target, buildId, config)
const configs: any = await Promise.all([
getBaseWebpackConfig(dir, { buildId, isServer: false, config, target: config.target, entrypoints: entrypoints.client }),
getBaseWebpackConfig(dir, { buildId, isServer: true, config, target: config.target, entrypoints: entrypoints.server })
getBaseWebpackConfig(dir, {
buildId,
isServer: false,
config,
target: config.target,
entrypoints: entrypoints.client,
}),
getBaseWebpackConfig(dir, {
buildId,
isServer: true,
config,
target: config.target,
entrypoints: entrypoints.server,
}),
])
let result: CompilerResult = {warnings: [], errors: []}
if (config.target === 'serverless') {
if (config.publicRuntimeConfig) throw new Error('Cannot use publicRuntimeConfig with target=serverless https://err.sh/zeit/next.js/serverless-publicRuntimeConfig')
let result: CompilerResult = { warnings: [], errors: [] }
if (config.target === 'serverless' || config.target === 'unified') {
if (config.publicRuntimeConfig)
throw new Error(
`Cannot use publicRuntimeConfig with target=${config.target} https://err.sh/zeit/next.js/serverless-publicRuntimeConfig`
)
const clientResult = await runCompiler([configs[0]])
// Fail build if clientResult contains errors
if(clientResult.errors.length > 0) {
result = {warnings: [...clientResult.warnings], errors: [...clientResult.errors]}
if (clientResult.errors.length > 0) {
result = {
warnings: [...clientResult.warnings],
errors: [...clientResult.errors],
}
} else {
const serverResult = await runCompiler([configs[1]])
result = {warnings: [...clientResult.warnings, ...serverResult.warnings], errors: [...clientResult.errors, ...serverResult.errors]}
result = {
warnings: [...clientResult.warnings, ...serverResult.warnings],
errors: [...clientResult.errors, ...serverResult.errors],
}
}
} else {
result = await runCompiler(configs)
}
if (result.warnings.length > 0) {
console.warn('> Emitted warnings from webpack')
result.warnings.forEach((warning) => console.warn(warning))
}
result = formatWebpackMessages(result)
if (result.errors.length > 0) {
result.errors.forEach((error) => console.error(error))
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (result.errors.length > 1) {
result.errors.length = 1
}
console.error(chalk.red('Failed to compile.\n'))
console.error(result.errors.join('\n\n'))
console.error()
throw new Error('> Build failed because of webpack errors')
} else if (result.warnings.length > 0) {
console.warn(chalk.yellow('Compiled with warnings.\n'))
console.warn(result.warnings.join('\n\n'))
console.warn()
} else {
console.log(chalk.green('Compiled successfully.\n'))
}
printTreeView(Object.keys(pages))

View file

@ -0,0 +1,13 @@
// This file is derived from Jest:
// https://github.com/facebook/jest/blob/d9d501ac342212b8a58ddb23a31518beb7b56f47/packages/jest-util/src/specialChars.ts#L18
const isWindows = process.platform === 'win32'
const isInteractive = process.stdout.isTTY
export function clearConsole() {
if (!isInteractive) {
return
}
process.stdout.write(isWindows ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H')
}

View file

@ -0,0 +1,110 @@
import createStore from 'unistore'
import { store, OutputState } from './store'
import formatWebpackMessages from '../../client/dev-error-overlay/format-webpack-messages'
export function startedDevelopmentServer(appUrl: string) {
store.setState({ appUrl })
}
let previousClient: any = null
let previousServer: any = null
type WebpackStatus =
| { loading: true }
| {
loading: false
errors: string[] | null
warnings: string[] | null
}
type WebpackStatusStore = {
client: WebpackStatus
server: WebpackStatus
}
enum WebpackStatusPhase {
COMPILING = 1,
COMPILED_WITH_ERRORS = 2,
COMPILED_WITH_WARNINGS = 3,
COMPILED = 4,
}
function getWebpackStatusPhase(status: WebpackStatus): WebpackStatusPhase {
if (status.loading) {
return WebpackStatusPhase.COMPILING
}
if (status.errors) {
return WebpackStatusPhase.COMPILED_WITH_ERRORS
}
if (status.warnings) {
return WebpackStatusPhase.COMPILED_WITH_WARNINGS
}
return WebpackStatusPhase.COMPILED
}
const webpackStore = createStore<WebpackStatusStore>()
webpackStore.subscribe(state => {
const { client, server } = state
const [{ status }] = [
{ status: client, phase: getWebpackStatusPhase(client) },
{ status: server, phase: getWebpackStatusPhase(server) },
].sort((a, b) => a.phase.valueOf() - b.phase.valueOf())
const { bootstrap: bootstrapping, appUrl } = store.getState()
if (bootstrapping && status.loading) {
return
}
let nextStoreState: OutputState = {
bootstrap: false,
appUrl: appUrl!,
...status,
}
store.setState(nextStoreState, true)
})
export function watchCompiler(client: any, server: any) {
if (previousClient === client && previousServer === server) {
return
}
webpackStore.setState({
client: { loading: true },
server: { loading: true },
})
function tapCompiler(
key: string,
compiler: any,
onEvent: (status: WebpackStatus) => void
) {
compiler.hooks.invalid.tap(`NextJsInvalid-${key}`, () => {
onEvent({ loading: true })
})
compiler.hooks.done.tap(`NextJsDone-${key}`, (stats: any) => {
const { errors, warnings } = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true })
)
onEvent({
loading: false,
errors: errors && errors.length ? errors : null,
warnings: warnings && warnings.length ? warnings : null,
})
})
}
tapCompiler('client', client, status =>
webpackStore.setState({ client: status })
)
tapCompiler('server', server, status =>
webpackStore.setState({ server: status })
)
previousClient = client
previousServer = server
}

View file

@ -0,0 +1,56 @@
import chalk from 'chalk'
import createStore from 'unistore'
import { clearConsole } from './clearConsole'
export type OutputState =
| { bootstrap: true; appUrl: string | null }
| ({ bootstrap: false; appUrl: string | null } & (
| { loading: true }
| {
loading: false
errors: string[] | null
warnings: string[] | null
}))
export const store = createStore<OutputState>({ appUrl: null, bootstrap: true })
store.subscribe(state => {
clearConsole()
if (state.bootstrap) {
console.log(chalk.cyan('Starting the development server ...'))
if (state.appUrl) {
console.log()
console.log(` > Waiting on ${state.appUrl!}`)
}
return
}
if (state.loading) {
console.log('Compiling ...')
return
}
if (state.errors) {
console.log(chalk.red('Failed to compile.'))
console.log()
console.log(state.errors[0])
return
}
if (state.warnings) {
console.log(chalk.yellow('Compiled with warnings.'))
console.log()
console.log(state.warnings.join('\n\n'))
return
}
console.log(chalk.green('Compiled successfully!'))
if (state.appUrl) {
console.log()
console.log(` > Ready on ${state.appUrl!}`)
}
console.log()
console.log('Note that pages will be compiled when you first load them.')
})

View file

@ -2,8 +2,6 @@ import path from 'path'
import webpack from 'webpack'
import resolve from 'resolve'
import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'
import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
import WebpackBar from 'webpackbar'
import NextJsSsrImportPlugin from './webpack/plugins/nextjs-ssr-import'
import NextJsSSRModuleCachePlugin from './webpack/plugins/nextjs-ssr-module-cache'
import NextJsRequireCacheHotReloader from './webpack/plugins/nextjs-require-cache-hot-reloader'
@ -26,7 +24,7 @@ function externalsConfig (isServer, target) {
// When the serverless target is used all node_modules will be compiled into the output bundles
// So that the serverless bundles have 0 runtime dependencies
if (!isServer || target === 'serverless') {
if (!isServer || target === 'serverless' || target === 'unified') {
return externals
}
@ -81,7 +79,7 @@ function optimizationConfig ({ dev, isServer, totalPages, target }) {
}
}
if (isServer && target === 'serverless') {
if (isServer && (target === 'serverless' || target === 'unified')) {
return {
splitChunks: false,
minimizer: [
@ -166,7 +164,12 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
.filter((p) => !!p)
const distDir = path.join(dir, config.distDir)
const outputDir = target === 'serverless' ? 'serverless' : SERVER_DIRECTORY
let outputDir = SERVER_DIRECTORY
if (target === 'serverless') {
outputDir = 'serverless'
} else if (target === 'unified') {
outputDir = 'unified'
}
const outputPath = path.join(distDir, isServer ? outputDir : '')
const totalPages = Object.keys(entrypoints).length
const clientEntries = !isServer ? {
@ -189,7 +192,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
[PAGES_DIR_ALIAS]: path.join(dir, 'pages'),
[DOT_NEXT_ALIAS]: distDir
},
mainFields: isServer ? ['main'] : ['browser', 'module', 'main']
mainFields: isServer ? ['main', 'module'] : ['browser', 'module', 'main']
}
const webpackMode = dev ? 'development' : 'production'
@ -242,7 +245,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
module: {
rules: [
{
test: /\.(js|jsx)$/,
test: /\.(js|mjs|jsx)$/,
include: [dir, /next-server[\\/]dist[\\/]lib/],
exclude: (path) => {
if (/next-server[\\/]dist[\\/]lib/.exec(path)) {
@ -278,10 +281,6 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
!isServer && new ReactLoadablePlugin({
filename: REACT_LOADABLE_MANIFEST
}),
new WebpackBar({
name: isServer ? 'server' : 'client'
}),
dev && !isServer && new FriendlyErrorsWebpackPlugin(),
// Even though require.cache is server only we have to clear assets from both compilations
// This is because the client compilation generates the build manifest that's used on the server side
dev && new NextJsRequireCacheHotReloader(),
@ -291,26 +290,28 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
dev && new CaseSensitivePathPlugin(), // Since on macOS the filesystem is case-insensitive this will make sure your path are case-sensitive
!dev && new webpack.HashedModuleIdsPlugin(),
// Removes server/client code by minifier
new webpack.DefinePlugin(Object.assign(
{},
config.env ? Object.keys(config.env)
.reduce((acc, key) => ({
new webpack.DefinePlugin({
...(Object.keys(config.env).reduce((acc, key) => {
if (/^(?:NODE_.+)|(?:__.+)$/i.test(key)) {
throw new Error(`The key "${key}" under "env" in next.config.js is not allowed. https://err.sh/zeit/next.js/env-key-not-allowed`)
}
return {
...acc,
...{ [`process.env.${key}`]: JSON.stringify(config.env[key]) }
}), {}) : {},
{
'process.crossOrigin': JSON.stringify(config.crossOrigin),
'process.browser': JSON.stringify(!isServer)
}
)),
[`process.env.${key}`]: JSON.stringify(config.env[key])
}
}, {})),
'process.crossOrigin': JSON.stringify(config.crossOrigin),
'process.browser': JSON.stringify(!isServer)
}),
// This is used in client/dev-error-overlay/hot-dev-client.js to replace the dist directory
!isServer && dev && new webpack.DefinePlugin({
'process.env.__NEXT_DIST_DIR': JSON.stringify(distDir)
}),
target !== 'serverless' && isServer && new PagesManifestPlugin(),
target !== 'serverless' && target !== 'unified' && isServer && new PagesManifestPlugin(),
!isServer && new BuildManifestPlugin(),
isServer && new NextJsSsrImportPlugin(),
target !== 'serverless' && isServer && new NextJsSSRModuleCachePlugin({outputPath}),
target !== 'serverless' && target !== 'unified' && isServer && new NextJsSSRModuleCachePlugin({ outputPath }),
!dev && new webpack.IgnorePlugin({
checkResource: (resource) => {
return /react-is/.test(resource)

View file

@ -0,0 +1,135 @@
import { loader } from 'webpack';
import { join } from 'path';
import { parse } from 'querystring';
import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST } from 'next-server/constants';
export type UnifiedLoaderQuery = {
pages: string
distDir: string
absolutePagePaths: string
absoluteAppPath: string
absoluteDocumentPath: string
absoluteErrorPath: string
buildId: string
assetPrefix: string
}
const nextUnifiedLoader: loader.Loader = function() {
const {distDir, absolutePagePaths, pages, buildId, assetPrefix, absoluteAppPath, absoluteDocumentPath, absoluteErrorPath}: UnifiedLoaderQuery =
typeof this.query === 'string' ? parse(this.query.substr(1)) : this.query
const buildManifest = join(distDir, BUILD_MANIFEST).replace(/\\/g, '/')
const reactLoadableManifest = join(distDir, REACT_LOADABLE_MANIFEST).replace(/\\/g, '/')
const parsedPagePaths = absolutePagePaths.split(',')
const parsedPages = pages.split(',')
return `
import {renderToHTML} from 'next-server/dist/server/render'
import buildManifest from '${buildManifest}'
import reactLoadableManifest from '${reactLoadableManifest}'
import Document from '${absoluteDocumentPath}'
import Error from '${absoluteErrorPath}'
import App from '${absoluteAppPath}'
${parsedPagePaths
.map(
(absolutePagePath, index) =>
`import page${index} from '${absolutePagePath}'`,
)
.join('\n')}
const errorPage = '/_error'
const routes = ${JSON.stringify(
Object.assign(
{},
...parsedPages.map((page, index) => ({ [page]: `page${index}` })),
),
).replace(/"(page\d+)"/g, '$1')}
function matchRoute(url) {
let page = '/index'
if (url === '/' && routes.hasOwnProperty(page)) {
return [page, routes[page]]
}
if (routes.hasOwnProperty(url)) {
return [url, routes[url]]
}
const splitUrl = url.split('/');
for (let i = splitUrl.length; i > 0; i--) {
const currentPrefix = splitUrl.slice(0, i).join('/')
if (routes.hasOwnProperty(currentPrefix)) {
return [currentPrefix, routes[currentPrefix]]
}
}
return [errorPage, routes[errorPage]]
};
const errorResponse = { status: 500, body: 'Internal Server Error', headers: {} }
export async function render(url, query = {}, reqHeaders = {}) {
const req = {
headers: reqHeaders,
method: 'GET',
url
}
const [page, Component] = matchRoute(url)
const headers = {}
let body = ''
const res = {
statusCode: 200,
finished: false,
headersSent: false,
setHeader(name, value) {
headers[name.toLowerCase()] = value
},
getHeader(name) {
return headers[name.toLowerCase()]
}
};
const options = {
App,
Document,
buildManifest,
reactLoadableManifest,
buildId: ${JSON.stringify(buildId)},
assetPrefix: ${JSON.stringify(assetPrefix)},
Component
}
try {
if (page === '/_error') {
res.statusCode = 404
}
body = await renderToHTML(req, res, page, query, Object.assign({}, options))
} catch (err) {
if (err.code === 'ENOENT') {
res.statusCode = 404
body = await renderToHTML(req, res, "/_error", query, Object.assign({}, options, {
Component: Error
}))
} else {
console.error(err)
try {
res.statusCode = 500
body = await renderToHTML(req, res, "/_error", query, Object.assign({}, options, {
Component: Error,
err
}))
} catch (e) {
console.error(e)
return errorResponse; // non-html fatal/fallback error
}
}
}
return {
status: res.statusCode,
headers: Object.assign({
'content-type': 'text/html; charset=utf-8',
'content-length': Buffer.byteLength(body)
}, headers),
body
}
}
`;
};
export default nextUnifiedLoader;

View file

@ -1,7 +1,7 @@
/**
MIT License
Copyright (c) 2013-present, Facebook, Inc.
Copyright (c) 2015-present, Facebook, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -21,16 +21,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// This file is based on https://github.com/facebook/create-react-app/blob/v1.1.4/packages/react-dev-utils/formatWebpackMessages.js
// It's been edited to remove chalk
// This file is based on https://github.com/facebook/create-react-app/blob/7b1a32be6ec9f99a6c9a3c66813f3ac09c4736b9/packages/react-dev-utils/formatWebpackMessages.js
// It's been edited to remove chalk and CRA-specific logic
'use strict'
// Some custom utilities to prettify Webpack output.
// This is quite hacky and hopefully won't be needed when Webpack fixes this.
// https://github.com/webpack/webpack/issues/2878
var friendlySyntaxErrorLabel = 'Syntax error:'
const friendlySyntaxErrorLabel = 'Syntax error:'
function isLikelyASyntaxError (message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1
@ -39,112 +35,99 @@ function isLikelyASyntaxError (message) {
// Cleans up webpack error messages.
// eslint-disable-next-line no-unused-vars
function formatMessage (message, isError) {
var lines = message.split('\n')
let lines = message.split('\n')
if (lines.length > 2 && lines[1] === '') {
// Remove extra newline.
// Strip Webpack-added headers off errors/warnings
// https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
lines = lines.filter(line => !/Module [A-z ]+\(from/.test(line))
// Transform parsing error into syntax error
// TODO: move this to our ESLint formatter?
lines = lines.map(line => {
const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(
line
)
if (!parsingError) {
return line
}
const [, errorLine, errorColumn, errorMessage] = parsingError
return `${friendlySyntaxErrorLabel} ${errorMessage} (${errorLine}:${errorColumn})`
})
message = lines.join('\n')
// Smoosh syntax errors (commonly found in CSS)
message = message.replace(
/SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g,
`${friendlySyntaxErrorLabel} $3 ($1:$2)\n`
)
// Remove columns from ESLint formatter output (we added these for more
// accurate syntax errors)
message = message.replace(/Line (\d+):\d+:/g, 'Line $1:')
// Clean up export errors
message = message.replace(
/^.*export '(.+?)' was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$2'.`
)
message = message.replace(
/^.*export 'default' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$2' does not contain a default export (imported as '$1').`
)
message = message.replace(
/^.*export '(.+?)' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$3' (imported as '$2').`
)
lines = message.split('\n')
// Remove leading newline
if (lines.length > 2 && lines[1].trim() === '') {
lines.splice(1, 1)
}
// Remove webpack-specific loader notation from filename.
// Before:
// ./~/css-loader!./~/postcss-loader!./src/App.css
// After:
// ./src/App.css
if (lines[0].lastIndexOf('!') !== -1) {
lines[0] = lines[0].substr(lines[0].lastIndexOf('!') + 1)
}
// Remove unnecessary stack added by `thread-loader`
var threadLoaderIndex = -1
lines.forEach(function (line, index) {
if (threadLoaderIndex !== -1) {
return
}
if (/thread.loader/i.test(line)) {
threadLoaderIndex = index
}
})
if (threadLoaderIndex !== -1) {
lines = lines.slice(0, threadLoaderIndex)
}
lines = lines.filter(function (line) {
// Webpack adds a list of entry points to warning messages:
// @ ./src/index.js
// @ multi react-scripts/~/react-dev-utils/webpackHotDevClient.js ...
// It is misleading (and unrelated to the warnings) so we clean it up.
// It is only useful for syntax errors but we have beautiful frames for them.
return line.indexOf(' @ ') !== 0
})
// line #0 is filename
// line #1 is the main error message
if (!lines[0] || !lines[1]) {
return lines.join('\n')
}
// Clean up file name
lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, '$1')
// Cleans up verbose "module not found" messages for files and packages.
if (lines[1].indexOf('Module not found: ') === 0) {
if (lines[1] && lines[1].indexOf('Module not found: ') === 0) {
lines = [
lines[0],
// Clean up message because "Module not found: " is descriptive enough.
lines[1]
.replace("Cannot resolve 'file' or 'directory' ", '')
.replace('Cannot resolve module ', '')
.replace('Error: ', '')
.replace('[CaseSensitivePathsPlugin] ', '')
.replace('Module not found: Cannot find file:', 'Cannot find file:')
]
}
// Cleans up syntax error messages.
if (lines[1].indexOf('Module build failed: ') === 0) {
lines[1] = lines[1].replace(
'Module build failed: SyntaxError:',
friendlySyntaxErrorLabel
)
}
// Clean up export errors.
// TODO: we should really send a PR to Webpack for this.
var exportError = /\s*(.+?)\s*(")?export '(.+?)' was not found in '(.+?)'/
if (lines[1].match(exportError)) {
lines[1] = lines[1].replace(
exportError,
"$1 '$4' does not contain an export named '$3'."
)
}
// Reassemble the message.
message = lines.join('\n')
// Internal stacks are generally useless so we strip them... with the
// exception of stacks containing `webpack:` because they're normally
// from user code generated by WebPack. For more information see
// from user code generated by Webpack. For more information see
// https://github.com/facebook/create-react-app/pull/1050
message = message.replace(
/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm,
''
) // at ... ...:x:y
message = message.replace(/^\s*at\s<anonymous>(\n|$)/gm, '') // at <anonymous>
lines = message.split('\n')
// Remove duplicated newlines
lines = lines.filter(
(line, index, arr) =>
index === 0 || line.trim() !== '' || line.trim() !== arr[index - 1].trim()
)
// Reassemble the message
message = lines.join('\n')
return message.trim()
}
function formatWebpackMessages (json) {
var formattedErrors = json.errors.map(function (message) {
const formattedErrors = json.errors.map(function (message) {
return formatMessage(message, true)
})
var formattedWarnings = json.warnings.map(function (message) {
const formattedWarnings = json.warnings.map(function (message) {
return formatMessage(message, false)
})
var result = {
errors: formattedErrors,
warnings: formattedWarnings
}
const result = { errors: formattedErrors, warnings: formattedWarnings }
if (result.errors.some(isLikelyASyntaxError)) {
// If there are any syntax errors, show just them.
// This prevents a confusing ESLint parsing error
// preceding a much more useful Babel syntax error.
result.errors = result.errors.filter(isLikelyASyntaxError)
}
return result

View file

@ -8,25 +8,37 @@ const wsProtocol = protocol.includes('https') ? 'wss' : 'ws'
const retryTime = 5000
let ws = null
let lastHref = null
let wsConnectTries = 0
let showedWarning = false
export default async ({ assetPrefix }) => {
Router.ready(() => {
Router.events.on('routeChangeComplete', ping)
})
const setup = async (reconnect) => {
const setup = async () => {
if (ws && ws.readyState === ws.OPEN) {
return Promise.resolve()
} else if (wsConnectTries > 1) {
return
}
wsConnectTries++
return new Promise(resolve => {
ws = new WebSocket(`${wsProtocol}://${hostname}:${process.env.NEXT_WS_PORT}${process.env.NEXT_WS_PROXY_PATH}`)
ws.onopen = () => resolve()
ws = new WebSocket(`${wsProtocol}://${hostname}:${process.env.__NEXT_WS_PORT}${process.env.__NEXT_WS_PROXY_PATH}`)
ws.onopen = () => {
wsConnectTries = 0
resolve()
}
ws.onclose = () => {
setTimeout(async () => {
// check if next restarted and we have to reload to get new port
await fetch(`${assetPrefix}/_next/on-demand-entries-ping`)
.then(res => res.status === 200 && location.reload())
.then(res => {
// Only reload if next was restarted and we have a new WebSocket port
if (res.status === 200 && res.headers.get('port') !== process.env.__NEXT_WS_PORT + '') {
location.reload()
}
})
.catch(() => {})
await setup(true)
resolve()
@ -49,11 +61,36 @@ export default async ({ assetPrefix }) => {
}
})
}
await setup()
setup()
async function ping () {
if (ws.readyState === ws.OPEN) {
ws.send(Router.pathname)
// Use WebSocket if available
if (ws && ws.readyState === ws.OPEN) {
return ws.send(Router.pathname)
}
if (!showedWarning) {
console.warn('onDemandEntries WebSocket failed to connect, falling back to fetch based pinging. https://err.sh/zeit/next.js/on-demand-entries-websocket-unavailable')
showedWarning = true
}
// If not, fallback to fetch based pinging
try {
const url = `${assetPrefix || ''}/_next/on-demand-entries-ping?page=${Router.pathname}`
const res = await fetch(url, {
credentials: 'same-origin'
})
const payload = await res.json()
if (payload.invalid) {
// Payload can be invalid even if the page does not exist.
// So, we need to make sure it exists before reloading.
const pageRes = await fetch(location.href, {
credentials: 'same-origin'
})
if (pageRes.status === 200) {
location.reload()
}
}
} catch (err) {
console.error(`Error with on-demand-entries-ping: ${err.message}`)
}
}

View file

@ -7,7 +7,6 @@ import { resolve, join } from 'path'
import { existsSync, readFileSync } from 'fs'
import loadConfig from 'next-server/next-config'
import { PHASE_EXPORT, SERVER_DIRECTORY, PAGES_MANIFEST, CONFIG_FILE, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH } from 'next-server/constants'
import * as envConfig from 'next-server/config'
import createProgress from 'tty-aware-progress'
export default async function (dir, options, configuration) {
@ -98,11 +97,6 @@ export default async function (dir, options, configuration) {
renderOpts.runtimeConfig = publicRuntimeConfig
}
envConfig.setConfig({
serverRuntimeConfig,
publicRuntimeConfig
})
// We need this for server rendering the Link component.
global.__NEXT_DATA__ = {
nextExport: true
@ -138,6 +132,7 @@ export default async function (dir, options, configuration) {
exportPathMap: chunk.pathMap,
outDir,
renderOpts,
serverRuntimeConfig,
concurrency
})
worker.on('message', ({ type, payload }) => {

View file

@ -8,6 +8,7 @@ const { renderToHTML } = require('next-server/dist/server/render')
const { writeFile } = require('fs')
const Sema = require('async-sema')
const {loadComponents} = require('next-server/dist/server/load-components')
const envConfig = require('next-server/config')
process.on(
'message',
@ -18,6 +19,7 @@ process.on(
exportPathMap,
outDir,
renderOpts,
serverRuntimeConfig,
concurrency
}) => {
const sema = new Sema(concurrency, { capacity: exportPaths.length })
@ -27,6 +29,10 @@ process.on(
const { page, query = {} } = exportPathMap[path]
const req = { url: path }
const res = {}
envConfig.setConfig({
serverRuntimeConfig,
publicRuntimeConfig: renderOpts.runtimeConfig
})
let htmlFilename = `${path}${sep}index.html`
if (extname(path) !== '') {

View file

@ -1,6 +1,6 @@
{
"name": "next",
"version": "8.0.0",
"version": "8.0.2-canary.3",
"description": "The React Framework",
"main": "./dist/server/next.js",
"license": "MIT",
@ -19,7 +19,8 @@
"error.js",
"head.js",
"link.js",
"router.js"
"router.js",
"amp.js"
],
"bin": {
"next": "./dist/bin/next"
@ -58,13 +59,13 @@
"babel-plugin-transform-react-remove-prop-types": "0.4.15",
"cacache": "^11.0.2",
"case-sensitive-paths-webpack-plugin": "2.1.2",
"chalk": "2.4.2",
"cross-spawn": "5.1.0",
"del": "3.0.0",
"event-source-polyfill": "0.0.12",
"find-cache-dir": "2.0.0",
"find-up": "2.1.0",
"fresh": "0.5.2",
"friendly-errors-webpack-plugin": "1.7.0",
"glob": "7.1.2",
"hoist-non-react-statics": "3.2.0",
"http-status": "1.0.1",
@ -72,7 +73,7 @@
"loader-utils": "1.1.0",
"mkdirp-then": "1.2.0",
"nanoid": "1.2.1",
"next-server": "8.0.0",
"next-server": "8.0.2-canary.3",
"prop-types": "15.6.2",
"prop-types-exact": "1.2.0",
"react-error-overlay": "4.0.0",
@ -87,12 +88,12 @@
"terser": "3.16.1",
"tty-aware-progress": "1.0.3",
"unfetch": "3.0.0",
"unistore": "3.2.1",
"url": "0.11.0",
"webpack": "4.29.0",
"webpack-dev-middleware": "3.4.0",
"webpack-hot-middleware": "2.24.3",
"webpack-sources": "1.3.0",
"webpackbar": "3.1.4 ",
"worker-farm": "1.5.2",
"ws": "6.1.2"
},

View file

@ -4,10 +4,6 @@ import PropTypes from 'prop-types'
import {htmlEscapeJsonString} from '../server/htmlescape'
import flush from 'styled-jsx/server'
const Fragment = React.Fragment || function Fragment ({ children }) {
return <div>{children}</div>
}
export default class Document extends Component {
static childContextTypes = {
_documentProps: PropTypes.any,
@ -31,7 +27,7 @@ export default class Document extends Component {
}
render () {
return <html>
return <html amp={this.props.amphtml ? '' : null}>
<Head />
<body>
<Main />
@ -115,10 +111,9 @@ export class Head extends Component {
}
render () {
const { head, styles, assetPrefix, __NEXT_DATA__ } = this.context._documentProps
const { asPath, ampEnabled, head, styles, amphtml, assetPrefix, __NEXT_DATA__ } = this.context._documentProps
const { _devOnlyInvalidateCacheQueryString } = this.context
const { page, buildId } = __NEXT_DATA__
const pagePathname = getPagePathname(page)
let children = this.props.children
// show a warning if Head contains <title> (only in development)
@ -134,12 +129,26 @@ export class Head extends Component {
return <head {...this.props}>
{children}
{head}
{page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages${pagePathname}${_devOnlyInvalidateCacheQueryString}`} as='script' nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />}
{amphtml && <>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"/>
<link rel="canonical" href={asPath === '/amp' ? '/' : asPath.replace(/\/amp$/, '')} />
{/* https://www.ampproject.org/docs/fundamentals/optimize_amp#optimize-the-amp-runtime-loading */}
<link rel="preload" as="script" href="https://cdn.ampproject.org/v0.js" />
{/* Add custom styles before AMP styles to prevent accidental overrides */}
{styles && <style amp-custom="" dangerouslySetInnerHTML={{__html: styles.map((style) => style.props.dangerouslySetInnerHTML.__html)}} />}
<style amp-boilerplate="" dangerouslySetInnerHTML={{__html: `body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}`}}></style>
<noscript><style amp-boilerplate="" dangerouslySetInnerHTML={{__html: `body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}`}}></style></noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
</>}
{!amphtml && <>
{ampEnabled && <link rel="amphtml" href={asPath === '/' ? '/amp' : (asPath.replace(/\/$/, '') + '/amp')} />}
{page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages${getPagePathname(page)}${_devOnlyInvalidateCacheQueryString}`} as='script' nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />}
<link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages/_app.js${_devOnlyInvalidateCacheQueryString}`} as='script' nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />
{this.getPreloadDynamicChunks()}
{this.getPreloadMainLinks()}
{this.getCssLinks()}
{styles || null}
</>}
</head>
}
}
@ -221,25 +230,29 @@ export class NextScript extends Component {
}
render () {
const { staticMarkup, assetPrefix, devFiles, __NEXT_DATA__ } = this.context._documentProps
const { staticMarkup, assetPrefix, amphtml, devFiles, __NEXT_DATA__ } = this.context._documentProps
const { _devOnlyInvalidateCacheQueryString } = this.context
if(amphtml) {
return null
}
const { page, buildId } = __NEXT_DATA__
const pagePathname = getPagePathname(page)
if (process.env.NODE_ENV !== 'production') {
if (this.props.crossOrigin) console.warn('Warning: `NextScript` attribute `crossOrigin` is deprecated. https://err.sh/next.js/doc-crossorigin-deprecated')
}
return <Fragment>
return <>
{devFiles ? devFiles.map((file) => <script key={file} src={`${assetPrefix}/_next/${file}${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />) : null}
{staticMarkup ? null : <script id="__NEXT_DATA__" type="application/json" nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} dangerouslySetInnerHTML={{
__html: NextScript.getInlineScriptSource(this.context._documentProps)
}} />}
{page !== '/_error' && <script async id={`__NEXT_PAGE__${page}`} src={`${assetPrefix}/_next/static/${buildId}/pages${pagePathname}${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />}
{page !== '/_error' && <script async id={`__NEXT_PAGE__${page}`} src={`${assetPrefix}/_next/static/${buildId}/pages${getPagePathname(page)}${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />}
<script async id={`__NEXT_PAGE__/_app`} src={`${assetPrefix}/_next/static/${buildId}/pages/_app.js${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />
{staticMarkup ? null : this.getDynamicChunks()}
{staticMarkup ? null : this.getScripts()}
</Fragment>
</>
}
}

View file

@ -7,29 +7,38 @@ export default class Error extends React.Component {
static displayName = 'ErrorPage'
static getInitialProps ({ res, err }) {
const statusCode = res ? res.statusCode : (err ? err.statusCode : null)
const statusCode =
res && res.statusCode ? res.statusCode : err ? err.statusCode : 404
return { statusCode }
}
render () {
const { statusCode } = this.props
const title = statusCode === 404
? 'This page could not be found'
: HTTPStatus[statusCode] || 'An unexpected error has occurred'
const title =
statusCode === 404
? 'This page could not be found'
: HTTPStatus[statusCode] || 'An unexpected error has occurred'
return <div style={styles.error}>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>{statusCode}: {title}</title>
</Head>
<div>
<style dangerouslySetInnerHTML={{ __html: 'body { margin: 0 }' }} />
{statusCode ? <h1 style={styles.h1}>{statusCode}</h1> : null}
<div style={styles.desc}>
<h2 style={styles.h2}>{title}.</h2>
return (
<div style={styles.error}>
<Head>
<meta
name='viewport'
content='width=device-width, initial-scale=1.0'
/>
<title>
{statusCode}: {title}
</title>
</Head>
<div>
<style dangerouslySetInnerHTML={{ __html: 'body { margin: 0 }' }} />
{statusCode ? <h1 style={styles.h1}>{statusCode}</h1> : null}
<div style={styles.desc}>
<h2 style={styles.h2}>{title}.</h2>
</div>
</div>
</div>
</div>
)
}
}
@ -43,7 +52,8 @@ const styles = {
error: {
color: '#000',
background: '#fff',
fontFamily: '-apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif',
fontFamily:
'-apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif',
height: '100vh',
textAlign: 'center',
display: 'flex',

View file

@ -12,6 +12,7 @@ import {route} from 'next-server/dist/server/router'
import globModule from 'glob'
import {promisify} from 'util'
import {createPagesMapping, createEntrypoints} from '../build/entries'
import {watchCompiler} from '../build/output'
const glob = promisify(globModule)
@ -167,8 +168,8 @@ export default class HotReloader {
addWsConfig (configs) {
const { websocketProxyPath, websocketProxyPort } = this.config.onDemandEntries
const opts = {
'process.env.NEXT_WS_PORT': websocketProxyPort || this.wsPort,
'process.env.NEXT_WS_PROXY_PATH': JSON.stringify(websocketProxyPath)
'process.env.__NEXT_WS_PORT': websocketProxyPort || this.wsPort,
'process.env.__NEXT_WS_PROXY_PATH': JSON.stringify(websocketProxyPath)
}
configs[0].plugins.push(new webpack.DefinePlugin(opts))
}
@ -259,6 +260,11 @@ export default class HotReloader {
}
async prepareBuildTools (multiCompiler) {
watchCompiler(
multiCompiler.compilers[0],
multiCompiler.compilers[1]
)
// This plugin watches for changes to _document.js and notifies the client side that it should reload the page
multiCompiler.compilers[1].hooks.done.tap('NextjsHotReloaderForServer', (stats) => {
if (!this.initialized) {

View file

@ -91,7 +91,7 @@ export default class DevServer extends Server {
return routes
}
async renderToHTML (req, res, pathname, query) {
async renderToHTML (req, res, pathname, query, options) {
const compilationErr = await this.getCompilationError(pathname)
if (compilationErr) {
res.statusCode = 500
@ -109,7 +109,7 @@ export default class DevServer extends Server {
if (!this.quiet) console.error(err)
}
return super.renderToHTML(req, res, pathname, query)
return super.renderToHTML(req, res, pathname, query, options)
}
async renderErrorToHTML (err, req, res, pathname, query) {

View file

@ -1,6 +1,7 @@
import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'
import { EventEmitter } from 'events'
import { join } from 'path'
import {parse} from 'url'
import fs from 'fs'
import promisify from '../lib/promisify'
import globModule from 'glob'
@ -152,6 +153,40 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
reloadCallbacks = null
}
function handlePing (pg, socket) {
const page = normalizePage(pg)
const entryInfo = entries[page]
// If there's no entry.
// Then it seems like an weird issue.
if (!entryInfo) {
const message = `Client pings, but there's no entry for page: ${page}`
console.error(message)
return sendJson(socket, { invalid: true })
}
// 404 is an on demand entry but when a new page is added we have to refresh the page
if (page === '/_error') {
sendJson(socket, { invalid: true })
} else {
sendJson(socket, { success: true })
}
// We don't need to maintain active state of anything other than BUILT entries
if (entryInfo.status !== BUILT) return
// If there's an entryInfo
if (!lastAccessPages.includes(page)) {
lastAccessPages.unshift(page)
// Maintain the buffer max length
if (lastAccessPages.length > pagesBufferLength) {
lastAccessPages.pop()
}
}
entryInfo.lastActiveTime = Date.now()
}
return {
waitUntilReloaded () {
if (!reloading) return Promise.resolve(true)
@ -225,37 +260,8 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
wsConnection (ws) {
ws.onmessage = ({ data }) => {
const page = normalizePage(data)
const entryInfo = entries[page]
// If there's no entry.
// Then it seems like an weird issue.
if (!entryInfo) {
const message = `Client pings, but there's no entry for page: ${page}`
console.error(message)
return sendJson(ws, { invalid: true })
}
// 404 is an on demand entry but when a new page is added we have to refresh the page
if (page === '/_error') {
sendJson(ws, { invalid: true })
} else {
sendJson(ws, { success: true })
}
// We don't need to maintain active state of anything other than BUILT entries
if (entryInfo.status !== BUILT) return
// If there's an entryInfo
if (!lastAccessPages.includes(page)) {
lastAccessPages.unshift(page)
// Maintain the buffer max length
if (lastAccessPages.length > pagesBufferLength) {
lastAccessPages.pop()
}
}
entryInfo.lastActiveTime = Date.now()
// `data` should be the page here
handlePing(data, ws)
}
},
@ -280,6 +286,12 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
} else {
if (!/^\/_next\/on-demand-entries-ping/.test(req.url)) return next()
const { query } = parse(req.url, true)
if (query.page) {
return handlePing(query.page, res)
}
res.statusCode = 200
res.setHeader('port', wsPort)
res.end('200')
@ -328,8 +340,17 @@ export function normalizePage (page) {
return unixPagePath.replace(/\/index$/, '')
}
function sendJson (ws, data) {
ws.send(JSON.stringify(data))
function sendJson (socket, data) {
data = JSON.stringify(data)
// Handle fetch request
if (socket.setHeader) {
socket.setHeader('content-type', 'application/json')
socket.status = 200
return socket.end(data)
}
// Should be WebSocket so just send
socket.send(data)
}
// Make sure only one invalidation happens at a time

View file

@ -40,7 +40,7 @@ try {
if (file.base === 'next-dev.js') result.outputText = result.outputText.replace('// REPLACE_NOOP_IMPORT', `import('./noop');`)
// update file's data
file.data = Buffer.from(result.outputText.replace(/process\.env\.NEXT_VERSION/, `"${require('./package.json').version}"`), 'utf8')
file.data = Buffer.from(result.outputText.replace(/process\.env\.__NEXT_VERSION/, `"${require('./package.json').version}"`), 'utf8')
})
}
} catch (err) {

View file

@ -0,0 +1,9 @@
module.exports = {
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60
},
experimental: {
amp: true
}
}

View file

@ -0,0 +1 @@
export default () => 'Hello World'

View file

@ -0,0 +1,6 @@
import {useAmp} from 'next/amp'
export default () => {
const isAmp = useAmp()
return `Hello ${isAmp ? 'AMP' : 'others'}`
}

View file

@ -0,0 +1,117 @@
/* eslint-env jest */
/* global jasmine */
import { join } from 'path'
import {
nextServer,
nextBuild,
startApp,
stopApp,
renderViaHTTP
} from 'next-test-utils'
import cheerio from 'cheerio'
import amphtmlValidator from 'amphtml-validator'
const appDir = join(__dirname, '../')
let appPort
let server
let app
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
const context = {}
async function validateAMP (html) {
const validator = await amphtmlValidator.getInstance()
const result = validator.validateString(html)
if (result.status !== 'PASS') {
for (let ii = 0; ii < result.errors.length; ii++) {
const error = result.errors[ii]
let msg = 'line ' + error.line + ', col ' + error.col + ': ' + error.message
if (error.specUrl !== null) {
msg += ' (see ' + error.specUrl + ')'
}
((error.severity === 'ERROR') ? console.error : console.warn)(msg)
}
}
expect(result.status).toBe('PASS')
}
describe('AMP Usage', () => {
beforeAll(async () => {
await nextBuild(appDir)
app = nextServer({
dir: join(__dirname, '../'),
dev: false,
quiet: true
})
server = await startApp(app)
context.appPort = appPort = server.address().port
})
afterAll(() => stopApp(server))
describe('With basic usage', () => {
it('should render the page', async () => {
const html = await renderViaHTTP(appPort, '/')
expect(html).toMatch(/Hello World/)
})
})
describe('With basic AMP usage', () => {
it('should render the page as valid AMP', async () => {
const html = await renderViaHTTP(appPort, '/amp')
await validateAMP(html)
expect(html).toMatch(/Hello World/)
})
it('should add link preload for amp script', async () => {
const html = await renderViaHTTP(appPort, '/amp')
await validateAMP(html)
const $ = cheerio.load(html)
expect($($('link[rel=preload]').toArray().find(i => $(i).attr('href') === 'https://cdn.ampproject.org/v0.js')).attr('href')).toBe('https://cdn.ampproject.org/v0.js')
})
it('should add custom styles before amp boilerplate styles', async () => {
const html = await renderViaHTTP(appPort, '/amp')
await validateAMP(html)
const $ = cheerio.load(html)
const order = []
$('style').toArray().forEach((i) => {
if ($(i).attr('amp-custom') === '') {
order.push('amp-custom')
}
if ($(i).attr('amp-boilerplate') === '') {
order.push('amp-boilerplate')
}
})
expect(order).toEqual(['amp-custom', 'amp-boilerplate', 'amp-boilerplate'])
})
})
describe('With AMP context', () => {
it('should render the normal page that uses the AMP hook', async () => {
const html = await renderViaHTTP(appPort, '/use-amp-hook')
expect(html).toMatch(/Hello others/)
})
it('should render the AMP page that uses the AMP hook', async () => {
const html = await renderViaHTTP(appPort, '/use-amp-hook/amp')
await validateAMP(html)
expect(html).toMatch(/Hello AMP/)
})
})
describe('canonical amphtml', () => {
it('should render link rel amphtml', async () => {
const html = await renderViaHTTP(appPort, '/use-amp-hook')
const $ = cheerio.load(html)
expect($('link[rel=amphtml]').first().attr('href')).toBe('/use-amp-hook/amp')
})
it('should render the AMP page that uses the AMP hook', async () => {
const html = await renderViaHTTP(appPort, '/use-amp-hook/amp')
const $ = cheerio.load(html)
await validateAMP(html)
expect($('link[rel=canonical]').first().attr('href')).toBe('/use-amp-hook')
})
})
})

View file

@ -0,0 +1,8 @@
import dynamic from 'next/dynamic'
const Nested2 = dynamic(() => import('./nested2'))
export default () => <div>
Nested 1
<Nested2 />
</div>

View file

@ -0,0 +1,12 @@
import dynamic from 'next/dynamic'
const BrowserLoaded = dynamic(async () => () => <div>Browser hydrated</div>, {
ssr: false
})
export default () => <div>
<div>
Nested 2
</div>
<BrowserLoaded />
</div>

View file

@ -0,0 +1,5 @@
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() => import('../../components/nested1'))
export default DynamicComponent

View file

@ -37,6 +37,26 @@ export default (context, render) => {
}
})
it('should hydrate nested chunks', async () => {
let browser
try {
browser = await webdriver(context.appPort, '/dynamic/nested')
await check(() => browser.elementByCss('body').text(), /Nested 1/)
await check(() => browser.elementByCss('body').text(), /Nested 2/)
await check(() => browser.elementByCss('body').text(), /Browser hydrated/)
const logs = await browser.log('browser')
logs.forEach(logItem => {
expect(logItem.message).not.toMatch(/Expected server HTML to contain/)
})
} finally {
if (browser) {
browser.close()
}
}
})
it('should render the component Head content', async () => {
let browser
try {

View file

@ -0,0 +1,6 @@
module.exports = {
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60
}
}

View file

@ -0,0 +1,23 @@
import Link from 'next/link'
import NextError from 'next/error'
import React from 'react'
export default class Error extends React.Component {
static getInitialProps (ctx) {
const { statusCode } = NextError.getInitialProps(ctx)
return { statusCode: statusCode || null }
}
render () {
return (
<div>
<div id='errorStatusCode'>{this.props.statusCode || 'unknown'}</div>
<p>
<Link href='/'>
<a id='errorGoHome'>go home</a>
</Link>
</p>
</div>
)
}
}

View file

@ -0,0 +1 @@
export default () => <div id='hellom8'>OK</div>

View file

@ -0,0 +1,27 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
export default (context) => {
describe('Client Navigation 404', () => {
describe('should show 404 upon client replacestate', () => {
it('should navigate the page', async () => {
const browser = await webdriver(context.appPort, '/asd')
const serverCode = await browser
.waitForElementByCss('#errorStatusCode')
.text()
await browser.waitForElementByCss('#errorGoHome').click()
await browser.waitForElementByCss('#hellom8').back()
const clientCode = await browser
.waitForElementByCss('#errorStatusCode')
.text()
expect({ serverCode, clientCode }).toMatchObject({
serverCode: '404',
clientCode: '404'
})
browser.close()
})
})
})
}

View file

@ -0,0 +1,23 @@
/* eslint-env jest */
/* global jasmine */
import { join } from 'path'
import { renderViaHTTP, findPort, launchApp, killApp } from 'next-test-utils'
// test suite
import clientNavigation from './client-navigation'
const context = {}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
describe('Client 404', () => {
beforeAll(async () => {
context.appPort = await findPort()
context.server = await launchApp(join(__dirname, '../'), context.appPort)
// pre-build page at the start
await renderViaHTTP(context.appPort, '/')
})
afterAll(() => killApp(context.server))
clientNavigation(context, (p, q) => renderViaHTTP(context.appPort, p, q))
})

View file

@ -0,0 +1 @@
module.exports = "I am sometimes found by tooling. I shouldn't be."

View file

@ -0,0 +1 @@
export default 'OK'

View file

@ -0,0 +1,5 @@
{
"name": "module-only-package",
"description": "I'm a hipster package that only ships a module entrypoint.",
"module": "./modern.js"
}

View file

@ -0,0 +1,3 @@
import messageInAPackage from 'module-only-package'
export default () => <p id='messageInAPackage'>{messageInAPackage}</p>

View file

@ -35,7 +35,8 @@ describe('Configuration', () => {
await Promise.all([
renderViaHTTP(context.appPort, '/next-config'),
renderViaHTTP(context.appPort, '/build-id'),
renderViaHTTP(context.appPort, '/webpack-css')
renderViaHTTP(context.appPort, '/webpack-css'),
renderViaHTTP(context.appPort, '/module-only-component')
])
})
afterAll(() => {

View file

@ -33,5 +33,10 @@ export default function ({ app }, suiteName, render, fetch) {
const $ = await get$('/build-id')
expect($('#buildId').text() === '-')
})
test('correctly imports a package that defines `module` but no `main` in package.json', async () => {
const $ = await get$('/module-only-content')
expect($('#messageInAPackage').text() === 'OK')
})
})
}

View file

@ -3,6 +3,12 @@ const {PHASE_DEVELOPMENT_SERVER} = require('next-server/constants')
module.exports = (phase) => {
return {
distDir: phase === PHASE_DEVELOPMENT_SERVER ? '.next-dev' : '.next',
publicRuntimeConfig: {
foo: 'foo'
},
serverRuntimeConfig: {
bar: 'bar'
},
exportPathMap: function () {
return {
'/': { page: '/' },

View file

@ -1,12 +1,20 @@
import Link from 'next/link'
import getConfig from 'next/config'
const { publicRuntimeConfig, serverRuntimeConfig } = getConfig()
export default () => (
const About = ({ bar }) => (
<div id='about-page'>
<div>
<Link href='/'>
<a>Go Back</a>
</Link>
</div>
<p>This is the About page</p>
<p>{`This is the About page ${publicRuntimeConfig.foo}${bar || ''}`}</p>
</div>
)
About.getInitialProps = async (ctx) => {
return { bar: serverRuntimeConfig.bar }
}
export default About

View file

@ -20,7 +20,7 @@ export default function (context) {
.waitForElementByCss('#about-page')
.elementByCss('#about-page p').text()
expect(text).toBe('This is the About page')
expect(text).toBe('This is the About page foo')
browser.close()
})
@ -31,7 +31,7 @@ export default function (context) {
.waitForElementByCss('#about-page')
.elementByCss('#about-page p').text()
expect(text).toBe('This is the About page')
expect(text).toBe('This is the About page foo')
browser.close()
})

View file

@ -9,6 +9,11 @@ export default function (context) {
expect(html).toMatch(/This is the home page/)
})
it('should render the about page', async () => {
const html = await renderViaHTTP(context.port, '/about')
expect(html).toMatch(/This is the About page foobar/)
})
it('should render links correctly', async () => {
const html = await renderViaHTTP(context.port, '/')
const $ = cheerio.load(html)

View file

@ -103,4 +103,12 @@ describe('On Demand Entries', () => {
}
}
})
it('should able to ping using fetch fallback', async () => {
const about = await renderViaHTTP(context.appPort, '/_next/on-demand-entries-ping', {page: '/about'})
expect(JSON.parse(about)).toEqual({success: true})
const third = await renderViaHTTP(context.appPort, '/_next/on-demand-entries-ping', {page: '/third'})
expect(JSON.parse(third)).toEqual({success: true})
})
})

Some files were not shown because too many files have changed in this diff Show more