mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Merge branch 'next-export' into v3-beta
This commit is contained in:
commit
d4c62b1bea
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)
|
||||
})
|
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
|
||||
|
@ -48,6 +48,12 @@ export default class PageLoader {
|
|||
|
||||
this.registerEvents.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)
|
||||
|
@ -59,6 +65,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
|
||||
|
@ -100,5 +110,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 } = {}) {
|
||||
|
@ -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)
|
||||
|
|
|
@ -104,6 +104,8 @@
|
|||
"cheerio": "0.22.0",
|
||||
"chromedriver": "2.29.0",
|
||||
"coveralls": "2.13.1",
|
||||
"cross-env": "4.0.0",
|
||||
"express": "4.15.2",
|
||||
"cross-env": "5.0.0",
|
||||
"fly": "2.0.6",
|
||||
"fly-babel": "2.1.1",
|
||||
|
@ -118,7 +120,9 @@
|
|||
"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": {
|
||||
|
|
70
readme.md
70
readme.md
|
@ -39,6 +39,7 @@ 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)
|
||||
|
@ -767,6 +768,75 @@ 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/)
|
||||
|
|
|
@ -74,6 +74,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,
|
||||
|
|
|
@ -67,11 +67,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.getPreloadMainLinks()}
|
||||
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
|
||||
{styles || null}
|
||||
|
@ -133,7 +134,8 @@ export class NextScript extends Component {
|
|||
|
||||
render () {
|
||||
const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
|
||||
const { pathname, buildId, assetPrefix } = __NEXT_DATA__
|
||||
const { pathname, nextExport, buildId, assetPrefix } = __NEXT_DATA__
|
||||
const pagePathname = getPagePathname(pathname, nextExport)
|
||||
|
||||
return <div>
|
||||
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
|
||||
|
@ -147,9 +149,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.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)
|
||||
})
|
||||
}
|
|
@ -123,7 +123,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 }
|
||||
|
|
|
@ -40,7 +40,8 @@ async function doRender (req, res, pathname, query, {
|
|||
assetPrefix,
|
||||
dir = process.cwd(),
|
||||
dev = false,
|
||||
staticMarkup = false
|
||||
staticMarkup = false,
|
||||
nextExport = false
|
||||
} = {}) {
|
||||
page = page || pathname
|
||||
|
||||
|
@ -98,9 +99,11 @@ async function doRender (req, res, pathname, query, {
|
|||
buildId,
|
||||
buildStats,
|
||||
assetPrefix,
|
||||
nextExport,
|
||||
err: (err) ? serializeError(dev, err) : null
|
||||
},
|
||||
dev,
|
||||
dir,
|
||||
staticMarkup,
|
||||
...docProps
|
||||
})
|
||||
|
|
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/)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
import fetch from 'node-fetch'
|
||||
import qs from 'querystring'
|
||||
import http from 'http'
|
||||
import express from 'express'
|
||||
|
||||
import server from '../../dist/server/next'
|
||||
import build from '../../dist/server/build'
|
||||
import _export from '../../dist/server/export'
|
||||
import _pkg from '../../package.json'
|
||||
|
||||
export const nextServer = server
|
||||
export const nextBuild = build
|
||||
export const nextExport = _export
|
||||
export const pkg = _pkg
|
||||
|
||||
export function renderViaAPI (app, pathname, query = {}) {
|
||||
|
@ -31,7 +34,9 @@ export async function startApp (app) {
|
|||
}
|
||||
|
||||
export async function stopApp (app) {
|
||||
await server.__app.close()
|
||||
if (server.__app) {
|
||||
await server.__app.close()
|
||||
}
|
||||
await promiseCall(server, 'close')
|
||||
}
|
||||
|
||||
|
@ -52,3 +57,12 @@ function promiseCall (obj, method, ...args) {
|
|||
export function waitFor (millis) {
|
||||
return new Promise((resolve) => setTimeout(resolve, millis))
|
||||
}
|
||||
|
||||
export async function startStaticServer (dir) {
|
||||
const app = express()
|
||||
const server = http.createServer(app)
|
||||
app.use(express.static(dir))
|
||||
|
||||
await promiseCall(server, 'listen')
|
||||
return server
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ describe('Router', () => {
|
|||
const request = { clone: () => null }
|
||||
describe('.prefetch()', () => {
|
||||
it('should prefetch a given page', async () => {
|
||||
global.__NEXT_DATA__ = {}
|
||||
const pageLoader = new PageLoader()
|
||||
const router = new Router('/', {}, '/', { pageLoader })
|
||||
const route = '/routex'
|
||||
|
@ -29,6 +30,7 @@ describe('Router', () => {
|
|||
})
|
||||
|
||||
it('should only run two jobs at a time', async () => {
|
||||
global.__NEXT_DATA__ = {}
|
||||
// delay loading pages for an hour
|
||||
const pageLoader = new PageLoader({ delay: 1000 * 3600 })
|
||||
const router = new Router('/', {}, '/', { pageLoader })
|
||||
|
@ -46,6 +48,7 @@ describe('Router', () => {
|
|||
})
|
||||
|
||||
it('should run all the jobs', async () => {
|
||||
global.__NEXT_DATA__ = {}
|
||||
const pageLoader = new PageLoader()
|
||||
const router = new Router('/', {}, '/', { pageLoader })
|
||||
const routes = ['route1', 'route2', 'route3', 'route4']
|
||||
|
|
Loading…
Reference in a new issue