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:
parent
5b1be2bb98
commit
9300151118
20 changed files with 339 additions and 36 deletions
|
@ -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
.github/workflows/build_test_deploy.yml
vendored
1
.github/workflows/build_test_deploy.yml
vendored
|
@ -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/*
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
9
test/integration/async-modules/next.config.js
Normal file
9
test/integration/async-modules/next.config.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
// target: 'experimental-serverless-trace',
|
||||
webpack: (config, options) => {
|
||||
config.experiments = {
|
||||
topLevelAwait: true,
|
||||
}
|
||||
return config
|
||||
},
|
||||
}
|
5
test/integration/async-modules/pages/404.jsx
Normal file
5
test/integration/async-modules/pages/404.jsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
const content = await Promise.resolve("hi y'all")
|
||||
|
||||
export default function Custom404() {
|
||||
return <h1 id="content-404">{content}</h1>
|
||||
}
|
5
test/integration/async-modules/pages/_app.jsx
Normal file
5
test/integration/async-modules/pages/_app.jsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
const appValue = await Promise.resolve('hello')
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} appValue={appValue} />
|
||||
}
|
25
test/integration/async-modules/pages/_document.jsx
Normal file
25
test/integration/async-modules/pages/_document.jsx
Normal 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
|
7
test/integration/async-modules/pages/_error.jsx
Normal file
7
test/integration/async-modules/pages/_error.jsx
Normal 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
|
5
test/integration/async-modules/pages/api/hello.js
Normal file
5
test/integration/async-modules/pages/api/hello.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const value = await Promise.resolve(42)
|
||||
|
||||
export default function (req, res) {
|
||||
res.json({ value })
|
||||
}
|
22
test/integration/async-modules/pages/config.jsx
Normal file
22
test/integration/async-modules/pages/config.jsx
Normal 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>
|
||||
)
|
||||
}
|
15
test/integration/async-modules/pages/gsp.jsx
Normal file
15
test/integration/async-modules/pages/gsp.jsx
Normal 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>
|
||||
)
|
||||
}
|
15
test/integration/async-modules/pages/gssp.jsx
Normal file
15
test/integration/async-modules/pages/gssp.jsx
Normal 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>
|
||||
)
|
||||
}
|
10
test/integration/async-modules/pages/index.jsx
Normal file
10
test/integration/async-modules/pages/index.jsx
Normal 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>
|
||||
)
|
||||
}
|
7
test/integration/async-modules/pages/make-error.jsx
Normal file
7
test/integration/async-modules/pages/make-error.jsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
export async function getServerSideProps() {
|
||||
throw new Error('BOOM')
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <div>hello</div>
|
||||
}
|
146
test/integration/async-modules/test/index.test.js
Normal file
146
test/integration/async-modules/test/index.test.js
Normal 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()
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue