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:
commit
4d0147385c
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -6,10 +6,12 @@ dist
|
|||
node_modules
|
||||
|
||||
# logs
|
||||
npm-debug.log
|
||||
*.log
|
||||
|
||||
# coverage
|
||||
.nyc_output
|
||||
coverage
|
||||
|
||||
# test output
|
||||
test/**/out
|
||||
.DS_Store
|
||||
|
|
1
bin/next
1
bin/next
|
@ -22,6 +22,7 @@ const commands = new Set([
|
|||
'init',
|
||||
'build',
|
||||
'start',
|
||||
'export',
|
||||
defaultCommand
|
||||
])
|
||||
|
||||
|
|
67
bin/next-export
Normal file
67
bin/next-export
Normal 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)
|
||||
})
|
|
@ -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) {
|
||||
|
|
29
examples/root-static-files/README.md
Normal file
29
examples/root-static-files/README.md
Normal 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.
|
12
examples/root-static-files/package.json
Normal file
12
examples/root-static-files/package.json
Normal 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"
|
||||
}
|
||||
}
|
7
examples/root-static-files/pages/index.js
Normal file
7
examples/root-static-files/pages/index.js
Normal 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>
|
||||
)
|
30
examples/root-static-files/server.js
Normal file
30
examples/root-static-files/server.js
Normal 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')
|
||||
})
|
||||
})
|
BIN
examples/root-static-files/static/favicon.ico
Normal file
BIN
examples/root-static-files/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
2
examples/root-static-files/static/robots.txt
Normal file
2
examples/root-static-files/static/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
6
examples/root-static-files/static/sitemap.xml
Normal file
6
examples/root-static-files/static/sitemap.xml
Normal 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>
|
|
@ -7,7 +7,7 @@
|
|||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "beta"
|
||||
"next": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-inline-react-svg": "^0.2.0"
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
36
examples/with-apollo-and-redux/lib/initApollo.js
Normal file
36
examples/with-apollo-and-redux/lib/initApollo.js
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
39
examples/with-apollo-and-redux/lib/initRedux.js
Normal file
39
examples/with-apollo-and-redux/lib/initRedux.js
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { combineReducers } from 'redux'
|
||||
|
||||
export default function getReducer (client) {
|
||||
return combineReducers({
|
||||
apollo: client.reducer()
|
||||
})
|
||||
}
|
12
examples/with-apollo-and-redux/lib/reducers.js
Normal file
12
examples/with-apollo-and-redux/lib/reducers.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
example: (state = {}, { type, payload }) => {
|
||||
switch (type) {
|
||||
case 'EXAMPLE_ACTION':
|
||||
return {
|
||||
...state
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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": "",
|
||||
|
|
37
examples/with-apollo/lib/initApollo.js
Normal file
37
examples/with-apollo/lib/initApollo.js
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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}>
|
||||
|
|
3
examples/with-firebase/credentials/client.js
Normal file
3
examples/with-firebase/credentials/client.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
// TODO firebase client config
|
||||
}
|
3
examples/with-firebase/credentials/server.js
Normal file
3
examples/with-firebase/credentials/server.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
// TODO firebase server config
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
module.exports = {
|
||||
clientCredentials: {
|
||||
// TODO firebase client config
|
||||
},
|
||||
serverCredentials: {
|
||||
// TODO service account json here
|
||||
}
|
||||
}
|
|
@ -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}) {
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
33
examples/with-i18next/README.md
Normal file
33
examples/with-i18next/README.md
Normal 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/)
|
2
examples/with-i18next/components/Title.js
Normal file
2
examples/with-i18next/components/Title.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { translate } from 'react-i18next'
|
||||
export default translate(['common'])((props) => (<h1>{props.t('hello')}</h1>))
|
19
examples/with-i18next/package.json
Normal file
19
examples/with-i18next/package.json
Normal 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"
|
||||
}
|
27
examples/with-i18next/pages/index.js
Normal file
27
examples/with-i18next/pages/index.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
3
examples/with-i18next/static/locales/pt/common.json
Normal file
3
examples/with-i18next/static/locales/pt/common.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"hello": "e ae tche"
|
||||
}
|
11
examples/with-i18next/tools/startI18n.js
Normal file
11
examples/with-i18next/tools/startI18n.js
Normal 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
|
13
examples/with-i18next/tools/translationHelpers.js
Normal file
13
examples/with-i18next/tools/translationHelpers.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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!')
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
11
examples/with-material-ui/next.config.js
Normal file
11
examples/with-material-ui/next.config.js
Normal 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
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
9
examples/with-react-md/next.config.js
Normal file
9
examples/with-react-md/next.config.js
Normal 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
|
||||
}
|
||||
}
|
|
@ -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`
|
||||
|
|
35
examples/with-redux/components/AddCount.js
Normal file
35
examples/with-redux/components/AddCount.js
Normal 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)
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "next@beta",
|
||||
"next": "latest",
|
||||
"react": "^15.4.2",
|
||||
"react-dom": "^15.4.2"
|
||||
},
|
||||
|
|
8
examples/with-styled-components/.babelrc
Normal file
8
examples/with-styled-components/.babelrc
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
["styled-components", { "ssr": true, "displayName": true, "preprocess": false } ]
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
13
lib/link.js
13
lib/link.js
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}/`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
28
package.json
28
package.json
|
@ -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
109
readme.md
|
@ -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>
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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 = `
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -5,6 +5,7 @@ const cache = new Map()
|
|||
|
||||
const defaultConfig = {
|
||||
webpack: null,
|
||||
webpackDevMiddleware: null,
|
||||
poweredByHeader: true,
|
||||
distDir: '.next',
|
||||
assetPrefix: ''
|
||||
|
|
|
@ -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
136
server/export.js
Normal 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)
|
||||
})
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
19
test/integration/basic/lib/cdm.js
Normal file
19
test/integration/basic/lib/cdm.js
Normal 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>
|
||||
}
|
||||
}
|
2
test/integration/basic/pages/index.js
Normal file
2
test/integration/basic/pages/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import CDM from '../lib/cdm'
|
||||
export default CDM
|
22
test/integration/basic/pages/nav/as-path-using-router.js
Normal file
22
test/integration/basic/pages/nav/as-path-using-router.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
15
test/integration/basic/pages/nav/as-path.js
Normal file
15
test/integration/basic/pages/nav/as-path.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
2
test/integration/basic/pages/nested-cdm/index.js
Normal file
2
test/integration/basic/pages/nested-cdm/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import CDM from '../../lib/cdm'
|
||||
export default CDM
|
2
test/integration/basic/pages/with-cdm.js
Normal file
2
test/integration/basic/pages/with-cdm.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import CDM from '../lib/cdm'
|
||||
export default CDM
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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')
|
||||
|
|
12
test/integration/static/next.config.js
Normal file
12
test/integration/static/next.config.js
Normal 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' } }
|
||||
}
|
||||
}
|
||||
}
|
12
test/integration/static/pages/about.js
Normal file
12
test/integration/static/pages/about.js
Normal 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>
|
||||
)
|
30
test/integration/static/pages/counter.js
Normal file
30
test/integration/static/pages/counter.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
18
test/integration/static/pages/dynamic.js
Normal file
18
test/integration/static/pages/dynamic.js
Normal 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
|
56
test/integration/static/pages/index.js
Normal file
56
test/integration/static/pages/index.js
Normal 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>
|
||||
)
|
12
test/integration/static/pages/level1/about.js
Normal file
12
test/integration/static/pages/level1/about.js
Normal 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>
|
||||
)
|
12
test/integration/static/pages/level1/index.js
Normal file
12
test/integration/static/pages/level1/index.js
Normal 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>
|
||||
)
|
120
test/integration/static/test/browser.js
Normal file
120
test/integration/static/test/browser.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
31
test/integration/static/test/index.test.js
Normal file
31
test/integration/static/test/index.test.js
Normal 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)
|
||||
})
|
21
test/integration/static/test/ssr.js
Normal file
21
test/integration/static/test/ssr.js
Normal 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
Loading…
Reference in a new issue