diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts
index c8244e51b3273..84e9cb24076c9 100644
--- a/packages/next/build/webpack/loaders/next-serverless-loader.ts
+++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts
@@ -104,7 +104,7 @@ const nextServerlessLoader: loader.Loader = function() {
export const config = ComponentInfo['confi' + 'g'] || {}
export const _app = App
- export async function renderReqToHTML(req, res, fromExport) {
+ export async function renderReqToHTML(req, res, fromExport, _renderOpts, _params) {
const options = {
App,
Document,
@@ -117,6 +117,7 @@ const nextServerlessLoader: loader.Loader = function() {
assetPrefix: "${assetPrefix}",
ampBindInitData: ${ampBindInitData === true ||
ampBindInitData === 'true'},
+ ..._renderOpts
}
let sprData = false
@@ -176,7 +177,7 @@ const nextServerlessLoader: loader.Loader = function() {
`
: `const nowParams = null;`
}
- let result = await renderToHTML(req, res, "${page}", Object.assign({}, unstable_getStaticProps ? {} : parsedUrl.query, nowParams ? nowParams : params), renderOpts)
+ let result = await renderToHTML(req, res, "${page}", Object.assign({}, unstable_getStaticProps ? {} : parsedUrl.query, nowParams ? nowParams : params, _params), renderOpts)
if (sprData && !fromExport) {
const payload = JSON.stringify(renderOpts.sprData)
diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts
index 44db3995b11d3..0998998bf06ab 100644
--- a/packages/next/export/index.ts
+++ b/packages/next/export/index.ts
@@ -116,12 +116,6 @@ export default async function(
const subFolders = nextConfig.exportTrailingSlash
const isLikeServerless = nextConfig.target !== 'server'
- if (!options.buildExport && isLikeServerless) {
- throw new Error(
- 'Cannot export when target is not server. https://err.sh/zeit/next.js/next-export-serverless'
- )
- }
-
log(`> using build directory: ${distDir}`)
if (!existsSync(distDir)) {
@@ -132,7 +126,12 @@ export default async function(
const buildId = readFileSync(join(distDir, BUILD_ID_FILE), 'utf8')
const pagesManifest =
- !options.pages && require(join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST))
+ !options.pages &&
+ require(join(
+ distDir,
+ isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
+ PAGES_MANIFEST
+ ))
let prerenderManifest
try {
diff --git a/packages/next/export/worker.js b/packages/next/export/worker.js
index c551e93529064..072faae8dd116 100644
--- a/packages/next/export/worker.js
+++ b/packages/next/export/worker.js
@@ -1,5 +1,6 @@
import mkdirpModule from 'mkdirp'
import { promisify } from 'util'
+import url from 'url'
import { extname, join, dirname, sep } from 'path'
import { renderToHTML } from '../next-server/server/render'
import { writeFile, access } from 'fs'
@@ -40,14 +41,18 @@ export default async function({
const { page } = pathMap
const filePath = path === '/' ? '/index' : path
const ampPath = `${filePath}.amp`
+ let params
// Check if the page is a specified dynamic route
if (isDynamicRoute(page) && page !== path) {
- const params = getRouteMatcher(getRouteRegex(page))(path)
+ params = getRouteMatcher(getRouteRegex(page))(path)
if (params) {
- query = {
- ...query,
- ...params,
+ // we have to pass these separately for serverless
+ if (!serverless) {
+ query = {
+ ...query,
+ ...params,
+ }
}
} else {
throw new Error(
@@ -107,26 +112,40 @@ export default async function({
}
if (serverless) {
- const mod = require(join(
+ const curUrl = url.parse(req.url, true)
+ req.url = url.format({
+ ...curUrl,
+ query: {
+ ...curUrl.query,
+ ...query,
+ },
+ })
+ const { Component: mod } = await loadComponents(
distDir,
- 'serverless/pages',
- (page === '/' ? 'index' : page) + '.js'
- ))
+ buildId,
+ page,
+ serverless
+ )
- // for non-dynamic SPR pages we should have already
- // prerendered the file
- if (renderedDuringBuild(mod.unstable_getStaticProps)) return results
+ // if it was auto-exported the HTML is loaded here
+ if (typeof mod === 'string') {
+ html = mod
+ } else {
+ // for non-dynamic SPR pages we should have already
+ // prerendered the file
+ if (renderedDuringBuild(mod.unstable_getStaticProps)) return results
- if (mod.unstable_getStaticProps && !htmlFilepath.endsWith('.html')) {
- // make sure it ends with .html if the name contains a dot
- htmlFilename += '.html'
- htmlFilepath += '.html'
- }
+ if (mod.unstable_getStaticProps && !htmlFilepath.endsWith('.html')) {
+ // make sure it ends with .html if the name contains a dot
+ htmlFilename += '.html'
+ htmlFilepath += '.html'
+ }
- renderMethod = mod.renderReqToHTML
- const result = await renderMethod(req, res, true)
- curRenderOpts = result.renderOpts || {}
- html = result.html
+ renderMethod = mod.renderReqToHTML
+ const result = await renderMethod(req, res, true, { ampPath }, params)
+ curRenderOpts = result.renderOpts || {}
+ html = result.html
+ }
if (!html) {
throw new Error(`Failed to render serverless page`)
diff --git a/test/integration/export-default-map-serverless/next.config.js b/test/integration/export-default-map-serverless/next.config.js
new file mode 100644
index 0000000000000..f0b0ae0241978
--- /dev/null
+++ b/test/integration/export-default-map-serverless/next.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ target: 'serverless',
+}
diff --git a/test/integration/export-default-map-serverless/pages/docs/index.js b/test/integration/export-default-map-serverless/pages/docs/index.js
new file mode 100644
index 0000000000000..a9a0dda5d87e4
--- /dev/null
+++ b/test/integration/export-default-map-serverless/pages/docs/index.js
@@ -0,0 +1,5 @@
+import { useAmp } from 'next/amp'
+
+export const config = { amp: 'hybrid' }
+
+export default () =>
I'm an {useAmp() ? 'AMP' : 'normal'} page
diff --git a/test/integration/export-default-map-serverless/pages/index.js b/test/integration/export-default-map-serverless/pages/index.js
new file mode 100644
index 0000000000000..75c983e69e85b
--- /dev/null
+++ b/test/integration/export-default-map-serverless/pages/index.js
@@ -0,0 +1,2 @@
+export default () => Simple hybrid amp/non-amp page
+export const config = { amp: 'hybrid' }
diff --git a/test/integration/export-default-map-serverless/pages/info.js b/test/integration/export-default-map-serverless/pages/info.js
new file mode 100644
index 0000000000000..a9a0dda5d87e4
--- /dev/null
+++ b/test/integration/export-default-map-serverless/pages/info.js
@@ -0,0 +1,5 @@
+import { useAmp } from 'next/amp'
+
+export const config = { amp: 'hybrid' }
+
+export default () => I'm an {useAmp() ? 'AMP' : 'normal'} page
diff --git a/test/integration/export-default-map-serverless/pages/just-amp/index.js b/test/integration/export-default-map-serverless/pages/just-amp/index.js
new file mode 100644
index 0000000000000..332a64770b868
--- /dev/null
+++ b/test/integration/export-default-map-serverless/pages/just-amp/index.js
@@ -0,0 +1,2 @@
+export default () => I am an AMP only page
+export const config = { amp: true }
diff --git a/test/integration/export-default-map-serverless/pages/some.js b/test/integration/export-default-map-serverless/pages/some.js
new file mode 100644
index 0000000000000..76b8930bd6f46
--- /dev/null
+++ b/test/integration/export-default-map-serverless/pages/some.js
@@ -0,0 +1,3 @@
+export const config = { amp: 'hybrid' }
+
+export default () => I'm an AMP page
diff --git a/test/integration/export-default-map-serverless/pages/v1.12/docs.js b/test/integration/export-default-map-serverless/pages/v1.12/docs.js
new file mode 100644
index 0000000000000..569e62008df65
--- /dev/null
+++ b/test/integration/export-default-map-serverless/pages/v1.12/docs.js
@@ -0,0 +1,3 @@
+export default function Docs(props) {
+ return Hello again 👋
+}
diff --git a/test/integration/export-default-map-serverless/pages/v1.12/index.js b/test/integration/export-default-map-serverless/pages/v1.12/index.js
new file mode 100644
index 0000000000000..6a9540f9efec5
--- /dev/null
+++ b/test/integration/export-default-map-serverless/pages/v1.12/index.js
@@ -0,0 +1,3 @@
+export default function Index(props) {
+ return Hello 👋
+}
diff --git a/test/integration/export-default-map-serverless/test/index.test.js b/test/integration/export-default-map-serverless/test/index.test.js
new file mode 100644
index 0000000000000..4c638adf7c2ba
--- /dev/null
+++ b/test/integration/export-default-map-serverless/test/index.test.js
@@ -0,0 +1,68 @@
+/* eslint-env jest */
+/* global jasmine */
+import fs from 'fs'
+import { join } from 'path'
+import cheerio from 'cheerio'
+import { promisify } from 'util'
+import { nextBuild, nextExport } from 'next-test-utils'
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
+const readFile = promisify(fs.readFile)
+const access = promisify(fs.access)
+const appDir = join(__dirname, '../')
+const outdir = join(appDir, 'out')
+
+describe('Export with default map', () => {
+ beforeAll(async () => {
+ await nextBuild(appDir)
+ await nextExport(appDir, { outdir })
+ })
+
+ it('should export with folder that has dot in name', async () => {
+ expect.assertions(1)
+ await expect(access(join(outdir, 'v1.12.html'))).resolves.toBe(undefined)
+ })
+
+ it('should export an amp only page to clean path', async () => {
+ expect.assertions(1)
+ await expect(access(join(outdir, 'docs.html'))).resolves.toBe(undefined)
+ })
+
+ it('should export hybrid amp page correctly', async () => {
+ expect.assertions(2)
+ await expect(access(join(outdir, 'some.html'))).resolves.toBe(undefined)
+ await expect(access(join(outdir, 'some.amp.html'))).resolves.toBe(undefined)
+ })
+
+ it('should export nested hybrid amp page correctly', async () => {
+ expect.assertions(3)
+ await expect(access(join(outdir, 'docs.html'))).resolves.toBe(undefined)
+ await expect(access(join(outdir, 'docs.amp.html'))).resolves.toBe(undefined)
+
+ const html = await readFile(join(outdir, 'docs.html'))
+ const $ = cheerio.load(html)
+ expect($('link[rel=amphtml]').attr('href')).toBe('/docs.amp')
+ })
+
+ it('should export nested hybrid amp page correctly with folder', async () => {
+ expect.assertions(3)
+ await expect(access(join(outdir, 'info.html'))).resolves.toBe(undefined)
+ await expect(access(join(outdir, 'info.amp.html'))).resolves.toBe(undefined)
+
+ const html = await readFile(join(outdir, 'info.html'))
+ const $ = cheerio.load(html)
+ expect($('link[rel=amphtml]').attr('href')).toBe('/info.amp')
+ })
+
+ it('should export hybrid index amp page correctly', async () => {
+ expect.assertions(3)
+ await expect(access(join(outdir, 'index.html'))).resolves.toBe(undefined)
+ await expect(access(join(outdir, 'index.amp.html'))).resolves.toBe(
+ undefined
+ )
+
+ const html = await readFile(join(outdir, 'index.html'))
+ const $ = cheerio.load(html)
+ expect($('link[rel=amphtml]').attr('href')).toBe('/index.amp')
+ })
+})
diff --git a/test/integration/export-dynamic-pages-serverless/next.config.js b/test/integration/export-dynamic-pages-serverless/next.config.js
new file mode 100644
index 0000000000000..dd6fb6a25adc9
--- /dev/null
+++ b/test/integration/export-dynamic-pages-serverless/next.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ target: 'serverless',
+ exportPathMap() {
+ return {
+ '/regression/jeff-is-cool': { page: '/regression/[slug]' },
+ }
+ },
+}
diff --git a/test/integration/export-dynamic-pages-serverless/pages/regression/[slug].js b/test/integration/export-dynamic-pages-serverless/pages/regression/[slug].js
new file mode 100644
index 0000000000000..675fc6e844332
--- /dev/null
+++ b/test/integration/export-dynamic-pages-serverless/pages/regression/[slug].js
@@ -0,0 +1,13 @@
+import { useRouter } from 'next/router'
+
+function Regression() {
+ const { asPath } = useRouter()
+ if (typeof window !== 'undefined') {
+ window.__AS_PATHS = [...new Set([...(window.__AS_PATHS || []), asPath])]
+ }
+ return {asPath}
+}
+
+Regression.getInitialProps = () => ({})
+
+export default Regression
diff --git a/test/integration/export-dynamic-pages-serverless/test/index.test.js b/test/integration/export-dynamic-pages-serverless/test/index.test.js
new file mode 100644
index 0000000000000..dd2c576ee0ae3
--- /dev/null
+++ b/test/integration/export-dynamic-pages-serverless/test/index.test.js
@@ -0,0 +1,52 @@
+/* eslint-env jest */
+/* global jasmine */
+import { join } from 'path'
+import cheerio from 'cheerio'
+import webdriver from 'next-webdriver'
+import {
+ nextBuild,
+ nextExport,
+ startCleanStaticServer,
+ stopApp,
+ renderViaHTTP,
+ waitFor,
+} from 'next-test-utils'
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60
+const appDir = join(__dirname, '../')
+const outdir = join(appDir, 'out')
+
+describe('Export Dyanmic Pages', () => {
+ let server
+ let port
+ beforeAll(async () => {
+ await nextBuild(appDir)
+ await nextExport(appDir, { outdir })
+
+ server = await startCleanStaticServer(outdir)
+ port = server.address().port
+ })
+
+ afterAll(async () => {
+ await stopApp(server)
+ })
+
+ it('should of exported with correct asPath', async () => {
+ const html = await renderViaHTTP(port, '/regression/jeff-is-cool')
+ const $ = cheerio.load(html)
+ expect($('#asPath').text()).toBe('/regression/jeff-is-cool')
+ })
+
+ it('should hydrate with correct asPath', async () => {
+ expect.assertions(1)
+ const browser = await webdriver(port, '/regression/jeff-is-cool')
+ try {
+ await waitFor(3000)
+ expect(await browser.eval(`window.__AS_PATHS`)).toEqual([
+ '/regression/jeff-is-cool',
+ ])
+ } finally {
+ await browser.close()
+ }
+ })
+})
diff --git a/test/integration/export-serverless/.gitignore b/test/integration/export-serverless/.gitignore
new file mode 100644
index 0000000000000..3ec8dc5141d75
--- /dev/null
+++ b/test/integration/export-serverless/.gitignore
@@ -0,0 +1 @@
+.next-dev
diff --git a/test/integration/export-serverless/components/hello.js b/test/integration/export-serverless/components/hello.js
new file mode 100644
index 0000000000000..c39174c9dbc7e
--- /dev/null
+++ b/test/integration/export-serverless/components/hello.js
@@ -0,0 +1 @@
+export default () => Welcome to dynamic imports.
diff --git a/test/integration/export-serverless/next.config.js b/test/integration/export-serverless/next.config.js
new file mode 100644
index 0000000000000..fd4243317d353
--- /dev/null
+++ b/test/integration/export-serverless/next.config.js
@@ -0,0 +1,37 @@
+const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
+
+module.exports = phase => {
+ return {
+ target: 'serverless',
+ distDir: phase === PHASE_DEVELOPMENT_SERVER ? '.next-dev' : '.next',
+ exportTrailingSlash: true,
+ exportPathMap: function() {
+ return {
+ '/': { page: '/' },
+ '/about': { page: '/about' },
+ '/button-link': { page: '/button-link' },
+ '/get-initial-props-with-no-query': {
+ page: '/get-initial-props-with-no-query',
+ },
+ '/counter': { page: '/counter' },
+ '/dynamic-imports': { page: '/dynamic-imports' },
+ '/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' },
+ },
+ '/file-name.md': {
+ page: '/dynamic',
+ query: { text: 'this file has an extension' },
+ },
+ '/query': { page: '/query', query: { a: 'blue' } },
+ // API route
+ '/blog/nextjs/comment/test': { page: '/blog/[post]/comment/[id]' },
+ }
+ }, // end exportPathMap
+ }
+}
diff --git a/test/integration/export-serverless/pages/about.js b/test/integration/export-serverless/pages/about.js
new file mode 100644
index 0000000000000..41c60ddae891f
--- /dev/null
+++ b/test/integration/export-serverless/pages/about.js
@@ -0,0 +1,18 @@
+import Link from 'next/link'
+
+const About = ({ bar }) => (
+
+
+
{`This is the About page foo${bar || ''}`}
+
+)
+
+About.getInitialProps = async () => {
+ return { bar: typeof window === 'undefined' ? 'bar' : '' }
+}
+
+export default About
diff --git a/test/integration/export-serverless/pages/api/data.js b/test/integration/export-serverless/pages/api/data.js
new file mode 100644
index 0000000000000..0484d94b2c5f2
--- /dev/null
+++ b/test/integration/export-serverless/pages/api/data.js
@@ -0,0 +1,3 @@
+export default (req, res) => {
+ res.send('Hello World')
+}
diff --git a/test/integration/export-serverless/pages/blog/[post]/comment/[id].js b/test/integration/export-serverless/pages/blog/[post]/comment/[id].js
new file mode 100644
index 0000000000000..e23ec345283f5
--- /dev/null
+++ b/test/integration/export-serverless/pages/blog/[post]/comment/[id].js
@@ -0,0 +1,16 @@
+import { useRouter } from 'next/router'
+
+const Page = () => {
+ const router = useRouter()
+ const { post, id } = router.query
+
+ return (
+ <>
+ {`Blog post ${post} comment ${id || '(all)'}`}
+ >
+ )
+}
+
+Page.getInitialProps = () => ({})
+
+export default Page
diff --git a/test/integration/export-serverless/pages/button-link.js b/test/integration/export-serverless/pages/button-link.js
new file mode 100644
index 0000000000000..f26b849d6818a
--- /dev/null
+++ b/test/integration/export-serverless/pages/button-link.js
@@ -0,0 +1,12 @@
+import Link from 'next/link'
+
+export default () => (
+
+
+
+
+
+
+
This is the About page
+
+)
diff --git a/test/integration/export-serverless/pages/counter.js b/test/integration/export-serverless/pages/counter.js
new file mode 100644
index 0000000000000..5360313b66186
--- /dev/null
+++ b/test/integration/export-serverless/pages/counter.js
@@ -0,0 +1,27 @@
+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 (
+
+
+
Counter: {counter}
+
+
+ )
+ }
+}
diff --git a/test/integration/export-serverless/pages/dynamic-imports.js b/test/integration/export-serverless/pages/dynamic-imports.js
new file mode 100644
index 0000000000000..43ba2866bcf48
--- /dev/null
+++ b/test/integration/export-serverless/pages/dynamic-imports.js
@@ -0,0 +1,15 @@
+import Link from 'next/link'
+import dynamic from 'next/dynamic'
+
+const DynamicComponent = dynamic(() => import('../components/hello'))
+
+export default () => (
+
+)
diff --git a/test/integration/export-serverless/pages/dynamic.js b/test/integration/export-serverless/pages/dynamic.js
new file mode 100644
index 0000000000000..76fbdbc9981be
--- /dev/null
+++ b/test/integration/export-serverless/pages/dynamic.js
@@ -0,0 +1,33 @@
+/* global location */
+import React from 'react'
+import Link from 'next/link'
+
+export default class DynamicPage extends React.Component {
+ static getInitialProps({ query }) {
+ return { text: query.text }
+ }
+
+ state = {}
+
+ componentDidMount() {
+ const [, hash] = location.href.split('#')
+ this.setState({ hash })
+ }
+
+ render() {
+ const { text } = this.props
+ const { hash } = this.state
+
+ return (
+
+
+
{text}
+
Hash: {hash}
+
+ )
+ }
+}
diff --git a/test/integration/export-serverless/pages/get-initial-props-with-no-query.js b/test/integration/export-serverless/pages/get-initial-props-with-no-query.js
new file mode 100644
index 0000000000000..f7a7a1f8ec0be
--- /dev/null
+++ b/test/integration/export-serverless/pages/get-initial-props-with-no-query.js
@@ -0,0 +1,7 @@
+const Page = ({ query }) => {`Query is: ${query}`}
+
+Page.getInitialProps = ({ query }) => {
+ return { query: JSON.stringify(query) }
+}
+
+export default Page
diff --git a/test/integration/export-serverless/pages/index.js b/test/integration/export-serverless/pages/index.js
new file mode 100644
index 0000000000000..ba411295915e9
--- /dev/null
+++ b/test/integration/export-serverless/pages/index.js
@@ -0,0 +1,53 @@
+import Link from 'next/link'
+import Router from 'next/router'
+
+function routeToAbout(e) {
+ e.preventDefault()
+ Router.push('/about')
+}
+
+export default () => (
+
+
+
This is the home page
+
+
+)
diff --git a/test/integration/export-serverless/pages/level1/about.js b/test/integration/export-serverless/pages/level1/about.js
new file mode 100644
index 0000000000000..4455046b86c57
--- /dev/null
+++ b/test/integration/export-serverless/pages/level1/about.js
@@ -0,0 +1,12 @@
+import Link from 'next/link'
+
+export default () => (
+
+
+
This is the Level1 about page
+
+)
diff --git a/test/integration/export-serverless/pages/level1/index.js b/test/integration/export-serverless/pages/level1/index.js
new file mode 100644
index 0000000000000..a285bcdac8aa4
--- /dev/null
+++ b/test/integration/export-serverless/pages/level1/index.js
@@ -0,0 +1,12 @@
+import Link from 'next/link'
+
+export default () => (
+
+
+
This is the Level1 home page
+
+)
diff --git a/test/integration/export-serverless/pages/query.js b/test/integration/export-serverless/pages/query.js
new file mode 100644
index 0000000000000..81794e10427ee
--- /dev/null
+++ b/test/integration/export-serverless/pages/query.js
@@ -0,0 +1,12 @@
+import { Component } from 'react'
+
+class Page extends Component {
+ static getInitialProps({ query }) {
+ return { query }
+ }
+ render() {
+ return JSON.stringify(this.props.query, null, 2)
+ }
+}
+
+export default Page
diff --git a/test/integration/export-serverless/public/about/data.txt b/test/integration/export-serverless/public/about/data.txt
new file mode 100644
index 0000000000000..6320cd248dd8a
--- /dev/null
+++ b/test/integration/export-serverless/public/about/data.txt
@@ -0,0 +1 @@
+data
\ No newline at end of file
diff --git a/test/integration/export-serverless/static/data/item.txt b/test/integration/export-serverless/static/data/item.txt
new file mode 100644
index 0000000000000..a713074253483
--- /dev/null
+++ b/test/integration/export-serverless/static/data/item.txt
@@ -0,0 +1 @@
+item
\ No newline at end of file
diff --git a/test/integration/export-serverless/test/api-routes.js b/test/integration/export-serverless/test/api-routes.js
new file mode 100644
index 0000000000000..0b2c9a6fa2873
--- /dev/null
+++ b/test/integration/export-serverless/test/api-routes.js
@@ -0,0 +1,28 @@
+/* eslint-env jest */
+import { join } from 'path'
+import { File, runNextCommand } from 'next-test-utils'
+
+export default function(context) {
+ describe('API routes export', () => {
+ const nextConfig = new File(join(context.appDir, 'next.config.js'))
+
+ beforeEach(() => {
+ nextConfig.replace('// API route', `'/data': { page: '/api/data' },`)
+ })
+ afterEach(() => {
+ nextConfig.restore()
+ })
+
+ it('Should throw if a route is matched', async () => {
+ const outdir = join(context.appDir, 'outApi')
+ const { stdout } = await runNextCommand(
+ ['export', context.appDir, '--outdir', outdir],
+ { stdout: true }
+ )
+
+ expect(stdout).toContain(
+ 'https://err.sh/zeit/next.js/api-routes-static-export'
+ )
+ })
+ })
+}
diff --git a/test/integration/export-serverless/test/browser.js b/test/integration/export-serverless/test/browser.js
new file mode 100644
index 0000000000000..aac586a142a5c
--- /dev/null
+++ b/test/integration/export-serverless/test/browser.js
@@ -0,0 +1,226 @@
+/* eslint-env jest */
+import webdriver from 'next-webdriver'
+import { check, waitFor, getBrowserBodyText } from 'next-test-utils'
+
+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')
+ await browser.close()
+ })
+
+ it('should add trailing slash on Link', async () => {
+ const browser = await webdriver(context.port, '/')
+ const link = await browser
+ .elementByCss('#about-via-link')
+ .getAttribute('href')
+
+ expect(link.substr(link.length - 1)).toBe('/')
+ })
+
+ it('should not add trailing slash on Link when disabled', async () => {
+ const browser = await webdriver(context.portNoTrailSlash, '/')
+ const link = await browser
+ .elementByCss('#about-via-link')
+ .getAttribute('href')
+
+ expect(link.substr(link.length - 1)).not.toBe('/')
+ })
+
+ 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 foo')
+ await 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 foo')
+ await 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')
+ await 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')
+ await 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')
+ await 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')
+
+ await browser.close()
+ })
+
+ it('should render dynamic import components in the client', async () => {
+ const browser = await webdriver(context.port, '/')
+ await browser
+ .elementByCss('#dynamic-imports-page')
+ .click()
+ .waitForElementByCss('#dynamic-imports-page')
+
+ await check(
+ () => browser.elementByCss('#dynamic-imports-page p').text(),
+ /Welcome to dynamic imports/
+ )
+
+ await browser.close()
+ })
+
+ it('should render pages with url hash correctly', async () => {
+ let browser
+ try {
+ browser = await webdriver(context.port, '/')
+
+ // Check for the query string content
+ const text = await browser
+ .elementByCss('#with-hash')
+ .click()
+ .waitForElementByCss('#dynamic-page')
+ .elementByCss('#dynamic-page p')
+ .text()
+
+ expect(text).toBe('zeit is awesome')
+
+ await check(() => browser.elementByCss('#hash').text(), /cool/)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should navigate even if used a button inside ', async () => {
+ const browser = await webdriver(context.port, '/button-link')
+
+ const text = await browser
+ .elementByCss('button')
+ .click()
+ .waitForElementByCss('#home-page')
+ .elementByCss('#home-page p')
+ .text()
+
+ expect(text).toBe('This is the home page')
+ await browser.close()
+ })
+
+ it('should update query after mount', async () => {
+ const browser = await webdriver(context.port, '/query?hello=1')
+
+ await waitFor(1000)
+ const text = await browser.eval('document.body.innerHTML')
+ expect(text).toMatch(/hello/)
+ await browser.close()
+ })
+
+ describe('pages in the nested level: level1', () => {
+ it('should render the home page', async () => {
+ const browser = await webdriver(context.port, '/')
+
+ await browser.eval(
+ 'document.getElementById("level1-home-page").click()'
+ )
+
+ await check(
+ () => getBrowserBodyText(browser),
+ /This is the Level1 home page/
+ )
+
+ await browser.close()
+ })
+
+ it('should render the about page', async () => {
+ const browser = await webdriver(context.port, '/')
+
+ await browser.eval(
+ 'document.getElementById("level1-about-page").click()'
+ )
+
+ await check(
+ () => getBrowserBodyText(browser),
+ /This is the Level1 about page/
+ )
+
+ await browser.close()
+ })
+ })
+ })
+}
diff --git a/test/integration/export-serverless/test/dev.js b/test/integration/export-serverless/test/dev.js
new file mode 100644
index 0000000000000..af68635a2c34d
--- /dev/null
+++ b/test/integration/export-serverless/test/dev.js
@@ -0,0 +1,46 @@
+/* eslint-env jest */
+import webdriver from 'next-webdriver'
+import { renderViaHTTP, getBrowserBodyText, check } from 'next-test-utils'
+import cheerio from 'cheerio'
+
+const loadJSONInPage = pageContent => {
+ const page = cheerio.load(pageContent)
+ return JSON.parse(page('#__next').text())
+}
+
+export default function(context) {
+ describe('Render in development mode', () => {
+ it('should render the home page', async () => {
+ const browser = await webdriver(context.port, '/')
+ await check(() => getBrowserBodyText(browser), /This is the home page/)
+ await browser.close()
+ })
+
+ it('should render pages only existent in exportPathMap page', async () => {
+ const browser = await webdriver(context.port, '/dynamic/one')
+ const text = await browser.elementByCss('#dynamic-page p').text()
+ expect(text).toBe('next export is nice')
+ await browser.close()
+ })
+ })
+
+ describe(`ExportPathMap's query in development mode`, () => {
+ it('should be present in ctx.query', async () => {
+ const pageContent = await renderViaHTTP(context.port, '/query')
+ const json = loadJSONInPage(pageContent)
+ expect(json).toEqual({ a: 'blue' })
+ })
+
+ it('should replace url query params in ctx.query when conflicting', async () => {
+ const pageContent = await renderViaHTTP(context.port, '/query?a=red')
+ const json = loadJSONInPage(pageContent)
+ expect(json).toEqual({ a: 'blue' })
+ })
+
+ it('should be merged with url query params in ctx.query', async () => {
+ const pageContent = await renderViaHTTP(context.port, '/query?b=green')
+ const json = loadJSONInPage(pageContent)
+ expect(json).toEqual({ a: 'blue', b: 'green' })
+ })
+ })
+}
diff --git a/test/integration/export-serverless/test/dynamic.js b/test/integration/export-serverless/test/dynamic.js
new file mode 100644
index 0000000000000..524413690df64
--- /dev/null
+++ b/test/integration/export-serverless/test/dynamic.js
@@ -0,0 +1,27 @@
+/* eslint-env jest */
+import { join } from 'path'
+import { File, runNextCommand } from 'next-test-utils'
+
+export default function(context) {
+ describe('Dynamic routes export', () => {
+ const nextConfig = new File(join(context.appDir, 'next.config.js'))
+ beforeEach(() => {
+ nextConfig.replace('/blog/nextjs/comment/test', '/bad/path')
+ })
+ afterEach(() => {
+ nextConfig.restore()
+ })
+
+ it('Should throw error not matched route', async () => {
+ const outdir = join(context.appDir, 'outDynamic')
+ const { stderr } = await runNextCommand(
+ ['export', context.appDir, '--outdir', outdir],
+ { stderr: true }
+ ).catch(err => err)
+
+ expect(stderr).toContain(
+ 'https://err.sh/zeit/next.js/export-path-mismatch'
+ )
+ })
+ })
+}
diff --git a/test/integration/export-serverless/test/index.test.js b/test/integration/export-serverless/test/index.test.js
new file mode 100644
index 0000000000000..5b3bf3db772fb
--- /dev/null
+++ b/test/integration/export-serverless/test/index.test.js
@@ -0,0 +1,101 @@
+/* eslint-env jest */
+/* global jasmine */
+import { join } from 'path'
+import {
+ nextBuild,
+ nextExport,
+ startStaticServer,
+ launchApp,
+ stopApp,
+ killApp,
+ findPort,
+ renderViaHTTP,
+ File,
+} from 'next-test-utils'
+
+import ssr from './ssr'
+import browser from './browser'
+import dev from './dev'
+import { promisify } from 'util'
+import fs from 'fs'
+import dynamic from './dynamic'
+import apiRoutes from './api-routes'
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
+
+const writeFile = promisify(fs.writeFile)
+const mkdir = promisify(fs.mkdir)
+const access = promisify(fs.access)
+const appDir = join(__dirname, '../')
+const context = {}
+context.appDir = appDir
+const devContext = {}
+const nextConfig = new File(join(appDir, 'next.config.js'))
+
+describe('Static Export', () => {
+ it('should delete existing exported files', async () => {
+ const outdir = join(appDir, 'out')
+ const tempfile = join(outdir, 'temp.txt')
+
+ await mkdir(outdir).catch(e => {
+ if (e.code !== 'EEXIST') throw e
+ })
+ await writeFile(tempfile, 'Hello there')
+
+ await nextBuild(appDir)
+ await nextExport(appDir, { outdir })
+
+ let doesNotExist = false
+ await access(tempfile).catch(e => {
+ if (e.code === 'ENOENT') doesNotExist = true
+ })
+ expect(doesNotExist).toBe(true)
+ })
+ beforeAll(async () => {
+ const outdir = join(appDir, 'out')
+ const outNoTrailSlash = join(appDir, 'outNoTrailSlash')
+
+ await nextBuild(appDir)
+ await nextExport(appDir, { outdir })
+
+ nextConfig.replace(
+ `exportTrailingSlash: true`,
+ `exportTrailingSlash: false`
+ )
+ await nextBuild(appDir)
+ await nextExport(appDir, { outdir: outNoTrailSlash })
+ nextConfig.restore()
+
+ context.server = await startStaticServer(outdir)
+ context.port = context.server.address().port
+
+ context.serverNoTrailSlash = await startStaticServer(outNoTrailSlash)
+ context.portNoTrailSlash = context.serverNoTrailSlash.address().port
+
+ devContext.port = await findPort()
+ devContext.server = await launchApp(
+ join(__dirname, '../'),
+ devContext.port,
+ true
+ )
+
+ // pre-build all pages at the start
+ await Promise.all([
+ renderViaHTTP(devContext.port, '/'),
+ renderViaHTTP(devContext.port, '/dynamic/one'),
+ ])
+ })
+ afterAll(async () => {
+ await Promise.all([
+ stopApp(context.server),
+ killApp(devContext.server),
+ stopApp(context.serverNoTrailSlash),
+ ])
+ })
+
+ ssr(context)
+ browser(context)
+ dev(devContext)
+ dynamic(context)
+ apiRoutes(context)
+})
diff --git a/test/integration/export-serverless/test/ssr.js b/test/integration/export-serverless/test/ssr.js
new file mode 100644
index 0000000000000..60770c1c9ff0f
--- /dev/null
+++ b/test/integration/export-serverless/test/ssr.js
@@ -0,0 +1,88 @@
+/* eslint-env jest */
+import { renderViaHTTP } from 'next-test-utils'
+import cheerio from 'cheerio'
+
+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 the about page', async () => {
+ const html = await renderViaHTTP(context.port, '/about')
+ expect(html).toMatch(/This is the About page foobar/)
+ })
+
+ it('should render links correctly', async () => {
+ const html = await renderViaHTTP(context.port, '/')
+ const $ = cheerio.load(html)
+ const dynamicLink = $('#dynamic-1').prop('href')
+ const filePathLink = $('#path-with-extension').prop('href')
+ expect(dynamicLink).toEqual('/dynamic/one/')
+ expect(filePathLink).toEqual('/file-name.md')
+ })
+
+ 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/)
+ })
+
+ it('should render pages with dynamic imports', async () => {
+ const html = await renderViaHTTP(context.port, '/dynamic-imports')
+ expect(html).toMatch(/Welcome to dynamic imports/)
+ })
+
+ it('should render paths with extensions', async () => {
+ const html = await renderViaHTTP(context.port, '/file-name.md')
+ expect(html).toMatch(/this file has an extension/)
+ })
+
+ it('should give empty object for query if there is no query', async () => {
+ const html = await renderViaHTTP(
+ context.port,
+ '/get-initial-props-with-no-query'
+ )
+ expect(html).toMatch(/Query is: {}/)
+ })
+
+ it('should render _error on 404.html even if not provided in exportPathMap', async () => {
+ const html = await renderViaHTTP(context.port, '/404.html')
+ // The default error page from the test server
+ // contains "404", so need to be specific here
+ expect(html).toMatch(/404.*page.*not.*found/i)
+ })
+
+ it('should not render _error on /404/index.html', async () => {
+ const html = await renderViaHTTP(context.port, '/404/index.html')
+ // The default error page from the test server
+ // contains "404", so need to be specific here
+ expect(html).not.toMatch(/404.*page.*not.*found/i)
+ })
+
+ it('Should serve static files', async () => {
+ const data = await renderViaHTTP(context.port, '/static/data/item.txt')
+ expect(data).toBe('item')
+ })
+
+ it('Should serve public files', async () => {
+ const html = await renderViaHTTP(context.port, '/about')
+ const data = await renderViaHTTP(context.port, '/about/data.txt')
+ expect(html).toMatch(/This is the About page foobar/)
+ expect(data).toBe('data')
+ })
+
+ it('Should render dynamic files with query', async () => {
+ const html = await renderViaHTTP(
+ context.port,
+ '/blog/nextjs/comment/test'
+ )
+ expect(html).toMatch(/Blog post nextjs comment test/)
+ })
+ })
+}
diff --git a/test/integration/export-subfolders-serverless/next.config.js b/test/integration/export-subfolders-serverless/next.config.js
new file mode 100644
index 0000000000000..f0b0ae0241978
--- /dev/null
+++ b/test/integration/export-subfolders-serverless/next.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ target: 'serverless',
+}
diff --git a/test/integration/export-subfolders-serverless/pages/about.js b/test/integration/export-subfolders-serverless/pages/about.js
new file mode 100644
index 0000000000000..4bd43bbc91d21
--- /dev/null
+++ b/test/integration/export-subfolders-serverless/pages/about.js
@@ -0,0 +1 @@
+export default () => I am an about page
diff --git a/test/integration/export-subfolders-serverless/pages/index.js b/test/integration/export-subfolders-serverless/pages/index.js
new file mode 100644
index 0000000000000..5c13c39dbd689
--- /dev/null
+++ b/test/integration/export-subfolders-serverless/pages/index.js
@@ -0,0 +1 @@
+export default () => I am a home page
diff --git a/test/integration/export-subfolders-serverless/pages/posts/index.js b/test/integration/export-subfolders-serverless/pages/posts/index.js
new file mode 100644
index 0000000000000..7c9f74caf7546
--- /dev/null
+++ b/test/integration/export-subfolders-serverless/pages/posts/index.js
@@ -0,0 +1 @@
+export default () => I am a list of posts
diff --git a/test/integration/export-subfolders-serverless/pages/posts/single.js b/test/integration/export-subfolders-serverless/pages/posts/single.js
new file mode 100644
index 0000000000000..0bd295f6d2ecc
--- /dev/null
+++ b/test/integration/export-subfolders-serverless/pages/posts/single.js
@@ -0,0 +1 @@
+export default () => I am a single post
diff --git a/test/integration/export-subfolders-serverless/test/index.test.js b/test/integration/export-subfolders-serverless/test/index.test.js
new file mode 100644
index 0000000000000..d0427f169766d
--- /dev/null
+++ b/test/integration/export-subfolders-serverless/test/index.test.js
@@ -0,0 +1,39 @@
+/* eslint-env jest */
+/* global jasmine */
+import fs from 'fs'
+import { join } from 'path'
+import cheerio from 'cheerio'
+import { promisify } from 'util'
+import { nextBuild, nextExport } from 'next-test-utils'
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
+const readFile = promisify(fs.readFile)
+const access = promisify(fs.access)
+const appDir = join(__dirname, '../')
+const outdir = join(appDir, 'out')
+
+describe('Export config#exportTrailingSlash set to false', () => {
+ beforeAll(async () => {
+ await nextBuild(appDir)
+ await nextExport(appDir, { outdir })
+ })
+
+ it('should export pages as [filename].html instead of [filename]/index.html', async () => {
+ expect.assertions(6)
+
+ await expect(access(join(outdir, 'index.html'))).resolves.toBe(undefined)
+ await expect(access(join(outdir, 'about.html'))).resolves.toBe(undefined)
+ await expect(access(join(outdir, 'posts.html'))).resolves.toBe(undefined)
+ await expect(access(join(outdir, 'posts', 'single.html'))).resolves.toBe(
+ undefined
+ )
+
+ const html = await readFile(join(outdir, 'index.html'))
+ const $ = cheerio.load(html)
+ expect($('p').text()).toBe('I am a home page')
+
+ const htmlSingle = await readFile(join(outdir, 'posts', 'single.html'))
+ const $single = cheerio.load(htmlSingle)
+ expect($single('p').text()).toBe('I am a single post')
+ })
+})
diff --git a/test/integration/serverless-trace/test/index.test.js b/test/integration/serverless-trace/test/index.test.js
index 1969e87ed0c8a..34cf0a698bc3e 100644
--- a/test/integration/serverless-trace/test/index.test.js
+++ b/test/integration/serverless-trace/test/index.test.js
@@ -68,7 +68,7 @@ describe('Serverless Trace', () => {
it('should have correct amphtml rel link', async () => {
const html = await renderViaHTTP(appPort, '/some-amp')
expect(html).toMatch(/Hi Im an AMP page/)
- expect(html).toMatch(/rel="amphtml" href="\/some-amp\?amp=1"/)
+ expect(html).toMatch(/rel="amphtml" href="\/some-amp\.amp"/)
})
it('should have correct canonical link', async () => {
diff --git a/test/integration/serverless/test/index.test.js b/test/integration/serverless/test/index.test.js
index eaf6d488cfef5..3d170482d0e0d 100644
--- a/test/integration/serverless/test/index.test.js
+++ b/test/integration/serverless/test/index.test.js
@@ -109,7 +109,7 @@ describe('Serverless', () => {
it('should have correct amphtml rel link', async () => {
const html = await renderViaHTTP(appPort, '/some-amp')
expect(html).toMatch(/Hi Im an AMP page/)
- expect(html).toMatch(/rel="amphtml" href="\/some-amp\?amp=1"/)
+ expect(html).toMatch(/rel="amphtml" href="\/some-amp\.amp"/)
})
it('should have correct canonical link', async () => {