Skip to content

Commit

Permalink
Allow pages to be async modules to enable top-level-await (#17590)
Browse files Browse the repository at this point in the history
Co-authored-by: JJ Kasper <jj@jjsweb.site>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Oct 14, 2020
1 parent 5b1be2b commit 9300151
Show file tree
Hide file tree
Showing 20 changed files with 339 additions and 36 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Expand Up @@ -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/**
1 change: 1 addition & 0 deletions .github/workflows/build_test_deploy.yml
Expand Up @@ -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/*

Expand Down
2 changes: 1 addition & 1 deletion packages/next/build/index.ts
Expand Up @@ -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),
Expand Down
25 changes: 13 additions & 12 deletions packages/next/build/utils.ts
Expand Up @@ -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(
Expand Down Expand Up @@ -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
}

Expand Down
50 changes: 36 additions & 14 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Expand Up @@ -339,7 +339,7 @@ const nextServerlessLoader: loader.Loader = function () {
: `{}`
}
const resolver = require('${absolutePagePath}')
const resolver = await require('${absolutePagePath}')
await apiResolver(
req,
res,
Expand Down Expand Up @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions packages/next/client/page-loader.ts
Expand Up @@ -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,
Expand Down
17 changes: 12 additions & 5 deletions packages/next/next-server/server/load-components.ts
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/next/next-server/server/next-server.ts
Expand Up @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions test/integration/async-modules/next.config.js
@@ -0,0 +1,9 @@
module.exports = {
// target: 'experimental-serverless-trace',
webpack: (config, options) => {
config.experiments = {
topLevelAwait: true,
}
return config
},
}
5 changes: 5 additions & 0 deletions test/integration/async-modules/pages/404.jsx
@@ -0,0 +1,5 @@
const content = await Promise.resolve("hi y'all")

export default function Custom404() {
return <h1 id="content-404">{content}</h1>
}
5 changes: 5 additions & 0 deletions test/integration/async-modules/pages/_app.jsx
@@ -0,0 +1,5 @@
const appValue = await Promise.resolve('hello')

export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} appValue={appValue} />
}
25 changes: 25 additions & 0 deletions test/integration/async-modules/pages/_document.jsx
@@ -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
7 changes: 7 additions & 0 deletions test/integration/async-modules/pages/_error.jsx
@@ -0,0 +1,7 @@
const errorContent = await Promise.resolve('hello error')

function Error({ statusCode }) {
return <p id="content-error">{errorContent}</p>
}

export default Error
5 changes: 5 additions & 0 deletions test/integration/async-modules/pages/api/hello.js
@@ -0,0 +1,5 @@
const value = await Promise.resolve(42)

export default function (req, res) {
res.json({ value })
}
22 changes: 22 additions & 0 deletions test/integration/async-modules/pages/config.jsx
@@ -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>
)
}
15 changes: 15 additions & 0 deletions test/integration/async-modules/pages/gsp.jsx
@@ -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>
)
}
15 changes: 15 additions & 0 deletions test/integration/async-modules/pages/gssp.jsx
@@ -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>
)
}
10 changes: 10 additions & 0 deletions test/integration/async-modules/pages/index.jsx
@@ -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>
)
}
7 changes: 7 additions & 0 deletions test/integration/async-modules/pages/make-error.jsx
@@ -0,0 +1,7 @@
export async function getServerSideProps() {
throw new Error('BOOM')
}

export default function Page() {
return <div>hello</div>
}

0 comments on commit 9300151

Please sign in to comment.