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 }) => ( +
+
+ + Go Back + +
+

{`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 () => ( + +) 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 ( +
+
+ + Go Back + +
+

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 () => ( +
+
+ + Go Back + +
+ +
+) 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 ( +
+
+ + Go Back + +
+

{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 () => ( +
+
+ + About via Link + + + About via Router + + + Counter + + + getInitialProps + + + Dynamic 1 + + + Dynamic 2 + + + With Hash + + + Path with extension + + + Level1 home page + + + Level1 about page + + + Dynamic imports page + +
+

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 () => ( +
+
+ + Go Back + +
+

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 () => ( +
+
+ + Go Back + +
+

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 () => {