Skip to content

Commit

Permalink
Add support for exporting from serverless build (#9744)
Browse files Browse the repository at this point in the history
* Add support for exporting from serverless build

* Add more tests

* Update syntax

* Dont add dynamic params in worker

* Update amphtml rel for serverless tests

* Update tests again

* Update dynamic params populating

* Fix params parsing

* Pass params separately
  • Loading branch information
ijjk authored and Timer committed Dec 14, 2019
1 parent fafb466 commit 6fcb623
Show file tree
Hide file tree
Showing 46 changed files with 1,040 additions and 31 deletions.
5 changes: 3 additions & 2 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -117,6 +117,7 @@ const nextServerlessLoader: loader.Loader = function() {
assetPrefix: "${assetPrefix}",
ampBindInitData: ${ampBindInitData === true ||
ampBindInitData === 'true'},
..._renderOpts
}
let sprData = false
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 6 additions & 7 deletions packages/next/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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 {
Expand Down
59 changes: 39 additions & 20 deletions packages/next/export/worker.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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`)
Expand Down
3 changes: 3 additions & 0 deletions test/integration/export-default-map-serverless/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
target: 'serverless',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useAmp } from 'next/amp'

export const config = { amp: 'hybrid' }

export default () => <p>I'm an {useAmp() ? 'AMP' : 'normal'} page</p>
2 changes: 2 additions & 0 deletions test/integration/export-default-map-serverless/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export default () => <p>Simple hybrid amp/non-amp page</p>
export const config = { amp: 'hybrid' }
5 changes: 5 additions & 0 deletions test/integration/export-default-map-serverless/pages/info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useAmp } from 'next/amp'

export const config = { amp: 'hybrid' }

export default () => <p>I'm an {useAmp() ? 'AMP' : 'normal'} page</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export default () => <p>I am an AMP only page</p>
export const config = { amp: true }
3 changes: 3 additions & 0 deletions test/integration/export-default-map-serverless/pages/some.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const config = { amp: 'hybrid' }

export default () => <p>I'm an AMP page</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Docs(props) {
return <div>Hello again 👋</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Index(props) {
return <div>Hello 👋</div>
}
68 changes: 68 additions & 0 deletions test/integration/export-default-map-serverless/test/index.test.js
Original file line number Diff line number Diff line change
@@ -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')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
target: 'serverless',
exportPathMap() {
return {
'/regression/jeff-is-cool': { page: '/regression/[slug]' },
}
},
}
Original file line number Diff line number Diff line change
@@ -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 <div id="asPath">{asPath}</div>
}

Regression.getInitialProps = () => ({})

export default Regression
Original file line number Diff line number Diff line change
@@ -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()
}
})
})
1 change: 1 addition & 0 deletions test/integration/export-serverless/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.next-dev
1 change: 1 addition & 0 deletions test/integration/export-serverless/components/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => <p>Welcome to dynamic imports.</p>
37 changes: 37 additions & 0 deletions test/integration/export-serverless/next.config.js
Original file line number Diff line number Diff line change
@@ -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
}
}
18 changes: 18 additions & 0 deletions test/integration/export-serverless/pages/about.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Link from 'next/link'

const About = ({ bar }) => (
<div id="about-page">
<div>
<Link href="/">
<a>Go Back</a>
</Link>
</div>
<p>{`This is the About page foo${bar || ''}`}</p>
</div>
)

About.getInitialProps = async () => {
return { bar: typeof window === 'undefined' ? 'bar' : '' }
}

export default About
3 changes: 3 additions & 0 deletions test/integration/export-serverless/pages/api/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default (req, res) => {
res.send('Hello World')
}
Loading

0 comments on commit 6fcb623

Please sign in to comment.