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

Merge v3-beta in dynamic-imports

This commit is contained in:
Arunoda Susiripala 2017-05-15 09:41:42 +05:30
commit 4d0147385c
103 changed files with 2236 additions and 708 deletions

4
.gitignore vendored
View file

@ -6,10 +6,12 @@ dist
node_modules
# logs
npm-debug.log
*.log
# coverage
.nyc_output
coverage
# test output
test/**/out
.DS_Store

View file

@ -22,6 +22,7 @@ const commands = new Set([
'init',
'build',
'start',
'export',
defaultCommand
])

67
bin/next-export Normal file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env node
import { resolve, join } from 'path'
import { existsSync } from 'fs'
import parseArgs from 'minimist'
import exportApp from '../server/export'
import { printAndExit } from '../lib/utils'
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
const argv = parseArgs(process.argv.slice(2), {
alias: {
h: 'help',
s: 'silent',
o: 'outdir'
},
boolean: ['h'],
default: {
s: false,
o: null
}
})
if (argv.help) {
console.log(`
Description
Exports the application for production deployment
Usage
$ next export [options] <dir>
<dir> represents where the compiled dist folder should go.
If no directory is provided, the dist folder will be created in the current directory.
You can set a custom folder in config https://github.com/zeit/next.js#custom-configuration, otherwise it will be created inside '.next'
Options
-h - list this help
-o - set the output dir (defaults to 'out')
-s - do not print any messages to console
`)
process.exit(0)
}
const dir = resolve(argv._[0] || '.')
// Check if pages dir exists and warn if not
if (!existsSync(dir)) {
printAndExit(`> No such directory exists as the project root: ${dir}`)
}
if (!existsSync(join(dir, 'pages'))) {
if (existsSync(join(dir, '..', 'pages'))) {
printAndExit('> No `pages` directory found. Did you mean to run `next` in the parent (`../`) directory?')
}
printAndExit('> Couldn\'t find a `pages` directory. Please create one under the project root')
}
const options = {
silent: argv.silent,
outdir: argv.outdir ? resolve(argv.outdir) : resolve(dir, 'out')
}
exportApp(dir, options)
.catch((err) => {
console.error(err)
process.exit(1)
})

View file

@ -30,6 +30,8 @@ const {
location
} = window
const asPath = getURL()
const pageLoader = new PageLoader(buildId, assetPrefix)
window.__NEXT_LOADED_PAGES__.forEach(({ route, fn }) => {
pageLoader.registerPage(route, fn)
@ -68,7 +70,7 @@ export default async () => {
Component = ErrorComponent
}
router = createRouter(pathname, query, getURL(), {
router = createRouter(pathname, query, asPath, {
pageLoader,
Component,
ErrorComponent,
@ -119,7 +121,7 @@ export async function renderError (error) {
console.error(errorMessage)
if (prod) {
const initProps = { err: error, pathname, query }
const initProps = { err: error, pathname, query, asPath }
const props = await loadGetInitialProps(ErrorComponent, initProps)
ReactDOM.render(createElement(ErrorComponent, props), errorContainer)
} else {
@ -132,8 +134,8 @@ async function doRender ({ Component, props, hash, err, emitter }) {
Component !== ErrorComponent &&
lastAppProps.Component === ErrorComponent) {
// fetch props if ErrorComponent was replaced with a page component by HMR
const { pathname, query } = router
props = await loadGetInitialProps(Component, { err, pathname, query })
const { pathname, query, asPath } = router
props = await loadGetInitialProps(Component, { err, pathname, query, asPath })
}
if (emitter) {

View file

@ -0,0 +1,29 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/custom-server)
# Root static files example
## How to use
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/custom-server
cd custom-server
```
Install it and run:
```bash
npm install
npm run dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
now
```
## The idea behind the example
This example demonstrates how to serve files such as /robots.txt and /sitemap.xml from the root.

View file

@ -0,0 +1,12 @@
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"next": "latest",
"react": "^15.5.4",
"react-dom": "^15.5.4"
}
}

View file

@ -0,0 +1,7 @@
export default () => (
<ul>
<li><a href='/robots.txt'>/robots.txt</a></li>
<li><a href='/sitemap.xml'>/sitemap.xml</a></li>
<li><a href='/favicon.ico'>/favicon.ico</a></li>
</ul>
)

View file

@ -0,0 +1,30 @@
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const { join } = require('path')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare()
.then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
const rootStaticFiles = [
'/robots.txt',
'/sitemap.xml',
'/favicon.ico'
]
if (rootStaticFiles.indexOf(parsedUrl.pathname) > -1) {
const path = join(__dirname, 'static', parsedUrl.pathname)
app.serveStatic(req, res, path)
} else {
handle(req, res, parsedUrl)
}
})
.listen(3000, (err) => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://www.example.com/foo.html</loc>
</url>
</urlset>

View file

@ -7,7 +7,7 @@
"start": "next start"
},
"dependencies": {
"next": "beta"
"next": "latest"
},
"devDependencies": {
"babel-plugin-inline-react-svg": "^0.2.0"

View file

@ -4,15 +4,31 @@ import { StyleSheetServer } from 'aphrodite'
export default class MyDocument extends Document {
static async getInitialProps ({ renderPage }) {
const { html, css } = StyleSheetServer.renderStatic(() => renderPage())
return { ...html, css }
const ids = css.renderedClassNames
return { ...html, css, ids }
}
constructor (props) {
super(props)
/* Take the renderedClassNames from aphrodite (as generated
in getInitialProps) and assign them to __NEXT_DATA__ so that they
are accessible to the client for rehydration. */
const { __NEXT_DATA__, ids } = props
if (ids) {
__NEXT_DATA__.ids = this.props.ids
}
}
render () {
/* Make sure to use data-aphrodite attribute in the style tag here
so that aphrodite knows which style tag it's in control of when
the client goes to render styles. If you don't you'll get a second
<style> tag */
return (
<html>
<Head>
<title>My page</title>
<style dangerouslySetInnerHTML={{ __html: this.props.css.content }} />
<style data-aphrodite dangerouslySetInnerHTML={{ __html: this.props.css.content }} />
</Head>
<body>
<Main />

View file

@ -1,6 +1,13 @@
import React from 'react'
import { StyleSheet, css } from 'aphrodite'
if (typeof window !== 'undefined') {
/* StyleSheet.rehydrate takes an array of rendered classnames,
and ensures that the client side render doesn't generate
duplicate style definitions in the <style data-aphrodite> tag */
StyleSheet.rehydrate(window.__NEXT_DATA__.ids)
}
export default () => (
<div className={css(styles.root)}>
<h1 className={css(styles.title)}>My page</h1>

View file

@ -0,0 +1,36 @@
import { ApolloClient, createNetworkInterface } from 'react-apollo'
import fetch from 'isomorphic-fetch'
let apolloClient = null
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch
}
function create () {
return new ApolloClient({
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
networkInterface: createNetworkInterface({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute)
opts: { // Additional fetch() options like `credentials` or `headers`
credentials: 'same-origin'
}
})
})
}
export default function initApollo () {
// Make sure to create a new client for every server-side request so that data
// isn't shared between connections (which would be bad)
if (!process.browser) {
return create()
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create()
}
return apolloClient
}

View file

@ -1,28 +0,0 @@
import { ApolloClient, createNetworkInterface } from 'react-apollo'
let apolloClient = null
function _initClient (headers, initialState) {
return new ApolloClient({
initialState,
ssrMode: !process.browser,
dataIdFromObject: result => result.id || null,
networkInterface: createNetworkInterface({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
opts: {
credentials: 'same-origin'
// Pass headers here if your graphql server requires them
}
})
})
}
export const initClient = (headers, initialState = {}) => {
if (!process.browser) {
return _initClient(headers, initialState)
}
if (!apolloClient) {
apolloClient = _initClient(headers, initialState)
}
return apolloClient
}

View file

@ -0,0 +1,39 @@
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import reducers from './reducers'
let reduxStore = null
// Get the Redux DevTools extension and fallback to a no-op function
let devtools = f => f
if (process.browser && window.__REDUX_DEVTOOLS_EXTENSION__) {
devtools = window.__REDUX_DEVTOOLS_EXTENSION__()
}
function create (apollo, initialState = {}) {
return createStore(
combineReducers({ // Setup reducers
...reducers,
apollo: apollo.reducer()
}),
initialState, // Hydrate the store with server-side data
compose(
applyMiddleware(apollo.middleware()), // Add additional middleware here
devtools
)
)
}
export default function initRedux (apollo, initialState) {
// Make sure to create a new store for every server-side request so that data
// isn't shared between connections (which would be bad)
if (!process.browser) {
return create(apollo, initialState)
}
// Reuse store on the client-side
if (!reduxStore) {
reduxStore = create(apollo, initialState)
}
return reduxStore
}

View file

@ -1,18 +0,0 @@
import { createStore } from 'redux'
import getReducer from './reducer'
import createMiddleware from './middleware'
let reduxStore = null
export const initStore = (client, initialState) => {
let store
if (!process.browser || !reduxStore) {
const middleware = createMiddleware(client.middleware())
store = createStore(getReducer(client), initialState, middleware)
if (!process.browser) {
return store
}
reduxStore = store
}
return reduxStore
}

View file

@ -1,9 +0,0 @@
import { applyMiddleware, compose } from 'redux'
export default function createMiddleware (clientMiddleware) {
const middleware = applyMiddleware(clientMiddleware)
if (process.browser && window.devToolsExtension) {
return compose(middleware, window.devToolsExtension())
}
return middleware
}

View file

@ -1,7 +0,0 @@
import { combineReducers } from 'redux'
export default function getReducer (client) {
return combineReducers({
apollo: client.reducer()
})
}

View file

@ -0,0 +1,12 @@
export default {
example: (state = {}, { type, payload }) => {
switch (type) {
case 'EXAMPLE_ACTION':
return {
...state
}
default:
return state
}
}
}

View file

@ -1,56 +1,75 @@
import 'isomorphic-fetch'
import React from 'react'
import PropTypes from 'prop-types'
import { ApolloProvider, getDataFromTree } from 'react-apollo'
import { initClient } from './initClient'
import { initStore } from './initStore'
import initApollo from './initApollo'
import initRedux from './initRedux'
export default ComposedComponent => {
return class WithData extends React.Component {
static displayName = `WithData(${ComposedComponent.displayName})`
static propTypes = {
serverState: PropTypes.object.isRequired
}
export default (Component) => (
class extends React.Component {
static async getInitialProps (ctx) {
const headers = ctx.req ? ctx.req.headers : {}
const client = initClient(headers)
const store = initStore(client, client.initialState)
let serverState = {}
const props = {
url: { query: ctx.query, pathname: ctx.pathname },
...await (Component.getInitialProps ? Component.getInitialProps(ctx) : {})
// Evaluate the composed component's getInitialProps()
let composedInitialProps = {}
if (ComposedComponent.getInitialProps) {
composedInitialProps = await ComposedComponent.getInitialProps(ctx)
}
// Run all graphql queries in the component tree
// and extract the resulting data
if (!process.browser) {
const apollo = initApollo()
const redux = initRedux(apollo)
// Provide the `url` prop data in case a graphql query uses it
const url = {query: ctx.query, pathname: ctx.pathname}
// Run all graphql queries
const app = (
<ApolloProvider client={client} store={store}>
<Component {...props} />
// No need to use the Redux Provider
// because Apollo sets up the store for us
<ApolloProvider client={apollo} store={redux}>
<ComposedComponent url={url} {...composedInitialProps} />
</ApolloProvider>
)
await getDataFromTree(app)
// Extract query data from the store
const state = redux.getState()
// No need to include other initial Redux state because when it
// initialises on the client-side it'll create it again anyway
serverState = {
apollo: { // Make sure to only include Apollo's data state
data: state.apollo.data
}
}
}
const state = store.getState()
return {
initialState: {
...state,
apollo: {
data: client.getInitialState().data
}
},
headers,
...props
serverState,
...composedInitialProps
}
}
constructor (props) {
super(props)
this.client = initClient(this.props.headers, this.props.initialState)
this.store = initStore(this.client, this.props.initialState)
this.apollo = initApollo()
this.redux = initRedux(this.apollo, this.props.serverState)
}
render () {
return (
<ApolloProvider client={this.client} store={this.store}>
<Component {...this.props} />
// No need to use the Redux Provider
// because Apollo sets up the store for us
<ApolloProvider client={this.apollo} store={this.redux}>
<ComposedComponent {...this.props} />
</ApolloProvider>
)
}
}
)
}

View file

@ -1,16 +1,19 @@
{
"name": "with-apollo-and-redux",
"version": "1.0.0",
"version": "2.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"graphql": "^0.9.1",
"graphql": "^0.9.3",
"isomorphic-fetch": "^2.2.1",
"next": "latest",
"react": "^15.4.2",
"react-apollo": "^1.0.0-rc.2",
"prop-types": "^15.5.8",
"react": "^15.5.4",
"react-apollo": "^1.1.3",
"react-dom": "^15.5.4",
"redux": "^3.6.0"
},
"author": "",

View file

@ -0,0 +1,37 @@
import { ApolloClient, createNetworkInterface } from 'react-apollo'
import fetch from 'isomorphic-fetch'
let apolloClient = null
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch
}
function create (initialState) {
return new ApolloClient({
initialState,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
networkInterface: createNetworkInterface({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute)
opts: { // Additional fetch() options like `credentials` or `headers`
credentials: 'same-origin'
}
})
})
}
export default function initApollo (initialState) {
// Make sure to create a new client for every server-side request so that data
// isn't shared between connections (which would be bad)
if (!process.browser) {
return create(initialState)
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState)
}
return apolloClient
}

View file

@ -1,28 +0,0 @@
import { ApolloClient, createNetworkInterface } from 'react-apollo'
let apolloClient = null
function _initClient (headers, initialState) {
return new ApolloClient({
initialState,
ssrMode: !process.browser,
dataIdFromObject: result => result.id || null,
networkInterface: createNetworkInterface({
uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
opts: {
credentials: 'same-origin'
// Pass headers here if your graphql server requires them
}
})
})
}
export const initClient = (headers, initialState = {}) => {
if (!process.browser) {
return _initClient(headers, initialState)
}
if (!apolloClient) {
apolloClient = _initClient(headers, initialState)
}
return apolloClient
}

View file

@ -1,50 +1,66 @@
import 'isomorphic-fetch'
import React from 'react'
import PropTypes from 'prop-types'
import { ApolloProvider, getDataFromTree } from 'react-apollo'
import { initClient } from './initClient'
import initApollo from './initApollo'
export default ComposedComponent => {
return class WithData extends React.Component {
static displayName = `WithData(${ComposedComponent.displayName})`
static propTypes = {
serverState: PropTypes.object.isRequired
}
export default (Component) => (
class extends React.Component {
static async getInitialProps (ctx) {
const headers = ctx.req ? ctx.req.headers : {}
const client = initClient(headers)
let serverState = {}
const props = {
url: { query: ctx.query, pathname: ctx.pathname },
...await (Component.getInitialProps ? Component.getInitialProps(ctx) : {})
// Evaluate the composed component's getInitialProps()
let composedInitialProps = {}
if (ComposedComponent.getInitialProps) {
composedInitialProps = await ComposedComponent.getInitialProps(ctx)
}
// Run all graphql queries in the component tree
// and extract the resulting data
if (!process.browser) {
const apollo = initApollo()
// Provide the `url` prop data in case a graphql query uses it
const url = {query: ctx.query, pathname: ctx.pathname}
// Run all graphql queries
const app = (
<ApolloProvider client={client}>
<Component {...props} />
<ApolloProvider client={apollo}>
<ComposedComponent url={url} {...composedInitialProps} />
</ApolloProvider>
)
await getDataFromTree(app)
// Extract query data from the Apollo's store
const state = apollo.getInitialState()
serverState = {
apollo: { // Make sure to only include Apollo's data state
data: state.data
}
}
}
return {
initialState: {
apollo: {
data: client.getInitialState().data
}
},
headers,
...props
serverState,
...composedInitialProps
}
}
constructor (props) {
super(props)
this.client = initClient(this.props.headers, this.props.initialState)
this.apollo = initApollo(this.props.serverState)
}
render () {
return (
<ApolloProvider client={this.client}>
<Component {...this.props} />
<ApolloProvider client={this.apollo}>
<ComposedComponent {...this.props} />
</ApolloProvider>
)
}
}
)
}

View file

@ -1,16 +1,19 @@
{
"name": "with-apollo",
"version": "1.0.1",
"version": "2.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"graphql": "^0.9.1",
"graphql": "^0.9.3",
"isomorphic-fetch": "^2.2.1",
"next": "latest",
"react": "^15.4.2",
"react-apollo": "^1.0.0-rc.3"
"prop-types": "^15.5.8",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-apollo": "^1.1.3"
},
"author": "",
"license": "ISC"

View file

@ -1,5 +1,5 @@
import Document, { Head, Main, NextScript } from 'next/document'
import cxs from 'cxs'
import cxs from 'cxs/lite'
export default class MyDocument extends Document {
static async getInitialProps ({ renderPage }) {
@ -13,7 +13,7 @@ export default class MyDocument extends Document {
<html>
<Head>
<title>My page</title>
<style dangerouslySetInnerHTML={{ __html: this.props.style }} />
<style id='cxs-style' dangerouslySetInnerHTML={{ __html: this.props.style }} />
</Head>
<body>
<Main />

View file

@ -1,5 +1,13 @@
import React from 'react'
import cxs from 'cxs'
import cxs from 'cxs/lite'
// Using cxs/lite on both the server and client,
// the styles will need to be rehydrated.
if (typeof window !== 'undefined') {
const styleTag = document.getElementById('cxs-style')
const serverCss = styleTag.innerHTML
cxs.rehydrate(serverCss)
}
export default () => (
<div className={cx.root}>

View file

@ -0,0 +1,3 @@
module.exports = {
// TODO firebase client config
}

View file

@ -0,0 +1,3 @@
module.exports = {
// TODO firebase server config
}

View file

@ -1,8 +0,0 @@
module.exports = {
clientCredentials: {
// TODO firebase client config
},
serverCredentials: {
// TODO service account json here
}
}

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react'
import firebase from 'firebase'
import 'isomorphic-fetch'
import { clientCredentials } from '../firebaseCredentials'
import clientCredentials from '../credentials/client'
export default class Index extends Component {
static async getInitialProps ({req, query}) {

View file

@ -10,7 +10,7 @@ const app = next({ dev })
const handle = app.getRequestHandler()
const firebase = admin.initializeApp({
credential: admin.credential.cert(require('./firebaseCredentials').serverCredentials),
credential: admin.credential.cert(require('./credentials/server')),
databaseURL: '' // TODO database URL goes here
}, 'server')

View file

@ -6,7 +6,7 @@
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-styled-components
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-flow
cd with-flow
```
@ -27,4 +27,4 @@ now
This example shows how you can use Flow, with the transform-flow-strip-types babel plugin stripping flow type annotations from your output code.
![with-flow](with-flow.gif)
![with-flow](with-flow.gif)

View file

@ -0,0 +1,33 @@
# with-i18next example
## How to use
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-i18next
cd with-i18next
```
Install it and run:
```bash
npm install
npm run dev
```
alternatively
```bash
yarn && yarn dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
now
```
## The idea behind the example
This example shows how to add internationalisation through [i18next](https://github.com/i18next/i18next) to your NextJS app. The possibilities and features are documented in the [i18next project](http://i18next.com/translate/)

View file

@ -0,0 +1,2 @@
import { translate } from 'react-i18next'
export default translate(['common'])((props) => (<h1>{props.t('hello')}</h1>))

View file

@ -0,0 +1,19 @@
{
"name": "with-i18next",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"i18next": "^7.1.3",
"isomorphic-fetch": "^2.2.1",
"next": "*",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-i18next": "^2.2.1"
},
"author": "",
"license": "ISC"
}

View file

@ -0,0 +1,27 @@
import React, { Component } from 'react'
import { I18nextProvider } from 'react-i18next'
import startI18n from '../tools/startI18n'
import { getTranslation } from '../tools/translationHelpers'
import Title from '../components/Title'
export default class Homepage extends Component {
static async getInitialProps () {
const translations = await getTranslation('pt', 'common', 'http://localhost:3000/static/locales/')
return { translations }
}
constructor (props) {
super(props)
this.i18n = startI18n(props.translations)
}
render (props) {
return (
<I18nextProvider i18n={this.i18n}>
<Title />
</ I18nextProvider>
)
}
}

View file

@ -0,0 +1,3 @@
{
"hello": "e ae tche"
}

View file

@ -0,0 +1,11 @@
import i18n from 'i18next'
const startI18n = file => i18n.init({
fallbackLng: 'pt',
resources: file,
ns: ['common'],
defaultNS: 'common',
debug: false
})
export default startI18n

View file

@ -0,0 +1,13 @@
/* global fetch */
import 'isomorphic-fetch'
export async function getTranslation (lang, file, baseUrl) {
const response = await fetch(`${baseUrl}${lang}/${file}.json`)
const json = await response.json()
return {
[lang]: {
[file]: json
}
}
}

View file

@ -1,15 +1,16 @@
{
"env": {
"development": {
"presets": ["next/babel"]
"presets": "next/babel"
},
"production": {
"presets": ["next/babel"]
"presets": "next/babel"
},
"test": {
// next/babel does not transpile import/export syntax.
// So, using es2015 in the beginning will fix that.
"presets": ["es2015", "next/babel"]
"presets": [
["env", { "modules": "commonjs" }],
"next/babel"
]
}
}
}

View file

@ -1,8 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`With Snapshot Testing App shows "Hello world!" 1`] = `
<div
data-jsx={2648947580}>
data-jsx={2648947580}
>
<p
data-jsx={2648947580}>
data-jsx={2648947580}
>
Hello World!
</p>
</div>

View file

@ -1,15 +1,14 @@
/* global it, expect, describe */
/* eslint-env jest */
import React from 'react'
import { shallow } from 'enzyme'
import React from 'react'
import renderer from 'react-test-renderer'
import App from '../pages/index.js'
describe('With Enzyme', () => {
it('App shows "Hello world!"', () => {
const app = shallow(
<App />
)
const app = shallow(<App />)
expect(app.find('p').text()).toEqual('Hello World!')
})

View file

@ -2,21 +2,19 @@
"name": "with-jest",
"dependencies": {
"next": "latest",
"react": "^15.4.2",
"react-dom": "^15.4.2"
"react": "^15.5.4",
"react-dom": "^15.5.4"
},
"devDependencies": {
"enzyme": "^2.8.2",
"jest": "^20.0.0",
"react-addons-test-utils": "^15.5.1",
"react-test-renderer": "^15.5.4"
},
"scripts": {
"test": "jest",
"dev": "next",
"build": "next build",
"start": "next start"
},
"devDependencies": {
"babel-jest": "^18.0.0",
"enzyme": "^2.5.1",
"jest-cli": "^18.0.0",
"react-addons-test-utils": "^15.4.2",
"babel-preset-es2015": "^6.22.0",
"react-test-renderer": "^15.4.2"
}
}

View file

@ -0,0 +1,11 @@
module.exports = {
webpack: (config) => {
// Remove minifed react aliases for material-ui so production builds work
if (config.resolve.alias) {
delete config.resolve.alias.react
delete config.resolve.alias['react-dom']
}
return config
}
}

View file

@ -1,6 +1,6 @@
{
"dependencies": {
"material-ui": "^0.17.4",
"material-ui": "^0.18.0",
"next": "latest",
"react": "^15.5.4",
"react-dom": "^15.5.4",

View file

@ -7,14 +7,11 @@ import getMuiTheme from 'material-ui/styles/getMuiTheme'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import injectTapEventPlugin from 'react-tap-event-plugin'
// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
try {
if (typeof window !== 'undefined') {
injectTapEventPlugin()
}
} catch (e) {
// do nothing
// Make sure react-tap-event-plugin only gets injected once
// Needed for material-ui
if (!process.tapEventInjected) {
injectTapEventPlugin()
process.tapEventInjected = true
}
const styles = {
@ -24,17 +21,23 @@ const styles = {
}
}
const _muiTheme = getMuiTheme({
const muiTheme = {
palette: {
accent1Color: deepOrange500
}
})
}
class Main extends Component {
static getInitialProps ({ req }) {
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
const isServer = !!req
return {isServer, userAgent}
// Ensures material-ui renders the correct css prefixes server-side
let userAgent
if (process.browser) {
userAgent = navigator.userAgent
} else {
userAgent = req.headers['user-agent']
}
return { userAgent }
}
constructor (props, context) {
@ -58,6 +61,8 @@ class Main extends Component {
}
render () {
const { userAgent } = this.props
const standardActions = (
<FlatButton
label='Ok'
@ -66,12 +71,8 @@ class Main extends Component {
/>
)
const { userAgent } = this.props
/* https://github.com/callemall/material-ui/issues/3336 */
const muiTheme = getMuiTheme(getMuiTheme({userAgent: userAgent}), _muiTheme)
return (
<MuiThemeProvider muiTheme={muiTheme}>
<MuiThemeProvider muiTheme={getMuiTheme({userAgent, ...muiTheme})}>
<div style={styles.container}>
<Dialog
open={this.state.open}

View file

@ -6,7 +6,7 @@
},
"dependencies": {
"express": "^4.15.2",
"next": "^2.0.0",
"next": "latest",
"next-url-prettifier": "^1.0.2",
"prop-types": "^15.5.6",
"react": "^15.4.2",

View file

@ -8,7 +8,7 @@ export default class GreetingPage extends React.Component {
return {lang, name}
}
renderSwitchLangageLink () {
renderSwitchLanguageLink () {
const {lang, name} = this.props
const switchLang = lang === 'fr' ? 'en' : 'fr'
return (
@ -23,7 +23,7 @@ export default class GreetingPage extends React.Component {
return (
<div>
<h1>{lang === 'fr' ? 'Bonjour' : 'Hello'} {name}</h1>
<div>{this.renderSwitchLangageLink()}</div>
<div>{this.renderSwitchLanguageLink()}</div>
</div>
)
}

View file

@ -0,0 +1,9 @@
module.exports = {
webpack: function (config) {
if (config.resolve.alias) {
delete config.resolve.alias['react']
delete config.resolve.alias['react-dom']
}
return config
}
}

View file

@ -28,11 +28,11 @@ now
Usually splitting your app state into `pages` feels natural but sometimes you'll want to have global state for your app. This is an example on how you can use redux that also works with our universal rendering approach. This is just a way you can do it but it's not the only one.
In this example we are going to display a digital clock that updates every second. The first render is happening in the server and then the browser will take over. To illustrate this, the server rendered clock will have a different background color than the client one.
In the first example we are going to display a digital clock that updates every second. The first render is happening in the server and then the browser will take over. To illustrate this, the server rendered clock will have a different background color than the client one.
![](http://i.imgur.com/JCxtWSj.gif)
Our page is located at `pages/index.js` so it will map the route `/`. To get the initial data for rendering we are implementing the static method `getInitialProps`, initializing the redux store and dispatching the required actions until we are ready to return the initial state to be rendered. Since the component is wrapped with `next-react-wrapper`, the component is automatically connected to Redux and wrapped with `react-redux Provider`, that allows us to access redux state immediately and send the store down to children components so they can access to the state when required.
Our page is located at `pages/index.js` so it will map the route `/`. To get the initial data for rendering we are implementing the static method `getInitialProps`, initializing the redux store and dispatching the required actions until we are ready to return the initial state to be rendered. Since the component is wrapped with `next-redux-wrapper`, the component is automatically connected to Redux and wrapped with `react-redux Provider`, that allows us to access redux state immediately and send the store down to children components so they can access to the state when required.
For safety it is recommended to wrap all pages, no matter if they use Redux or not, so that you should not care about it anymore in all child components.
@ -43,3 +43,7 @@ To pass the initial state from the server to the client we pass it as a prop cal
The trick here for supporting universal redux is to separate the cases for the client and the server. When we are on the server we want to create a new store every time, otherwise different users data will be mixed up. If we are in the client we want to use always the same store. That's what we accomplish on `store.js`
The clock, under `components/Clock.js`, has access to the state using the `connect` function from `react-redux`. In this case Clock is a direct child from the page but it could be deep down the render tree.
The second example, under `components/AddCount.js`, shows a simple add counter function with a class component implementing a common redux pattern of mapping state and props. Again, the first render is happening in the server and instead of starting the count at 0, it will dispatch an action in redux that starts the count at 1. This continues to highlight how each navigation triggers a server render first and then a client render second, when you navigate between pages.
For simplicity and readability, Reducers, Actions, and Store creators are all in the same file: `store.js`

View file

@ -0,0 +1,35 @@
import React, {Component} from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { addCount } from '../store'
class AddCount extends Component {
add = () => {
this.props.addCount()
}
render () {
const { count } = this.props
return (
<div>
<style jsx>{`
div {
padding: 0 0 20px 0;
}
`}</style>
<h1>AddCount: <span>{count}</span></h1>
<button onClick={this.add}>Add To Count</button>
</div>
)
}
}
const mapStateToProps = ({ count }) => ({ count })
const mapDispatchToProps = (dispatch) => {
return {
addCount: bindActionCreators(addCount, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AddCount)

View file

@ -1,12 +1,14 @@
import Link from 'next/link'
import { connect } from 'react-redux'
import Clock from './Clock'
import AddCount from './AddCount'
export default connect(state => state)(({ title, linkTo, lastUpdate, light }) => {
return (
<div>
<h1>{title}</h1>
<Clock lastUpdate={lastUpdate} light={light} />
<AddCount />
<nav>
<Link href={linkTo}><a>Navigate</a></Link>
</nav>

View file

@ -1,16 +1,19 @@
import React from 'react'
import { initStore, startClock } from '../store'
import { bindActionCreators } from 'redux'
import { initStore, startClock, addCount, serverRenderClock } from '../store'
import withRedux from 'next-redux-wrapper'
import Page from '../components/Page'
class Counter extends React.Component {
static getInitialProps ({ store, isServer }) {
store.dispatch({ type: 'TICK', light: !isServer, ts: Date.now() })
store.dispatch(serverRenderClock(isServer))
store.dispatch(addCount())
return { isServer }
}
componentDidMount () {
this.timer = this.props.dispatch(startClock())
this.timer = this.props.startClock()
}
componentWillUnmount () {
@ -24,4 +27,11 @@ class Counter extends React.Component {
}
}
export default withRedux(initStore)(Counter)
const mapDispatchToProps = (dispatch) => {
return {
addCount: bindActionCreators(addCount, dispatch),
startClock: bindActionCreators(startClock, dispatch)
}
}
export default withRedux(initStore, null, mapDispatchToProps)(Counter)

View file

@ -1,20 +1,18 @@
import React from 'react'
import { initStore, startClock } from '../store'
import { bindActionCreators } from 'redux'
import { initStore, startClock, addCount, serverRenderClock } from '../store'
import withRedux from 'next-redux-wrapper'
import Page from '../components/Page'
class Counter extends React.Component {
static getInitialProps ({ store, isServer }) {
store.dispatch({ type: 'TICK', light: !isServer, ts: Date.now() })
store.dispatch(serverRenderClock(isServer))
store.dispatch(addCount())
return { isServer }
}
componentDidMount () {
this.timer = this.props.dispatch(startClock())
}
componentWillUnmount () {
clearInterval(this.timer)
this.timer = this.props.startClock()
}
render () {
@ -24,4 +22,11 @@ class Counter extends React.Component {
}
}
export default withRedux(initStore)(Counter)
const mapDispatchToProps = (dispatch) => {
return {
addCount: bindActionCreators(addCount, dispatch),
startClock: bindActionCreators(startClock, dispatch)
}
}
export default withRedux(initStore, null, mapDispatchToProps)(Counter)

View file

@ -1,17 +1,43 @@
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
export const reducer = (state = { lastUpdate: 0, light: false }, action) => {
const exampleInitialState = {
lastUpdate: 0,
light: false,
count: 0
}
export const actionTypes = {
ADD: 'ADD',
TICK: 'TICK'
}
// REDUCERS
export const reducer = (state = exampleInitialState, action) => {
switch (action.type) {
case 'TICK': return { lastUpdate: action.ts, light: !!action.light }
case actionTypes.TICK:
return Object.assign({}, state, { lastUpdate: action.ts, light: !!action.light })
case actionTypes.ADD:
return Object.assign({}, state, {
count: state.count + 1
})
default: return state
}
}
// ACTIONS
export const serverRenderClock = (isServer) => dispatch => {
return dispatch({ type: actionTypes.TICK, light: !isServer, ts: Date.now() })
}
export const startClock = () => dispatch => {
return setInterval(() => dispatch({ type: 'TICK', light: true, ts: Date.now() }), 800)
}
export const initStore = (initialState) => {
export const addCount = () => dispatch => {
return dispatch({ type: actionTypes.ADD })
}
export const initStore = (initialState = exampleInitialState) => {
return createStore(reducer, initialState, applyMiddleware(thunkMiddleware))
}

View file

@ -27,7 +27,7 @@ now
## The idea behind the example
This example, just like `with-react` and `with-mobx` examples, shows how to manage a global state in your web-application.
This example, just like `with-redux` and `with-mobx` examples, shows how to manage a global state in your web-application.
In this case we are using [refnux](https://github.com/algesten/refnux) which is an alternative, simpler, purely functional store state manager.
We have two very similar pages (page1.js, page2.js). They both

View file

@ -7,7 +7,7 @@
"start": "next start"
},
"dependencies": {
"next": "next@beta",
"next": "latest",
"react": "^15.4.2",
"react-dom": "^15.4.2"
},

View file

@ -0,0 +1,8 @@
{
"presets": [
"next/babel"
],
"plugins": [
["styled-components", { "ssr": true, "displayName": true, "preprocess": false } ]
]
}

View file

@ -30,6 +30,4 @@ This example features how you use a different styling solution than [styled-jsx]
For this purpose we are extending the `<Document />` and injecting the server side rendered styles into the `<head>`.
## Notes:
- On initial install, you may see a server-side error: `TypeError: Cannot read property 'cssRules' of undefined when using this line of code` until you actually render a `styled-component`. I have submitted a PR to fix this issue with them [here](https://github.com/styled-components/styled-components/pull/391). For the time being, make sure you render at least one `styled-component` when you use this.
# WARNING This example uses styled-components v2 which is currently in BETA

View file

@ -7,10 +7,12 @@
"start": "next start"
},
"dependencies": {
"babel-plugin-styled-components": "^1.1.4",
"babel-preset-stage-0": "^6.24.1",
"next": "latest",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"styled-components": "^1.4.4"
"styled-components": "^2.0.0-17"
},
"author": "",
"license": "ISC"

View file

@ -1,23 +1,21 @@
import Document, { Head, Main, NextScript } from 'next/document'
import styleSheet from 'styled-components/lib/models/StyleSheet'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document {
static async getInitialProps ({ renderPage }) {
const page = renderPage()
const styles = (
<style dangerouslySetInnerHTML={{ __html: styleSheet.rules().map(rule => rule.cssText).join('\n') }} />
)
return { ...page, styles }
}
render () {
const sheet = new ServerStyleSheet()
const main = sheet.collectStyles(<Main />)
const styleTags = sheet.getStyleElement()
return (
<html>
<Head>
<title>My page</title>
{styleTags}
</Head>
<body>
<Main />
<div className='root'>
{main}
</div>
<NextScript />
</body>
</html>

View file

@ -8,7 +8,7 @@
"bundle:view": "webpack-bundle-analyzer .next/stats.json"
},
"dependencies": {
"next": "beta",
"next": "latest",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"webpack-bundle-analyzer": "^2.3.0"

View file

@ -1,7 +1,9 @@
/* global __NEXT_DATA__ */
import { resolve, format, parse } from 'url'
import React, { Component, Children } from 'react'
import PropTypes from 'prop-types'
import Router from './router'
import Router, { _rewriteUrlForNextExport } from './router'
import { warn, execOnce, getLocationOrigin } from './utils'
export default class Link extends Component {
@ -122,6 +124,15 @@ export default class Link extends Component {
props.href = as || href
}
// Add the ending slash to the paths. So, we can serve the
// "<page>/index.html" directly.
if (
typeof __NEXT_DATA__ !== 'undefined' &&
__NEXT_DATA__.nextExport
) {
props.href = _rewriteUrlForNextExport(props.href)
}
return React.cloneElement(child, props)
}
}

View file

@ -1,4 +1,4 @@
/* global window, document */
/* global window, document, __NEXT_DATA__ */
import mitt from 'mitt'
const webpackModule = module
@ -21,8 +21,10 @@ export default class PageLoader {
if (route[0] !== '/') {
throw new Error('Route name should start with a "/"')
}
route = route.replace(/index$/, '')
return route.replace(/index$/, '')
if (route === '/') return route
return route.replace(/\/$/, '')
}
loadPage (route) {
@ -49,6 +51,12 @@ export default class PageLoader {
this.pageRegisterEvents.on(route, fire)
// If the page is loading via SSR, we need to wait for it
// rather downloading it again.
if (document.getElementById(`__NEXT_PAGE__${route}`)) {
return
}
// Load the script if not asked to load yet.
if (!this.loadingRoutes[route]) {
this.loadScript(route)
@ -60,6 +68,10 @@ export default class PageLoader {
loadScript (route) {
route = this.normalizeRoute(route)
if (__NEXT_DATA__.nextExport) {
route = route === '/' ? '/index.js' : `${route}/index.js`
}
const script = document.createElement('script')
const url = `${this.assetPrefix}/_next/${encodeURIComponent(this.buildId)}/page${route}`
script.src = url
@ -123,5 +135,10 @@ export default class PageLoader {
route = this.normalizeRoute(route)
delete this.pageCache[route]
delete this.loadingRoutes[route]
const script = document.getElementById(`__NEXT_PAGE__${route}`)
if (script) {
script.parentNode.removeChild(script)
}
}
}

View file

@ -13,7 +13,7 @@ const SingletonRouter = {
}
// Create public properties and methods of the router in the SingletonRouter
const propertyFields = ['components', 'pathname', 'route', 'query']
const propertyFields = ['components', 'pathname', 'route', 'query', 'asPath']
const coreMethodFields = ['push', 'replace', 'reload', 'back', 'prefetch']
const routerEvents = ['routeChangeStart', 'beforeHistoryChange', 'routeChangeComplete', 'routeChangeError']
@ -85,3 +85,23 @@ export function _notifyBuildIdMismatch (nextRoute) {
window.location.href = nextRoute
}
}
export function _rewriteUrlForNextExport (url) {
// If there are no query strings
if (!/\?/.test(url)) {
return rewritePath(url)
}
const [path, qs] = url.split('?')
const newPath = rewritePath(path)
return `${newPath}?${qs}`
function rewritePath (path) {
// If ends with slash simply return that path
if (/\/$/.test(path)) {
return path
}
return `${path}/`
}
}

View file

@ -1,9 +1,11 @@
/* global __NEXT_DATA__ */
import { parse, format } from 'url'
import mitt from 'mitt'
import shallowEquals from '../shallow-equals'
import PQueue from '../p-queue'
import { loadGetInitialProps, getURL } from '../utils'
import { _notifyBuildIdMismatch } from './'
import { _notifyBuildIdMismatch, _rewriteUrlForNextExport } from './'
export default class Router {
constructor (pathname, query, as, { pageLoader, Component, ErrorComponent, err } = {}) {
@ -27,7 +29,7 @@ export default class Router {
this.ErrorComponent = ErrorComponent
this.pathname = pathname
this.query = query
this.as = as
this.asPath = as
this.subscriptions = new Set()
this.componentLoadCancel = null
this.onPopState = this.onPopState.bind(this)
@ -118,7 +120,13 @@ export default class Router {
// If url and as provided as an object representation,
// we'll format them into the string version here.
const url = typeof _url === 'object' ? format(_url) : _url
const as = typeof _as === 'object' ? format(_as) : _as
let as = typeof _as === 'object' ? format(_as) : _as
// Add the ending slash to the paths. So, we can serve the
// "<page>/index.html" directly for the SSR page.
if (__NEXT_DATA__.nextExport) {
as = _rewriteUrlForNextExport(as)
}
this.abortComponentLoad(as)
const { pathname, query } = parse(url, true)
@ -190,7 +198,7 @@ export default class Router {
}
const { Component } = routeInfo
const ctx = { pathname, query }
const ctx = { pathname, query, asPath: as }
routeInfo.props = await this.getInitialProps(Component, ctx)
this.components[route] = routeInfo
@ -229,13 +237,13 @@ export default class Router {
this.route = route
this.pathname = pathname
this.query = query
this.as = as
this.asPath = as
this.notify(data)
}
onlyAHashChange (as) {
if (!this.as) return false
const [ oldUrlNoHash ] = this.as.split('#')
if (!this.asPath) return false
const [ oldUrlNoHash ] = this.asPath.split('#')
const [ newUrlNoHash, newHash ] = as.split('#')
// If the urls are change, there's more than a hash change

View file

@ -1,6 +1,6 @@
{
"name": "next",
"version": "2.2.0",
"version": "2.3.1",
"description": "Minimalistic framework for server-rendered React applications",
"main": "./dist/server/next.js",
"license": "MIT",
@ -54,7 +54,7 @@
"babel-plugin-transform-es2015-modules-commonjs": "6.24.1",
"babel-plugin-transform-object-rest-spread": "6.22.0",
"babel-plugin-transform-react-jsx-source": "6.22.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.5",
"babel-plugin-transform-runtime": "6.22.0",
"babel-preset-env": "1.3.3",
"babel-preset-react": "6.24.1",
@ -80,47 +80,51 @@
"mv": "2.1.1",
"mz": "2.6.0",
"path-match": "1.2.4",
"pkg-up": "1.0.0",
"prop-types": "15.5.7",
"pkg-up": "2.0.0",
"prop-types": "15.5.10",
"react-hot-loader": "3.0.0-beta.6",
"send": "0.15.2",
"source-map-support": "0.4.14",
"source-map-support": "0.4.15",
"strip-ansi": "3.0.1",
"styled-jsx": "0.5.7",
"touch": "1.0.0",
"unfetch": "2.1.2",
"url": "0.11.0",
"uuid": "3.0.1",
"webpack": "2.4.0",
"webpack": "2.5.1",
"webpack-dev-middleware": "1.10.2",
"webpack-hot-middleware": "2.18.0",
"write-file-webpack-plugin": "4.0.2"
},
"devDependencies": {
"babel-eslint": "7.2.3",
"babel-jest": "18.0.0",
"babel-plugin-istanbul": "4.1.1",
"babel-jest": "20.0.1",
"babel-plugin-istanbul": "4.1.3",
"babel-plugin-transform-remove-strict-mode": "0.0.2",
"babel-preset-es2015": "6.24.1",
"benchmark": "2.1.4",
"cheerio": "0.22.0",
"chromedriver": "2.29.0",
"coveralls": "2.13.0",
"coveralls": "2.13.1",
"cross-env": "4.0.0",
"fly": "2.0.5",
"express": "4.15.2",
"cross-env": "5.0.0",
"fly": "2.0.6",
"fly-babel": "2.1.1",
"fly-clear": "1.0.1",
"fly-esnext": "2.0.1",
"fly-watch": "1.1.1",
"husky": "0.13.3",
"jest-cli": "19.0.1",
"jest-cli": "20.0.1",
"lint-staged": "^3.4.0",
"node-fetch": "1.6.3",
"node-notifier": "5.1.2",
"nyc": "10.2.0",
"nyc": "10.3.2",
"react": "15.5.3",
"react-dom": "15.5.3",
"recursive-copy": "^2.0.6",
"standard": "9.0.2",
"walk": "^2.3.9",
"wd": "1.2.0"
},
"peerDependencies": {

109
readme.md
View file

@ -6,12 +6,15 @@
Next.js is a minimalistic framework for server-rendered React applications.
**Visit https://learnnextjs.com to get started with Next.js.**
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<!-- https://github.com/thlorenz/doctoc -->
- [How to use](#how-to-use)
- [Getting Started](#getting-started)
- [Setup](#setup)
- [Automatic code splitting](#automatic-code-splitting)
- [CSS](#css)
@ -37,6 +40,8 @@ Next.js is a minimalistic framework for server-rendered React applications.
- [Customizing babel config](#customizing-babel-config)
- [CDN support with Asset Prefix](#cdn-support-with-asset-prefix)
- [Production deployment](#production-deployment)
- [Static HTML export](#static-html-export)
- [Recipes](#recipes)
- [FAQ](#faq)
- [Contributing](#contributing)
- [Authors](#authors)
@ -45,10 +50,6 @@ Next.js is a minimalistic framework for server-rendered React applications.
## How to use
### Getting Started
A step by step interactive guide of next features is available at [learnnextjs.com](https://learnnextjs.com/)
### Setup
Install it:
@ -130,17 +131,24 @@ export default () => (
}
}
`}</style>
<style global jsx>{`
body {
background: black;
}
`}</style>
</div>
)
```
Please see the [styled-jsx documentation](https://github.com/zeit/styled-jsx) for more examples.
#### CSS-in-JS
<p><details>
<summary>
<b>Examples</b>
</summary>
<ul><li><a href="./examples/with-styled-components">Styled components</a></li><li><a href="./examples/with-styletron">Styletron</a></li><li><a href="./examples/with-glamor">Glamor</a></li><li><a href="./examples/with-cxs">Cxs</a></li><li><a href="./examples/with-aphrodite">Aphrodite</a></li><li><a href="./examples/with-fela">Fela</a></li></ul>
<ul><li><a href="./examples/with-styled-components">Styled components</a></li><li><a href="./examples/with-styletron">Styletron</a></li><li><a href="./examples/with-glamor">Glamor</a></li><li><a href="./examples/with-glamorous">Glamorous</a></li><li><a href="./examples/with-cxs">Cxs</a></li><li><a href="./examples/with-aphrodite">Aphrodite</a></li><li><a href="./examples/with-fela">Fela</a></li></ul>
</details></p>
It's possible to use any existing CSS-in-JS solution. The simplest one is inline styles:
@ -239,6 +247,7 @@ export default Page
- `pathname` - path section of URL
- `query` - query string section of URL parsed as an object
- `asPath` - the actual url path
- `req` - HTTP request object (server only)
- `res` - HTTP response object (server only)
- `jsonPageRes` - [Fetch Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object (client only)
@ -284,6 +293,7 @@ Each top-level component receives a `url` property with the following API:
- `pathname` - `String` of the current path excluding the query string
- `query` - `Object` with the parsed query string. Defaults to `{}`
- `asPath` - `String` of the actual path (including the query) shows in the browser
- `push(url, as=url)` - performs a `pushState` call with the given url
- `replace(url, as=url)` - performs a `replaceState` call with the given url
@ -703,6 +713,8 @@ The `ctx` object is equivalent to the one received in all [`getInitialProps`](#f
- `renderPage` (`Function`) a callback that executes the actual React rendering logic (synchronously). It's useful to decorate this function in order to support server-rendering wrappers like Aphrodite's [`renderStatic`](https://github.com/Khan/aphrodite#server-side-rendering)
__Note: React-components outside of `<Main />` will not be initialised by the browser. If you need shared components in all your pages (like a menu or a toolbar), do _not_ add application logic here, but take a look at [this example](https://github.com/zeit/next.js/tree/master/examples/layout-component).__
### Custom error handling
404 or 500 errors are handled both client and server side by a default component `error.js`. If you wish to override it, define a `_error.js`:
@ -762,9 +774,15 @@ In order to extend our usage of `webpack`, you can define a function that extend
module.exports = {
webpack: (config, { dev }) => {
   // Perform customizations to config
   
   // Important: return the modified config
// Perform customizations to webpack config
// Important: return the modified config
return config
},
webpackDevMiddleware: (config) => {
// Perform customizations to webpack dev middleware config
// Important: return the modified config
return config
}
}
@ -841,6 +859,79 @@ Next.js can be deployed to other hosting solutions too. Please have a look at th
Note: we recommend putting `.next`, or your custom dist folder (Please have a look at ['Custom Config'](You can set a custom folder in config https://github.com/zeit/next.js#custom-configuration.)), in `.npmignore` or `.gitignore`. Otherwise, use `files` or `now.files` to opt-into a whitelist of files you want to deploy (and obviously exclude `.next` or your custom dist folder)
## Static HTML export
This is a way to run your Next.js app as a standalone static app without any Node.js server. The export app supports almost every feature of Next.js including dyanmic urls, prefetching, preloading and dynamic imports.
### Usage
Simply develop your app as you normally do with Next.js. Then create a custom Next.js [config](https://github.com/zeit/next.js#custom-configuration) as shown below:
```js
// next.config.js
module.exports = {
exportPathMap: function () {
return {
"/": { page: "/" },
"/about": { page: "/about" },
"/p/hello-nextjs": { page: "/post", query: { title: "hello-nextjs" } },
"/p/learn-nextjs": { page: "/post", query: { title: "learn-nextjs" } },
"/p/deploy-nextjs": { page: "/post", query: { title: "deploy-nextjs" } }
}
},
}
```
In that, you specify what are the pages you need to export as static HTML.
Then simply run these commands:
```sh
next build
next export
```
For that you may need to add a NPM script to `package.json` like this:
```json
{
"scripts": {
"build": "next build && next export"
}
}
```
And run it at once with:
```sh
npm run build
```
Then you've a static version of your app in the “out" directory.
> You can also customize the output directory. For that run `next export -h` for the help.
Now you can deploy that directory to any static hosting service.
For an example, simply visit the “out” directory and run following command to deploy your app to [ZEIT now](https://zeit.co/now).
```sh
now
```
### Limitation
With next export, we build HTML version of your app when you run the command `next export`. In that time, we'll run the `getInitialProps` functions of your pages.
So, you could only use `pathname`, `query` and `asPath` fields of the `context` object passed to `getInitialProps`. You can't use `req` or `res` fields.
> Basically, you won't be able to render HTML content dynamically as we pre-build HTML files. If you need that, you need run your app with `next start`.
## Recipes
- [Setting up 301 redirects](https://www.raygesualdo.com/posts/301-redirects-with-nextjs/)
## FAQ
<details>

View file

@ -9,7 +9,7 @@ const envPlugins = {
]
}
const plugins = envPlugins[process.env.NODE_ENV] || []
const plugins = envPlugins[process.env.NODE_ENV] || envPlugins['development']
module.exports = {
presets: [

View file

@ -12,7 +12,17 @@ export default class PagesPlugin {
pages.forEach((chunk) => {
const page = compilation.assets[chunk.name]
const pageName = matchRouteName.exec(chunk.name)[1]
const routeName = `/${pageName.replace(/[/\\]?index$/, '')}`
let routeName = `/${pageName.replace(/[/\\]?index$/, '')}`
// We need to convert \ into / when we are in windows
// to get the proper route name
// Here we need to do windows check because it's possible
// to have "\" in the filename in unix.
// Anyway if someone did that, he'll be having issues here.
// But that's something we cannot avoid.
if (/^win/.test(process.platform)) {
routeName = routeName.replace(/\\/g, '/')
}
const content = page.source()
const newContent = `

View file

@ -75,6 +75,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false,
}
const plugins = [
new webpack.IgnorePlugin(/(precomputed)/, /node_modules.+(elliptic)/),
new webpack.LoaderOptionsPlugin({
options: {
context: dir,
@ -299,10 +300,19 @@ export default async function createCompiler (dir, { dev = false, quiet = false,
module: {
rules
},
devtool: dev ? 'inline-source-map' : false,
devtool: dev ? 'cheap-module-inline-source-map' : false,
performance: { hints: false }
}
if (!dev) {
// We do this to use the minified version of React in production.
// This will significant file size redution when turned off uglifyjs.
webpackConfig.resolve.alias = {
'react': require.resolve('react/dist/react.min.js'),
'react-dom': require.resolve('react-dom/dist/react-dom.min.js')
}
}
if (config.webpack) {
console.log('> Using "webpack" config function defined in next.config.js.')
webpackConfig = await config.webpack(webpackConfig, { dev })

View file

@ -5,6 +5,7 @@ const cache = new Map()
const defaultConfig = {
webpack: null,
webpackDevMiddleware: null,
poweredByHeader: true,
distDir: '.next',
assetPrefix: ''

View file

@ -80,11 +80,12 @@ export class Head extends Component {
render () {
const { head, styles, __NEXT_DATA__ } = this.context._documentProps
const { pathname, buildId, assetPrefix } = __NEXT_DATA__
const { pathname, buildId, assetPrefix, nextExport } = __NEXT_DATA__
const pagePathname = getPagePathname(pathname, nextExport)
return <head>
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pathname}`} as='script' />
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error`} as='script' />
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} as='script' />
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error/index.js`} as='script' />
{this.getPreloadDynamicChunks()}
{this.getPreloadMainLinks()}
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
@ -122,6 +123,7 @@ export class NextScript extends Component {
return (
<script
key={filename}
type='text/javascript'
src={`${assetPrefix}/_next/${hash}/${filename}`}
{...additionalProps}
@ -132,18 +134,16 @@ export class NextScript extends Component {
getScripts () {
const { dev } = this.context._documentProps
if (dev) {
return (
<div>
{ this.getChunkScript('manifest.js') }
{ this.getChunkScript('commons.js') }
{ this.getChunkScript('main.js') }
</div>
)
return [
this.getChunkScript('manifest.js'),
this.getChunkScript('commons.js'),
this.getChunkScript('main.js')
]
}
// In the production mode, we have a single asset with all the JS content.
// So, we can load the script with async
return this.getChunkScript('app.js', { async: true })
return [this.getChunkScript('app.js', { async: true })]
}
getDynamicChunks () {
@ -165,7 +165,8 @@ export class NextScript extends Component {
render () {
const { staticMarkup, __NEXT_DATA__, chunks } = this.context._documentProps
const { pathname, buildId, assetPrefix } = __NEXT_DATA__
const { pathname, nextExport, buildId, assetPrefix } = __NEXT_DATA__
const pagePathname = getPagePathname(pathname, nextExport)
__NEXT_DATA__.chunks = chunks
@ -186,10 +187,16 @@ export class NextScript extends Component {
}
`
}} />}
<script async type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page${pathname}`} />
<script async type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page/_error`} />
<script async id={`__NEXT_PAGE__${pathname}`} type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} />
<script async id={`__NEXT_PAGE__/_error`} type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page/_error/index.js`} />
{staticMarkup ? null : this.getDynamicChunks()}
{staticMarkup ? null : this.getScripts()}
</div>
}
}
function getPagePathname (pathname, nextExport) {
if (!nextExport) return pathname
if (pathname === '/') return '/index.js'
return `${pathname}/index.js`
}

136
server/export.js Normal file
View file

@ -0,0 +1,136 @@
import del from 'del'
import cp from 'recursive-copy'
import mkdirp from 'mkdirp-then'
import walk from 'walk'
import { resolve, join, dirname, sep } from 'path'
import { existsSync, readFileSync, writeFileSync } from 'fs'
import getConfig from './config'
import { renderToHTML } from './render'
import { printAndExit } from '../lib/utils'
export default async function (dir, options) {
dir = resolve(dir)
const outDir = options.outdir
const nextDir = join(dir, '.next')
log(` Exporting to: ${outDir}\n`)
if (!existsSync(nextDir)) {
console.error('Build your with "next build" before running "next start".')
process.exit(1)
}
const config = getConfig(dir)
const buildId = readFileSync(join(nextDir, 'BUILD_ID'), 'utf8')
const buildStats = require(join(nextDir, 'build-stats.json'))
// Initialize the output directory
await del(outDir)
await mkdirp(join(outDir, '_next', buildStats['app.js'].hash))
await mkdirp(join(outDir, '_next', buildId))
// Copy files
await cp(
join(nextDir, 'app.js'),
join(outDir, '_next', buildStats['app.js'].hash, 'app.js')
)
// Copy static directory
if (existsSync(join(dir, 'static'))) {
log(' copying "static" directory')
await cp(
join(dir, 'static'),
join(outDir, 'static')
)
}
await copyPages(nextDir, outDir, buildId)
// Get the exportPathMap from the `next.config.js`
if (typeof config.exportPathMap !== 'function') {
printAndExit(
'> Could not found "exportPathMap" function inside "next.config.js"\n' +
'> "next export" uses that function build html pages.'
)
}
const exportPathMap = await config.exportPathMap()
const exportPaths = Object.keys(exportPathMap)
// Start the rendering process
const renderOpts = {
dir,
buildStats,
buildId,
nextExport: true,
assetPrefix: config.assetPrefix.replace(/\/$/, ''),
dev: false,
staticMarkup: false,
hotReloader: null
}
// We need this for server rendering the Link component.
global.__NEXT_DATA__ = {
nextExport: true
}
for (const path of exportPaths) {
log(` exporing path: ${path}`)
const { page, query } = exportPathMap[path]
const req = { url: path }
const res = {}
const htmlFilename = path === '/' ? 'index.html' : `${path}${sep}index.html`
const baseDir = join(outDir, dirname(htmlFilename))
const htmlFilepath = join(outDir, htmlFilename)
await mkdirp(baseDir)
const html = await renderToHTML(req, res, page, query, renderOpts)
writeFileSync(htmlFilepath, html, 'utf8')
}
// Add an empty line to the console for the better readability.
log('')
function log (message) {
if (options.silent) return
console.log(message)
}
}
function copyPages (nextDir, outDir, buildId) {
// TODO: do some proper error handling
return new Promise((resolve, reject) => {
const nextBundlesDir = join(nextDir, 'bundles', 'pages')
const walker = walk.walk(nextBundlesDir, { followLinks: false })
walker.on('file', (root, stat, next) => {
const filename = stat.name
const fullFilePath = `${root}${sep}${filename}`
const relativeFilePath = fullFilePath.replace(nextBundlesDir, '')
// We should not expose this page to the client side since
// it has no use in the client side.
if (relativeFilePath === '/_document.js') {
next()
return
}
let destFilePath = null
if (/index\.js$/.test(filename)) {
destFilePath = join(outDir, '_next', buildId, 'page', relativeFilePath)
} else {
const newRelativeFilePath = relativeFilePath.replace(/\.js/, `${sep}index.js`)
destFilePath = join(outDir, '_next', buildId, 'page', newRelativeFilePath)
}
cp(fullFilePath, destFilePath)
.then(next)
.catch(reject)
})
walker.on('end', resolve)
})
}

View file

@ -138,14 +138,21 @@ export default class HotReloader {
}
} : {}
this.webpackDevMiddleware = webpackDevMiddleware(compiler, {
let webpackDevMiddlewareConfig = {
publicPath: '/_next/webpack/',
noInfo: true,
quiet: true,
clientLogLevel: 'warning',
watchOptions: { ignored },
...windowsSettings
})
}
if (this.config.webpackDevMiddleware) {
console.log('> Using "webpackDevMiddleware" config function defined in next.config.js.')
webpackDevMiddlewareConfig = this.config.webpackDevMiddleware(webpackDevMiddlewareConfig)
}
this.webpackDevMiddleware = webpackDevMiddleware(compiler, webpackDevMiddlewareConfig)
this.webpackHotMiddleware = webpackHotMiddleware(compiler, {
path: '/_next/webpack-hmr',

View file

@ -136,7 +136,7 @@ export default class Server {
await this.serveStatic(req, res, p)
},
'/_next/:buildId/page/_error': async (req, res, params) => {
'/_next/:buildId/page/_error*': async (req, res, params) => {
if (!this.handleBuildId(params.buildId, res)) {
const error = new Error('INVALID_BUILD_ID')
const customFields = { buildIdMismatched: true }
@ -144,7 +144,7 @@ export default class Server {
return await renderScriptError(req, res, '/_error', error, customFields, this.renderOpts)
}
const p = join(this.dir, '.next/bundles/pages/_error.js')
const p = join(this.dir, `${this.dist}/bundles/pages/_error.js`)
await this.serveStatic(req, res, p)
},

View file

@ -41,7 +41,8 @@ async function doRender (req, res, pathname, query, {
assetPrefix,
dir = process.cwd(),
dev = false,
staticMarkup = false
staticMarkup = false,
nextExport = false
} = {}) {
page = page || pathname
@ -55,7 +56,8 @@ async function doRender (req, res, pathname, query, {
])
Component = Component.default || Component
Document = Document.default || Document
const ctx = { err, req, res, pathname, query }
const asPath = req.url
const ctx = { err, req, res, pathname, query, asPath }
const props = await loadGetInitialProps(Component, ctx)
// the response might be finshed on the getinitialprops call
@ -99,9 +101,11 @@ async function doRender (req, res, pathname, query, {
buildId,
buildStats,
assetPrefix,
nextExport,
err: (err) ? serializeError(dev, err) : null
},
dev,
dir,
staticMarkup,
...docProps
})
@ -111,7 +115,8 @@ async function doRender (req, res, pathname, query, {
export async function renderScript (req, res, page, opts) {
try {
const path = join(opts.dir, '.next', 'bundles', 'pages', page)
const dist = getConfig(opts.dir).distDir
const path = join(opts.dir, dist, 'bundles', 'pages', page)
const realPath = await resolvePath(path)
await serveStatic(req, res, realPath)
} catch (err) {

View file

@ -0,0 +1,19 @@
import React, {Component} from 'react'
export default class extends Component {
constructor (props) {
super(props)
this.state = {
mounted: false
}
}
componentDidMount () {
this.setState({mounted: true})
}
render () {
return <p>ComponentDidMount {this.state.mounted ? 'executed on client' : 'not executed'}.</p>
}
}

View file

@ -0,0 +1,2 @@
import CDM from '../lib/cdm'
export default CDM

View file

@ -0,0 +1,22 @@
import React from 'react'
import Router from 'next/router'
export default class extends React.Component {
constructor (...args) {
super(...args)
this.state = {}
}
componentDidMount () {
const asPath = Router.asPath
this.setState({ asPath })
}
render () {
return (
<div className='as-path-content'>
{this.state.asPath}
</div>
)
}
}

View file

@ -0,0 +1,15 @@
import React from 'react'
export default class extends React.Component {
static getInitialProps ({ asPath, req }) {
return { asPath }
}
render () {
return (
<div className='as-path-content'>
{this.props.asPath}
</div>
)
}
}

View file

@ -35,6 +35,9 @@ export default class extends Component {
<a id='query-string-link' style={linkStyle}>QueryString</a>
</Link>
<Link href='/nav/about' replace><a id='about-replace-link' style={linkStyle}>Replace state</a></Link>
<Link href='/nav/as-path' as='/as/path'><a id='as-path-link' style={linkStyle}>As Path</a></Link>
<Link href='/nav/as-path'><a id='as-path-link-no-as' style={linkStyle}>As Path (No as)</a></Link>
<Link href='/nav/as-path-using-router'><a id='as-path-using-router-link' style={linkStyle}>As Path (Using Router)</a></Link>
<button
onClick={() => this.visitQueryStringPage()}
style={linkStyle}

View file

@ -0,0 +1,2 @@
import CDM from '../../lib/cdm'
export default CDM

View file

@ -0,0 +1,2 @@
import CDM from '../lib/cdm'
export default CDM

View file

@ -321,5 +321,86 @@ export default (context, render) => {
browser.close()
})
})
describe('with different types of urls', () => {
it('should work with normal page', async () => {
const browser = await webdriver(context.appPort, '/with-cdm')
const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.')
browser.close()
})
it('should work with dir/index page ', async () => {
const browser = await webdriver(context.appPort, '/nested-cdm/index')
const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.')
browser.close()
})
it('should work with dir/ page ', async () => {
const browser = await webdriver(context.appPort, '/nested-cdm/')
const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.')
browser.close()
})
it('should work with /index page', async () => {
const browser = await webdriver(context.appPort, '/index')
const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.')
browser.close()
})
it('should work with / page', async () => {
const browser = await webdriver(context.appPort, '/')
const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.')
browser.close()
})
})
describe('with asPath', () => {
describe('inside getInitialProps', () => {
it('should show the correct asPath with a Link with as prop', async () => {
const browser = await webdriver(context.appPort, '/nav/')
const asPath = await browser
.elementByCss('#as-path-link').click()
.waitForElementByCss('.as-path-content')
.elementByCss('.as-path-content').text()
expect(asPath).toBe('/as/path')
browser.close()
})
it('should show the correct asPath with a Link without the as prop', async () => {
const browser = await webdriver(context.appPort, '/nav/')
const asPath = await browser
.elementByCss('#as-path-link-no-as').click()
.waitForElementByCss('.as-path-content')
.elementByCss('.as-path-content').text()
expect(asPath).toBe('/nav/as-path')
browser.close()
})
})
describe('with next/router', () => {
it('should show the correct asPath', async () => {
const browser = await webdriver(context.appPort, '/nav/')
const asPath = await browser
.elementByCss('#as-path-using-router-link').click()
.waitForElementByCss('.as-path-content')
.elementByCss('.as-path-content').text()
expect(asPath).toBe('/nav/as-path-using-router')
browser.close()
})
})
})
})
}

View file

@ -42,6 +42,7 @@ describe('Basic Features', () => {
renderViaHTTP(context.appPort, '/stateful'),
renderViaHTTP(context.appPort, '/stateless'),
renderViaHTTP(context.appPort, '/styled-jsx'),
renderViaHTTP(context.appPort, '/with-cdm'),
renderViaHTTP(context.appPort, '/nav'),
renderViaHTTP(context.appPort, '/nav/about'),
@ -49,7 +50,11 @@ describe('Basic Features', () => {
renderViaHTTP(context.appPort, '/nav/self-reload'),
renderViaHTTP(context.appPort, '/nav/hash-changes'),
renderViaHTTP(context.appPort, '/nav/shallow-routing'),
renderViaHTTP(context.appPort, '/nav/redirect')
renderViaHTTP(context.appPort, '/nav/redirect'),
renderViaHTTP(context.appPort, '/nav/as-path'),
renderViaHTTP(context.appPort, '/nav/as-path-using-router'),
renderViaHTTP(context.appPort, '/nested-cdm/index')
])
})
afterAll(() => stopApp(context.server))

View file

@ -3,8 +3,8 @@
import cheerio from 'cheerio'
export default function ({ app }, suiteName, render) {
async function get$ (path) {
const html = await render(path)
async function get$ (path, query) {
const html = await render(path, query)
return cheerio.load(html)
}
@ -69,6 +69,11 @@ export default function ({ app }, suiteName, render) {
expect($('pre').text()).toMatch(/This is an expected error/)
})
test('asPath', async () => {
const $ = await get$('/nav/as-path', { aa: 10 })
expect($('.as-path-content').text()).toBe('/nav/as-path?aa=10')
})
test('error 404', async () => {
const $ = await get$('/non-existent')
expect($('h1').text()).toBe('404')

View file

@ -0,0 +1,12 @@
module.exports = {
exportPathMap: function () {
return {
'/': { page: '/' },
'/about': { page: '/about' },
'/counter': { page: '/counter' },
'/dynamic': { page: '/dynamic', query: { text: 'cool dynamic text' } },
'/dynamic/one': { page: '/dynamic', query: { text: 'next export is nice' } },
'/dynamic/two': { page: '/dynamic', query: { text: 'zeit is awesome' } }
}
}
}

View file

@ -0,0 +1,12 @@
import Link from 'next/link'
export default () => (
<div id='about-page'>
<div>
<Link href='/'>
<a>Go Back</a>
</Link>
</div>
<p>This is the About page</p>
</div>
)

View file

@ -0,0 +1,30 @@
import React from 'react'
import Link from 'next/link'
let counter = 0
export default class Counter extends React.Component {
increaseCounter () {
counter++
this.forceUpdate()
}
render () {
return (
<div id='counter-page'>
<div>
<Link href='/'>
<a id='go-back'>Go Back</a>
</Link>
</div>
<p>Counter: {counter}</p>
<button
id='counter-increase'
onClick={() => this.increaseCounter()}
>
Increase
</button>
</div>
)
}
}

View file

@ -0,0 +1,18 @@
import Link from 'next/link'
const DynamicPage = ({ text }) => (
<div id='dynamic-page'>
<div>
<Link href='/'>
<a>Go Back</a>
</Link>
</div>
<p>{ text }</p>
</div>
)
DynamicPage.getInitialProps = ({ query }) => {
return { text: query.text }
}
export default DynamicPage

View file

@ -0,0 +1,56 @@
import Link from 'next/link'
import Router from 'next/router'
function routeToAbout (e) {
e.preventDefault()
Router.push('/about')
}
export default () => (
<div id='home-page'>
<div>
<Link href='/about'>
<a id='about-via-link'>About via Link</a>
</Link>
<a
href='#'
onClick={routeToAbout}
id='about-via-router'
>
About via Router
</a>
<Link href='/counter'>
<a id='counter'>Counter</a>
</Link>
<Link
href='/dynamic?text=cool+dynamic+text'
>
<a id='get-initial-props'>getInitialProps</a>
</Link>
<Link
href='/dynamic?text=next+export+is+nice'
as='/dynamic/one'
>
<a id='dynamic-1'>Dynamic 1</a>
</Link>
<Link
href='/dynamic?text=zeit+is+awesome'
as='/dynamic/two'
>
<a id='dynamic-2'>Dynamic 2</a>
</Link>
<Link href='/level1'>
<a id='level1-home-page'>Level1 home page</a>
</Link>
<Link href='/level1/about'>
<a id='level1-about-page'>Level1 about page</a>
</Link>
</div>
<p>This is the home page</p>
<style jsx>{`
a {
margin: 0 10px 0 0;
}
`}</style>
</div>
)

View file

@ -0,0 +1,12 @@
import Link from 'next/link'
export default () => (
<div id='level1-about-page'>
<div>
<Link href='/'>
<a>Go Back</a>
</Link>
</div>
<p>This is the Level1 about page</p>
</div>
)

View file

@ -0,0 +1,12 @@
import Link from 'next/link'
export default () => (
<div id='level1-home-page'>
<div>
<Link href='/'>
<a>Go Back</a>
</Link>
</div>
<p>This is the Level1 home page</p>
</div>
)

View file

@ -0,0 +1,120 @@
/* global describe, it, expect */
import webdriver from 'next-webdriver'
export default function (context) {
describe('Render via browser', () => {
it('should render the home page', async () => {
const browser = await webdriver(context.port, '/')
const text = await browser
.elementByCss('#home-page p').text()
expect(text).toBe('This is the home page')
browser.close()
})
it('should do navigations via Link', async () => {
const browser = await webdriver(context.port, '/')
const text = await browser
.elementByCss('#about-via-link').click()
.waitForElementByCss('#about-page')
.elementByCss('#about-page p').text()
expect(text).toBe('This is the About page')
browser.close()
})
it('should do navigations via Router', async () => {
const browser = await webdriver(context.port, '/')
const text = await browser
.elementByCss('#about-via-router').click()
.waitForElementByCss('#about-page')
.elementByCss('#about-page p').text()
expect(text).toBe('This is the About page')
browser.close()
})
it('should do run client side javascript', async () => {
const browser = await webdriver(context.port, '/')
const text = await browser
.elementByCss('#counter').click()
.waitForElementByCss('#counter-page')
.elementByCss('#counter-increase').click()
.elementByCss('#counter-increase').click()
.elementByCss('#counter-page p').text()
expect(text).toBe('Counter: 2')
browser.close()
})
it('should render pages using getInitialProps', async () => {
const browser = await webdriver(context.port, '/')
const text = await browser
.elementByCss('#get-initial-props').click()
.waitForElementByCss('#dynamic-page')
.elementByCss('#dynamic-page p').text()
expect(text).toBe('cool dynamic text')
browser.close()
})
it('should render dynamic pages with custom urls', async () => {
const browser = await webdriver(context.port, '/')
const text = await browser
.elementByCss('#dynamic-1').click()
.waitForElementByCss('#dynamic-page')
.elementByCss('#dynamic-page p').text()
expect(text).toBe('next export is nice')
browser.close()
})
it('should support client side naviagtion', async () => {
const browser = await webdriver(context.port, '/')
const text = await browser
.elementByCss('#counter').click()
.waitForElementByCss('#counter-page')
.elementByCss('#counter-increase').click()
.elementByCss('#counter-increase').click()
.elementByCss('#counter-page p').text()
expect(text).toBe('Counter: 2')
// let's go back and come again to this page:
const textNow = await browser
.elementByCss('#go-back').click()
.waitForElementByCss('#home-page')
.elementByCss('#counter').click()
.waitForElementByCss('#counter-page')
.elementByCss('#counter-page p').text()
expect(textNow).toBe('Counter: 2')
browser.close()
})
describe('pages in the nested level: level1', () => {
it('should render the home page', async () => {
const browser = await webdriver(context.port, '/')
const text = await browser
.elementByCss('#level1-home-page').click()
.waitForElementByCss('#level1-home-page')
.elementByCss('#level1-home-page p').text()
expect(text).toBe('This is the Level1 home page')
browser.close()
})
it('should render the about page', async () => {
const browser = await webdriver(context.port, '/')
const text = await browser
.elementByCss('#level1-about-page').click()
.waitForElementByCss('#level1-about-page')
.elementByCss('#level1-about-page p').text()
expect(text).toBe('This is the Level1 about page')
browser.close()
})
})
})
}

View file

@ -0,0 +1,31 @@
/* global jasmine, describe, beforeAll, afterAll */
import { join } from 'path'
import {
nextBuild,
nextExport,
startStaticServer,
stopApp
} from 'next-test-utils'
import ssr from './ssr'
import browser from './browser'
jasmine.DEFAULT_TIMEOUT_INTERVAL = 40000
const appDir = join(__dirname, '../')
const context = {}
describe('Static Export', () => {
beforeAll(async () => {
const outdir = join(appDir, 'out')
await nextBuild(appDir)
await nextExport(appDir, { outdir })
context.server = await startStaticServer(join(appDir, 'out'))
context.port = context.server.address().port
})
afterAll(() => stopApp(context.server))
ssr(context)
browser(context)
})

View file

@ -0,0 +1,21 @@
/* global describe, it, expect */
import { renderViaHTTP } from 'next-test-utils'
export default function (context) {
describe('Render via SSR', () => {
it('should render the home page', async () => {
const html = await renderViaHTTP(context.port, '/')
expect(html).toMatch(/This is the home page/)
})
it('should render a page with getInitialProps', async() => {
const html = await renderViaHTTP(context.port, '/dynamic')
expect(html).toMatch(/cool dynamic text/)
})
it('should render a dynamically rendered custom url page', async() => {
const html = await renderViaHTTP(context.port, '/dynamic/one')
expect(html).toMatch(/next export is nice/)
})
})
}

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