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
|
node_modules
|
||||||
|
|
||||||
# logs
|
# logs
|
||||||
npm-debug.log
|
*.log
|
||||||
|
|
||||||
# coverage
|
# coverage
|
||||||
.nyc_output
|
.nyc_output
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
|
# test output
|
||||||
|
test/**/out
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
1
bin/next
1
bin/next
|
@ -22,6 +22,7 @@ const commands = new Set([
|
||||||
'init',
|
'init',
|
||||||
'build',
|
'build',
|
||||||
'start',
|
'start',
|
||||||
|
'export',
|
||||||
defaultCommand
|
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 { resolve, format, parse } from 'url'
|
||||||
import React, { Component, Children } from 'react'
|
import React, { Component, Children } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import Router from './router'
|
import Router, { _rewriteUrlForNextExport } from './router'
|
||||||
import { warn, execOnce, getLocationOrigin } from './utils'
|
import { warn, execOnce, getLocationOrigin } from './utils'
|
||||||
|
|
||||||
export default class Link extends Component {
|
export default class Link extends Component {
|
||||||
|
@ -122,6 +124,15 @@ export default class Link extends Component {
|
||||||
props.href = as || href
|
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)
|
return React.cloneElement(child, props)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global window, document */
|
/* global window, document, __NEXT_DATA__ */
|
||||||
import mitt from 'mitt'
|
import mitt from 'mitt'
|
||||||
|
|
||||||
const webpackModule = module
|
const webpackModule = module
|
||||||
|
@ -48,6 +48,12 @@ export default class PageLoader {
|
||||||
|
|
||||||
this.registerEvents.on(route, fire)
|
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.
|
// Load the script if not asked to load yet.
|
||||||
if (!this.loadingRoutes[route]) {
|
if (!this.loadingRoutes[route]) {
|
||||||
this.loadScript(route)
|
this.loadScript(route)
|
||||||
|
@ -59,6 +65,10 @@ export default class PageLoader {
|
||||||
loadScript (route) {
|
loadScript (route) {
|
||||||
route = this.normalizeRoute(route)
|
route = this.normalizeRoute(route)
|
||||||
|
|
||||||
|
if (__NEXT_DATA__.nextExport) {
|
||||||
|
route = route === '/' ? '/index.js' : `${route}/index.js`
|
||||||
|
}
|
||||||
|
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
const url = `${this.assetPrefix}/_next/${encodeURIComponent(this.buildId)}/page${route}`
|
const url = `${this.assetPrefix}/_next/${encodeURIComponent(this.buildId)}/page${route}`
|
||||||
script.src = url
|
script.src = url
|
||||||
|
@ -100,5 +110,10 @@ export default class PageLoader {
|
||||||
route = this.normalizeRoute(route)
|
route = this.normalizeRoute(route)
|
||||||
delete this.pageCache[route]
|
delete this.pageCache[route]
|
||||||
delete this.loadingRoutes[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
|
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 { parse, format } from 'url'
|
||||||
import mitt from 'mitt'
|
import mitt from 'mitt'
|
||||||
import shallowEquals from '../shallow-equals'
|
import shallowEquals from '../shallow-equals'
|
||||||
import PQueue from '../p-queue'
|
import PQueue from '../p-queue'
|
||||||
import { loadGetInitialProps, getURL } from '../utils'
|
import { loadGetInitialProps, getURL } from '../utils'
|
||||||
import { _notifyBuildIdMismatch } from './'
|
import { _notifyBuildIdMismatch, _rewriteUrlForNextExport } from './'
|
||||||
|
|
||||||
export default class Router {
|
export default class Router {
|
||||||
constructor (pathname, query, as, { pageLoader, Component, ErrorComponent, err } = {}) {
|
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,
|
// If url and as provided as an object representation,
|
||||||
// we'll format them into the string version here.
|
// we'll format them into the string version here.
|
||||||
const url = typeof _url === 'object' ? format(_url) : _url
|
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)
|
this.abortComponentLoad(as)
|
||||||
const { pathname, query } = parse(url, true)
|
const { pathname, query } = parse(url, true)
|
||||||
|
|
|
@ -104,6 +104,8 @@
|
||||||
"cheerio": "0.22.0",
|
"cheerio": "0.22.0",
|
||||||
"chromedriver": "2.29.0",
|
"chromedriver": "2.29.0",
|
||||||
"coveralls": "2.13.1",
|
"coveralls": "2.13.1",
|
||||||
|
"cross-env": "4.0.0",
|
||||||
|
"express": "4.15.2",
|
||||||
"cross-env": "5.0.0",
|
"cross-env": "5.0.0",
|
||||||
"fly": "2.0.6",
|
"fly": "2.0.6",
|
||||||
"fly-babel": "2.1.1",
|
"fly-babel": "2.1.1",
|
||||||
|
@ -118,7 +120,9 @@
|
||||||
"nyc": "10.3.2",
|
"nyc": "10.3.2",
|
||||||
"react": "15.5.3",
|
"react": "15.5.3",
|
||||||
"react-dom": "15.5.3",
|
"react-dom": "15.5.3",
|
||||||
|
"recursive-copy": "^2.0.6",
|
||||||
"standard": "9.0.2",
|
"standard": "9.0.2",
|
||||||
|
"walk": "^2.3.9",
|
||||||
"wd": "1.2.0"
|
"wd": "1.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"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)
|
- [Customizing babel config](#customizing-babel-config)
|
||||||
- [CDN support with Asset Prefix](#cdn-support-with-asset-prefix)
|
- [CDN support with Asset Prefix](#cdn-support-with-asset-prefix)
|
||||||
- [Production deployment](#production-deployment)
|
- [Production deployment](#production-deployment)
|
||||||
|
- [Static HTML export](#static-html-export)
|
||||||
- [Recipes](#recipes)
|
- [Recipes](#recipes)
|
||||||
- [FAQ](#faq)
|
- [FAQ](#faq)
|
||||||
- [Contributing](#contributing)
|
- [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)
|
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
|
## Recipes
|
||||||
|
|
||||||
- [Setting up 301 redirects](https://www.raygesualdo.com/posts/301-redirects-with-nextjs/)
|
- [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 = [
|
const plugins = [
|
||||||
|
new webpack.IgnorePlugin(/(precomputed)/, /node_modules.+(elliptic)/),
|
||||||
new webpack.LoaderOptionsPlugin({
|
new webpack.LoaderOptionsPlugin({
|
||||||
options: {
|
options: {
|
||||||
context: dir,
|
context: dir,
|
||||||
|
|
|
@ -67,11 +67,12 @@ export class Head extends Component {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { head, styles, __NEXT_DATA__ } = this.context._documentProps
|
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>
|
return <head>
|
||||||
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pathname}`} as='script' />
|
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} as='script' />
|
||||||
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error`} as='script' />
|
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error/index.js`} as='script' />
|
||||||
{this.getPreloadMainLinks()}
|
{this.getPreloadMainLinks()}
|
||||||
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
|
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
|
||||||
{styles || null}
|
{styles || null}
|
||||||
|
@ -133,7 +134,8 @@ export class NextScript extends Component {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { staticMarkup, __NEXT_DATA__ } = this.context._documentProps
|
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>
|
return <div>
|
||||||
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
|
{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()}
|
{staticMarkup ? null : this.getScripts()}
|
||||||
</div>
|
</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)
|
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)) {
|
if (!this.handleBuildId(params.buildId, res)) {
|
||||||
const error = new Error('INVALID_BUILD_ID')
|
const error = new Error('INVALID_BUILD_ID')
|
||||||
const customFields = { buildIdMismatched: true }
|
const customFields = { buildIdMismatched: true }
|
||||||
|
|
|
@ -40,7 +40,8 @@ async function doRender (req, res, pathname, query, {
|
||||||
assetPrefix,
|
assetPrefix,
|
||||||
dir = process.cwd(),
|
dir = process.cwd(),
|
||||||
dev = false,
|
dev = false,
|
||||||
staticMarkup = false
|
staticMarkup = false,
|
||||||
|
nextExport = false
|
||||||
} = {}) {
|
} = {}) {
|
||||||
page = page || pathname
|
page = page || pathname
|
||||||
|
|
||||||
|
@ -98,9 +99,11 @@ async function doRender (req, res, pathname, query, {
|
||||||
buildId,
|
buildId,
|
||||||
buildStats,
|
buildStats,
|
||||||
assetPrefix,
|
assetPrefix,
|
||||||
|
nextExport,
|
||||||
err: (err) ? serializeError(dev, err) : null
|
err: (err) ? serializeError(dev, err) : null
|
||||||
},
|
},
|
||||||
dev,
|
dev,
|
||||||
|
dir,
|
||||||
staticMarkup,
|
staticMarkup,
|
||||||
...docProps
|
...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 fetch from 'node-fetch'
|
||||||
import qs from 'querystring'
|
import qs from 'querystring'
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
|
import express from 'express'
|
||||||
|
|
||||||
import server from '../../dist/server/next'
|
import server from '../../dist/server/next'
|
||||||
import build from '../../dist/server/build'
|
import build from '../../dist/server/build'
|
||||||
|
import _export from '../../dist/server/export'
|
||||||
import _pkg from '../../package.json'
|
import _pkg from '../../package.json'
|
||||||
|
|
||||||
export const nextServer = server
|
export const nextServer = server
|
||||||
export const nextBuild = build
|
export const nextBuild = build
|
||||||
|
export const nextExport = _export
|
||||||
export const pkg = _pkg
|
export const pkg = _pkg
|
||||||
|
|
||||||
export function renderViaAPI (app, pathname, query = {}) {
|
export function renderViaAPI (app, pathname, query = {}) {
|
||||||
|
@ -31,7 +34,9 @@ export async function startApp (app) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stopApp (app) {
|
export async function stopApp (app) {
|
||||||
await server.__app.close()
|
if (server.__app) {
|
||||||
|
await server.__app.close()
|
||||||
|
}
|
||||||
await promiseCall(server, 'close')
|
await promiseCall(server, 'close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,3 +57,12 @@ function promiseCall (obj, method, ...args) {
|
||||||
export function waitFor (millis) {
|
export function waitFor (millis) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, 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 }
|
const request = { clone: () => null }
|
||||||
describe('.prefetch()', () => {
|
describe('.prefetch()', () => {
|
||||||
it('should prefetch a given page', async () => {
|
it('should prefetch a given page', async () => {
|
||||||
|
global.__NEXT_DATA__ = {}
|
||||||
const pageLoader = new PageLoader()
|
const pageLoader = new PageLoader()
|
||||||
const router = new Router('/', {}, '/', { pageLoader })
|
const router = new Router('/', {}, '/', { pageLoader })
|
||||||
const route = '/routex'
|
const route = '/routex'
|
||||||
|
@ -29,6 +30,7 @@ describe('Router', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should only run two jobs at a time', async () => {
|
it('should only run two jobs at a time', async () => {
|
||||||
|
global.__NEXT_DATA__ = {}
|
||||||
// delay loading pages for an hour
|
// delay loading pages for an hour
|
||||||
const pageLoader = new PageLoader({ delay: 1000 * 3600 })
|
const pageLoader = new PageLoader({ delay: 1000 * 3600 })
|
||||||
const router = new Router('/', {}, '/', { pageLoader })
|
const router = new Router('/', {}, '/', { pageLoader })
|
||||||
|
@ -46,6 +48,7 @@ describe('Router', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should run all the jobs', async () => {
|
it('should run all the jobs', async () => {
|
||||||
|
global.__NEXT_DATA__ = {}
|
||||||
const pageLoader = new PageLoader()
|
const pageLoader = new PageLoader()
|
||||||
const router = new Router('/', {}, '/', { pageLoader })
|
const router = new Router('/', {}, '/', { pageLoader })
|
||||||
const routes = ['route1', 'route2', 'route3', 'route4']
|
const routes = ['route1', 'route2', 'route3', 'route4']
|
||||||
|
|
Loading…
Reference in a new issue