Allow pages to be async modules to enable top-level-await (#17590)

Co-authored-by: JJ Kasper <jj@jjsweb.site>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Jan Potoms 2020-10-14 11:55:42 +02:00 committed by GitHub
parent 5b1be2bb98
commit 9300151118
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 339 additions and 36 deletions

View file

@ -13,4 +13,5 @@ packages/next-codemod/transforms/__testfixtures__/**/*
packages/next-codemod/transforms/__tests__/**/*
packages/next-codemod/**/*.js
packages/next-codemod/**/*.d.ts
packages/next-env/**/*.d.ts
packages/next-env/**/*.d.ts
test/integration/async-modules/**

View file

@ -113,6 +113,7 @@ jobs:
- run: yarn install --check-files
- run: node run-tests.js test/integration/production/test/index.test.js
- run: node run-tests.js test/integration/basic/test/index.test.js
- run: node run-tests.js test/integration/async-modules/test/index.test.js
- run: node run-tests.js test/integration/font-optimization/test/index.test.js
- run: node run-tests.js test/acceptance/*

View file

@ -531,7 +531,7 @@ export default async function build(
const serverBundle = getPagePath(page, distDir, isLikeServerless)
if (customAppGetInitialProps === undefined) {
customAppGetInitialProps = hasCustomGetInitialProps(
customAppGetInitialProps = await hasCustomGetInitialProps(
isLikeServerless
? serverBundle
: getPagePath('/_app', distDir, isLikeServerless),

View file

@ -704,21 +704,21 @@ export async function isPageStatic(
}> {
try {
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
const mod = require(serverBundle)
const Comp = mod.default || mod
const mod = await require(serverBundle)
const Comp = await (mod.default || mod)
if (!Comp || !isValidElementType(Comp) || typeof Comp === 'string') {
throw new Error('INVALID_DEFAULT_EXPORT')
}
const hasGetInitialProps = !!(Comp as any).getInitialProps
const hasStaticProps = !!mod.getStaticProps
const hasStaticPaths = !!mod.getStaticPaths
const hasServerProps = !!mod.getServerSideProps
const hasLegacyServerProps = !!mod.unstable_getServerProps
const hasLegacyStaticProps = !!mod.unstable_getStaticProps
const hasLegacyStaticPaths = !!mod.unstable_getStaticPaths
const hasLegacyStaticParams = !!mod.unstable_getStaticParams
const hasStaticProps = !!(await mod.getStaticProps)
const hasStaticPaths = !!(await mod.getStaticPaths)
const hasServerProps = !!(await mod.getServerSideProps)
const hasLegacyServerProps = !!(await mod.unstable_getServerProps)
const hasLegacyStaticProps = !!(await mod.unstable_getStaticProps)
const hasLegacyStaticPaths = !!(await mod.unstable_getStaticPaths)
const hasLegacyStaticParams = !!(await mod.unstable_getStaticParams)
if (hasLegacyStaticParams) {
throw new Error(
@ -804,19 +804,20 @@ export async function isPageStatic(
}
}
export function hasCustomGetInitialProps(
export async function hasCustomGetInitialProps(
bundle: string,
runtimeEnvConfig: any,
checkingApp: boolean
): boolean {
): Promise<boolean> {
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
let mod = require(bundle)
if (checkingApp) {
mod = mod._app || mod.default || mod
mod = (await mod._app) || mod.default || mod
} else {
mod = mod.default || mod
}
mod = await mod
return mod.getInitialProps !== mod.origGetInitialProps
}

View file

@ -339,7 +339,7 @@ const nextServerlessLoader: loader.Loader = function () {
: `{}`
}
const resolver = require('${absolutePagePath}')
const resolver = await require('${absolutePagePath}')
await apiResolver(
req,
res,
@ -386,35 +386,57 @@ const nextServerlessLoader: loader.Loader = function () {
const {sendPayload} = require('next/dist/next-server/server/send-payload');
const buildManifest = require('${buildManifest}');
const reactLoadableManifest = require('${reactLoadableManifest}');
const Document = require('${absoluteDocumentPath}').default;
const Error = require('${absoluteErrorPath}').default;
const App = require('${absoluteAppPath}').default;
const appMod = require('${absoluteAppPath}')
let App = appMod.default || appMod.then && appMod.then(mod => mod.default);
${dynamicRouteImports}
${rewriteImports}
const ComponentInfo = require('${absolutePagePath}')
const compMod = require('${absolutePagePath}')
const Component = ComponentInfo.default
let Component = compMod.default || compMod.then && compMod.then(mod => mod.default)
export default Component
export const unstable_getStaticParams = ComponentInfo['unstable_getStaticParam' + 's']
export const getStaticProps = ComponentInfo['getStaticProp' + 's']
export const getStaticPaths = ComponentInfo['getStaticPath' + 's']
export const getServerSideProps = ComponentInfo['getServerSideProp' + 's']
export let getStaticProps = compMod['getStaticProp' + 's'] || compMod.then && compMod.then(mod => mod['getStaticProp' + 's'])
export let getStaticPaths = compMod['getStaticPath' + 's'] || compMod.then && compMod.then(mod => mod['getStaticPath' + 's'])
export let getServerSideProps = compMod['getServerSideProp' + 's'] || compMod.then && compMod.then(mod => mod['getServerSideProp' + 's'])
// kept for detecting legacy exports
export const unstable_getStaticProps = ComponentInfo['unstable_getStaticProp' + 's']
export const unstable_getStaticPaths = ComponentInfo['unstable_getStaticPath' + 's']
export const unstable_getServerProps = ComponentInfo['unstable_getServerProp' + 's']
export const unstable_getStaticParams = compMod['unstable_getStaticParam' + 's'] || compMod.then && compMod.then(mod => mod['unstable_getStaticParam' + 's'])
export const unstable_getStaticProps = compMod['unstable_getStaticProp' + 's'] || compMod.then && compMod.then(mod => mod['unstable_getStaticProp' + 's'])
export const unstable_getStaticPaths = compMod['unstable_getStaticPath' + 's'] || compMod.then && compMod.then(mod => mod['unstable_getStaticPath' + 's'])
export const unstable_getServerProps = compMod['unstable_getServerProp' + 's'] || compMod.then && compMod.then(mod => mod['unstable_getServerProp' + 's'])
${dynamicRouteMatcher}
${defaultRouteRegex}
${normalizeDynamicRouteParams}
${handleRewrites}
export const config = ComponentInfo['confi' + 'g'] || {}
export let config = compMod['confi' + 'g'] || (compMod.then && compMod.then(mod => mod['confi' + 'g'])) || {}
export const _app = App
export async function renderReqToHTML(req, res, renderMode, _renderOpts, _params) {
let Document
let Error
;[
getStaticProps,
getServerSideProps,
getStaticPaths,
Component,
App,
config,
{ default: Document },
{ default: Error }
] = await Promise.all([
getStaticProps,
getServerSideProps,
getStaticPaths,
Component,
App,
config,
require('${absoluteDocumentPath}'),
require('${absoluteErrorPath}')
])
const fromExport = renderMode === 'export' || renderMode === true;
const nextStartMode = renderMode === 'passthrough'

View file

@ -340,9 +340,9 @@ export default class PageLoader {
// This method if called by the route code.
registerPage(route: string, regFn: () => any) {
const register = (styleSheets: StyleSheetTuple[]) => {
const register = async (styleSheets: StyleSheetTuple[]) => {
try {
const mod = regFn()
const mod = await regFn()
const pageData: PageCacheEntry = {
page: mod.default || mod,
mod,

View file

@ -41,20 +41,27 @@ export async function loadComponents(
): Promise<LoadComponentsReturnType> {
if (serverless) {
const Component = await requirePage(pathname, distDir, serverless)
const { getStaticProps, getStaticPaths, getServerSideProps } = Component
let { getStaticProps, getStaticPaths, getServerSideProps } = Component
getStaticProps = await getStaticProps
getStaticPaths = await getStaticPaths
getServerSideProps = await getServerSideProps
const pageConfig = (await Component.config) || {}
return {
Component,
pageConfig: Component.config || {},
pageConfig,
getStaticProps,
getStaticPaths,
getServerSideProps,
} as LoadComponentsReturnType
}
const DocumentMod = requirePage('/_document', distDir, serverless)
const AppMod = requirePage('/_app', distDir, serverless)
const ComponentMod = requirePage(pathname, distDir, serverless)
const [DocumentMod, AppMod, ComponentMod] = await Promise.all([
requirePage('/_document', distDir, serverless),
requirePage('/_app', distDir, serverless),
requirePage(pathname, distDir, serverless),
])
const [
buildManifest,

View file

@ -866,7 +866,7 @@ export default class Server {
throw err
}
const pageModule = require(builtPagePath)
const pageModule = await require(builtPagePath)
query = { ...query, ...params }
if (!this.renderOpts.dev && this._isLikeServerless) {

View file

@ -0,0 +1,9 @@
module.exports = {
// target: 'experimental-serverless-trace',
webpack: (config, options) => {
config.experiments = {
topLevelAwait: true,
}
return config
},
}

View file

@ -0,0 +1,5 @@
const content = await Promise.resolve("hi y'all")
export default function Custom404() {
return <h1 id="content-404">{content}</h1>
}

View file

@ -0,0 +1,5 @@
const appValue = await Promise.resolve('hello')
export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} appValue={appValue} />
}

View file

@ -0,0 +1,25 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'
const docValue = await Promise.resolve('doc value')
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps, docValue }
}
render() {
return (
<Html>
<Head />
<body>
<div id="doc-value">{this.props.docValue}</div>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument

View file

@ -0,0 +1,7 @@
const errorContent = await Promise.resolve('hello error')
function Error({ statusCode }) {
return <p id="content-error">{errorContent}</p>
}
export default Error

View file

@ -0,0 +1,5 @@
const value = await Promise.resolve(42)
export default function (req, res) {
res.json({ value })
}

View file

@ -0,0 +1,22 @@
export const config = {
amp: true,
}
await Promise.resolve('tadaa')
export default function Config() {
const date = new Date()
return (
<div>
<amp-timeago
id="amp-timeago"
width="0"
height="15"
datetime={date.toJSON()}
layout="responsive"
>
fail
</amp-timeago>
</div>
)
}

View file

@ -0,0 +1,15 @@
const gspValue = await Promise.resolve(42)
export async function getStaticProps() {
return {
props: { gspValue },
}
}
export default function Index({ gspValue }) {
return (
<main>
<div id="gsp-value">{gspValue}</div>
</main>
)
}

View file

@ -0,0 +1,15 @@
const gsspValue = await Promise.resolve(42)
export async function getServerSideProps() {
return {
props: { gsspValue },
}
}
export default function Index({ gsspValue }) {
return (
<main>
<div id="gssp-value">{gsspValue}</div>
</main>
)
}

View file

@ -0,0 +1,10 @@
const value = await Promise.resolve(42)
export default function Index({ appValue }) {
return (
<main>
<div id="app-value">{appValue}</div>
<div id="page-value">{value}</div>
</main>
)
}

View file

@ -0,0 +1,7 @@
export async function getServerSideProps() {
throw new Error('BOOM')
}
export default function Page() {
return <div>hello</div>
}

View file

@ -0,0 +1,146 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
import cheerio from 'cheerio'
import {
fetchViaHTTP,
renderViaHTTP,
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
File,
} from 'next-test-utils'
import { join } from 'path'
import webpack from 'webpack'
jest.setTimeout(1000 * 60 * 2)
const isWebpack5 = parseInt(webpack.version) === 5
let app
let appPort
const appDir = join(__dirname, '../')
const nextConfig = new File(join(appDir, 'next.config.js'))
function runTests(dev = false) {
it('ssr async page modules', async () => {
const html = await renderViaHTTP(appPort, '/')
const $ = cheerio.load(html)
expect($('#app-value').text()).toBe('hello')
expect($('#page-value').text()).toBe('42')
})
it('csr async page modules', async () => {
let browser
try {
browser = await webdriver(appPort, '/')
expect(await browser.elementByCss('#app-value').text()).toBe('hello')
expect(await browser.elementByCss('#page-value').text()).toBe('42')
expect(await browser.elementByCss('#doc-value').text()).toBe('doc value')
} finally {
if (browser) await browser.close()
}
})
it('works on async api routes', async () => {
const res = await fetchViaHTTP(appPort, '/api/hello')
expect(res.status).toBe(200)
const result = await res.json()
expect(result).toHaveProperty('value', 42)
})
it('works with getServerSideProps', async () => {
let browser
try {
browser = await webdriver(appPort, '/gssp')
expect(await browser.elementByCss('#gssp-value').text()).toBe('42')
} finally {
if (browser) await browser.close()
}
})
it('works with getStaticProps', async () => {
let browser
try {
browser = await webdriver(appPort, '/gsp')
expect(await browser.elementByCss('#gsp-value').text()).toBe('42')
} finally {
if (browser) await browser.close()
}
})
it('can render async 404 pages', async () => {
let browser
try {
browser = await webdriver(appPort, '/dhiuhefoiahjeoij')
expect(await browser.elementByCss('#content-404').text()).toBe("hi y'all")
} finally {
if (browser) await browser.close()
}
})
it('can render async AMP pages', async () => {
let browser
try {
browser = await webdriver(appPort, '/config')
expect(await browser.elementByCss('#amp-timeago').text()).not.toBe('fail')
} finally {
if (browser) await browser.close()
}
})
;(dev ? it.skip : it)('can render async error page', async () => {
let browser
try {
browser = await webdriver(appPort, '/make-error')
expect(await browser.elementByCss('#content-error').text()).toBe(
'hello error'
)
} finally {
if (browser) await browser.close()
}
})
}
;(isWebpack5 ? describe : describe.skip)('Async modules', () => {
describe('dev mode', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
runTests(true)
})
describe('production mode', () => {
beforeAll(async () => {
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
runTests()
})
describe('serverless mode', () => {
beforeAll(async () => {
nextConfig.replace('// target:', 'target:')
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await nextConfig.restore()
await killApp(app)
})
runTests()
})
})