Add prerender
PageConfig option (#7699)
* Add prerender PageConfig option * Update PageConfig type * Add inlining of data when pre-render is set and add tests * Update types import * Add check for props * Rename prerender to experimentalPrerender for now
This commit is contained in:
parent
0faf693ed2
commit
0ca8087565
26 changed files with 406 additions and 59 deletions
|
@ -377,6 +377,10 @@ export default class Router implements BaseRouter {
|
|||
return new Promise((resolve, reject) => {
|
||||
const ctx = { pathname, query, asPath: as }
|
||||
this.getInitialProps(Component, ctx).then(props => {
|
||||
// if data is inlined during pre-render it is a string
|
||||
if (props && typeof props.pageProps === 'string') {
|
||||
props.pageProps = JSON.parse(props.pageProps)
|
||||
}
|
||||
routeInfo.props = props
|
||||
this.components[route] = routeInfo
|
||||
resolve(routeInfo)
|
||||
|
|
|
@ -5,23 +5,16 @@ import {
|
|||
SERVER_DIRECTORY,
|
||||
} from '../lib/constants'
|
||||
import { join } from 'path'
|
||||
|
||||
import { PageConfig } from 'next-server/types'
|
||||
import { requirePage } from './require'
|
||||
|
||||
export function interopDefault(mod: any) {
|
||||
return mod.default || mod
|
||||
}
|
||||
|
||||
export interface IPageConfig {
|
||||
amp?: boolean | 'hybrid'
|
||||
api?: {
|
||||
bodyParser?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type LoadComponentsReturnType = {
|
||||
Component: any
|
||||
PageConfig: IPageConfig
|
||||
pageConfig: PageConfig
|
||||
buildManifest?: any
|
||||
reactLoadableManifest?: any
|
||||
Document?: any
|
||||
|
@ -37,7 +30,7 @@ export async function loadComponents(
|
|||
): Promise<LoadComponentsReturnType> {
|
||||
if (serverless) {
|
||||
const Component = await requirePage(pathname, distDir, serverless)
|
||||
return { Component, PageConfig: Component.config || {} }
|
||||
return { Component, pageConfig: Component.config || {} }
|
||||
}
|
||||
const documentPath = join(
|
||||
distDir,
|
||||
|
@ -82,6 +75,6 @@ export async function loadComponents(
|
|||
buildManifest,
|
||||
DocumentMiddleware,
|
||||
reactLoadableManifest,
|
||||
PageConfig: ComponentMod.config || {},
|
||||
pageConfig: ComponentMod.config || {},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@ import {
|
|||
interopDefault,
|
||||
loadComponents,
|
||||
LoadComponentsReturnType,
|
||||
IPageConfig,
|
||||
} from './load-components'
|
||||
import { renderToHTML } from './render'
|
||||
import { getPagePath } from './require'
|
||||
|
@ -48,6 +47,7 @@ import Router, { route, Route, RouteMatch, Params } from './router'
|
|||
import { sendHTML } from './send-html'
|
||||
import { serveStatic } from './serve-static'
|
||||
import { isBlockedPage, isInternalUrl } from './utils'
|
||||
import { PageConfig } from 'next-server/types'
|
||||
|
||||
type NextConfig = any
|
||||
|
||||
|
@ -317,7 +317,7 @@ export default class Server {
|
|||
const resolverModule = require(resolverFunction)
|
||||
|
||||
if (resolverModule.config) {
|
||||
const config: IPageConfig = resolverModule.config
|
||||
const config: PageConfig = resolverModule.config
|
||||
if (config.api && config.api.bodyParser === false) {
|
||||
bodyParser = false
|
||||
}
|
||||
|
@ -516,7 +516,6 @@ export default class Server {
|
|||
return renderToHTML(req, res, pathname, query, {
|
||||
...result,
|
||||
...opts,
|
||||
PageConfig: result.PageConfig,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ import { getPageFiles, BuildManifest } from './get-page-files'
|
|||
import { AmpStateContext } from '../lib/amp-context'
|
||||
import optimizeAmp from './optimize-amp'
|
||||
import { isInAmpMode } from '../lib/amp'
|
||||
import { IPageConfig } from './load-components'
|
||||
import { PageConfig } from 'next-server/types'
|
||||
|
||||
export type ManifestItem = {
|
||||
id: number | string
|
||||
|
@ -145,13 +145,15 @@ type RenderOpts = {
|
|||
hybridAmp?: boolean
|
||||
buildManifest: BuildManifest
|
||||
reactLoadableManifest: ReactLoadableManifest
|
||||
PageConfig: IPageConfig
|
||||
pageConfig: PageConfig
|
||||
Component: React.ComponentType
|
||||
Document: DocumentType
|
||||
DocumentMiddleware: (ctx: NextPageContext) => void
|
||||
App: AppType
|
||||
ErrorDebug?: React.ComponentType<{ error: Error }>
|
||||
ampValidator?: (html: string, pathname: string) => Promise<void>
|
||||
isPrerender?: boolean
|
||||
pageData?: any
|
||||
}
|
||||
|
||||
function renderDocument(
|
||||
|
@ -250,7 +252,7 @@ export async function renderToHTML(
|
|||
ampPath = '',
|
||||
App,
|
||||
Document,
|
||||
PageConfig,
|
||||
pageConfig,
|
||||
DocumentMiddleware,
|
||||
Component,
|
||||
buildManifest,
|
||||
|
@ -259,7 +261,7 @@ export async function renderToHTML(
|
|||
} = renderOpts
|
||||
|
||||
await Loadable.preloadAll() // Make sure all dynamic imports are loaded
|
||||
let isStaticPage = false
|
||||
let isStaticPage = Boolean(pageConfig.experimentalPrerender)
|
||||
|
||||
if (dev) {
|
||||
const { isValidElementType } = require('react-is')
|
||||
|
@ -336,9 +338,9 @@ export async function renderToHTML(
|
|||
}
|
||||
|
||||
const ampState = {
|
||||
ampFirst: PageConfig.amp === true,
|
||||
ampFirst: pageConfig.amp === true,
|
||||
hasQuery: Boolean(query.amp),
|
||||
hybrid: PageConfig.amp === 'hybrid',
|
||||
hybrid: pageConfig.amp === 'hybrid',
|
||||
}
|
||||
|
||||
const reactLoadableModules: string[] = []
|
||||
|
@ -490,9 +492,13 @@ export async function renderToHTML(
|
|||
const inAmpMode = isInAmpMode(ampState)
|
||||
const hybridAmp = ampState.hybrid
|
||||
|
||||
// update renderOpts so export knows it's AMP state
|
||||
// update renderOpts so export knows current state
|
||||
renderOpts.inAmpMode = inAmpMode
|
||||
renderOpts.hybridAmp = hybridAmp
|
||||
renderOpts.pageData = props && props.pageProps
|
||||
renderOpts.isPrerender =
|
||||
pageConfig.experimentalPrerender === true ||
|
||||
pageConfig.experimentalPrerender === 'inline'
|
||||
|
||||
let html = renderDocument(Document, {
|
||||
...renderOpts,
|
||||
|
|
11
packages/next-server/types/index.d.ts
vendored
11
packages/next-server/types/index.d.ts
vendored
|
@ -1 +1,10 @@
|
|||
declare module 'react-ssr-prepass'
|
||||
/**
|
||||
* `Config` type, use it for export const config
|
||||
*/
|
||||
export type PageConfig = {
|
||||
amp?: boolean | 'hybrid'
|
||||
api?: {
|
||||
bodyParser?: boolean
|
||||
}
|
||||
experimentalPrerender?: boolean | 'inline' | 'legacy'
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { PluginObj } from '@babel/core'
|
||||
import { NodePath } from '@babel/traverse'
|
||||
import * as BabelTypes from '@babel/types'
|
||||
import { PageConfig } from 'next-server/types'
|
||||
|
||||
interface PageConfig {
|
||||
amp?: boolean | 'hybrid'
|
||||
}
|
||||
const configKeys = new Set(['amp', 'experimentalPrerender'])
|
||||
export const inlineGipIdentifier = '__NEXT_GIP_INLINE__'
|
||||
export const dropBundleIdentifier = '__NEXT_DROP_CLIENT_FILE__'
|
||||
|
||||
function replacePath(path: any, t: typeof BabelTypes) {
|
||||
// replace progam path with just a variable with the drop identifier
|
||||
function replaceBundle(path: any, t: typeof BabelTypes) {
|
||||
path.parentPath.replaceWith(
|
||||
t.program(
|
||||
[
|
||||
|
@ -15,8 +17,8 @@ function replacePath(path: any, t: typeof BabelTypes) {
|
|||
t.identifier('config'),
|
||||
t.assignmentExpression(
|
||||
'=',
|
||||
t.identifier('no'), // this can't be empty but is required
|
||||
t.stringLiteral(`__NEXT_DROP_CLIENT_FILE__ ${Date.now()}`)
|
||||
t.identifier(dropBundleIdentifier),
|
||||
t.stringLiteral(`${dropBundleIdentifier} ${Date.now()}`)
|
||||
)
|
||||
),
|
||||
]),
|
||||
|
@ -26,18 +28,20 @@ function replacePath(path: any, t: typeof BabelTypes) {
|
|||
)
|
||||
}
|
||||
|
||||
interface ConfigState {
|
||||
setupInlining?: boolean
|
||||
bundleDropped?: boolean
|
||||
}
|
||||
|
||||
export default function nextPageConfig({
|
||||
types: t,
|
||||
}: {
|
||||
types: typeof BabelTypes
|
||||
}): PluginObj<{
|
||||
insertedDrop?: boolean
|
||||
hasAmp?: boolean
|
||||
}> {
|
||||
}): PluginObj {
|
||||
return {
|
||||
visitor: {
|
||||
Program: {
|
||||
enter(path, state: any) {
|
||||
enter(path, state: ConfigState) {
|
||||
path.traverse(
|
||||
{
|
||||
ExportNamedDeclaration(
|
||||
|
@ -45,7 +49,7 @@ export default function nextPageConfig({
|
|||
state: any
|
||||
) {
|
||||
if (
|
||||
state.replaced ||
|
||||
state.bundleDropped ||
|
||||
!path.node.declaration ||
|
||||
!(path.node.declaration as any).declarations
|
||||
)
|
||||
|
@ -58,13 +62,24 @@ export default function nextPageConfig({
|
|||
|
||||
for (const prop of declaration.init.properties) {
|
||||
const { name } = prop.key
|
||||
if (name === 'amp') config.amp = prop.value.value
|
||||
if (configKeys.has(name)) {
|
||||
// @ts-ignore
|
||||
config[name] = prop.value.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.amp === true) {
|
||||
replacePath(path, t)
|
||||
state.replaced = true
|
||||
replaceBundle(path, t)
|
||||
state.bundleDropped = true
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
config.experimentalPrerender === true ||
|
||||
config.experimentalPrerender === 'inline'
|
||||
) {
|
||||
state.setupInlining = true
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -72,6 +87,29 @@ export default function nextPageConfig({
|
|||
)
|
||||
},
|
||||
},
|
||||
// handles Page.getInitialProps = () => {}
|
||||
AssignmentExpression(path, state: ConfigState) {
|
||||
if (!state.setupInlining) return
|
||||
const { property } = (path.node.left || {}) as any
|
||||
const { name } = property
|
||||
if (name !== 'getInitialProps') return
|
||||
// replace the getInitialProps function with an identifier for replacing
|
||||
path.node.right = t.functionExpression(
|
||||
null,
|
||||
[],
|
||||
t.blockStatement([
|
||||
t.returnStatement(t.stringLiteral(inlineGipIdentifier)),
|
||||
])
|
||||
)
|
||||
},
|
||||
// handles class { static async getInitialProps() {} }
|
||||
FunctionDeclaration(path, state: ConfigState) {
|
||||
if (!state.setupInlining) return
|
||||
if ((path.node.id && path.node.id.name) !== 'getInitialProps') return
|
||||
path.node.body = t.blockStatement([
|
||||
t.returnStatement(t.stringLiteral(inlineGipIdentifier)),
|
||||
])
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -334,7 +334,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
if (customAppGetInitialProps === false && nonReservedPage) {
|
||||
if (nonReservedPage) {
|
||||
try {
|
||||
await staticCheckSema.acquire()
|
||||
const result: any = await new Promise((resolve, reject) => {
|
||||
|
@ -348,7 +348,10 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
})
|
||||
staticCheckSema.release()
|
||||
|
||||
if (result.isStatic) {
|
||||
if (
|
||||
(result.static && customAppGetInitialProps === false) ||
|
||||
result.prerender
|
||||
) {
|
||||
staticPages.add(page)
|
||||
isStatic = true
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export default function worker(
|
|||
) {
|
||||
try {
|
||||
const { serverBundle, runtimeEnvConfig } = options || ({} as any)
|
||||
const isStatic = isPageStatic(serverBundle, runtimeEnvConfig)
|
||||
const result = isPageStatic(serverBundle, runtimeEnvConfig)
|
||||
|
||||
// clear require.cache to prevent running out of memory
|
||||
// since the cache is persisted by default
|
||||
|
@ -19,7 +19,7 @@ export default function worker(
|
|||
}
|
||||
})
|
||||
|
||||
callback(null, { isStatic })
|
||||
callback(null, result)
|
||||
} catch (error) {
|
||||
callback(error)
|
||||
}
|
||||
|
|
|
@ -266,17 +266,22 @@ export async function getPageSizeInKb(
|
|||
export function isPageStatic(
|
||||
serverBundle: string,
|
||||
runtimeEnvConfig: any
|
||||
): boolean {
|
||||
): { static?: boolean; prerender?: boolean } {
|
||||
try {
|
||||
nextEnvConfig.setConfig(runtimeEnvConfig)
|
||||
const Comp = require(serverBundle).default
|
||||
const mod = require(serverBundle)
|
||||
const Comp = mod.default
|
||||
|
||||
if (!Comp || !isValidElementType(Comp) || typeof Comp === 'string') {
|
||||
throw new Error('INVALID_DEFAULT_EXPORT')
|
||||
}
|
||||
return typeof (Comp as any).getInitialProps !== 'function'
|
||||
|
||||
return {
|
||||
static: typeof (Comp as any).getInitialProps !== 'function',
|
||||
prerender: mod.config && mod.config.experimentalPrerender,
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') return false
|
||||
if (err.code === 'MODULE_NOT_FOUND') return {}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ module.exports = babelLoader.custom(babel => {
|
|||
options.presets = [...options.presets, presetItem]
|
||||
}
|
||||
|
||||
if (!isServer && filename.indexOf('pages') !== -1) {
|
||||
if (!isServer) {
|
||||
const pageConfigPlugin = babel.createConfigItem(
|
||||
[require('../../babel/plugins/next-page-config')],
|
||||
{ type: 'plugin' }
|
||||
|
|
|
@ -55,7 +55,7 @@ const nextServerlessLoader: loader.Loader = function() {
|
|||
import * as ComponentInfo from '${absolutePagePath}';
|
||||
const Component = ComponentInfo.default
|
||||
export default Component
|
||||
export const PageConfig = ComponentInfo['confi' + 'g'] || {}
|
||||
export const config = ComponentInfo['confi' + 'g'] || {}
|
||||
export const _app = App
|
||||
export async function renderReqToHTML(req, res, fromExport) {
|
||||
const options = {
|
||||
|
@ -74,7 +74,7 @@ const nextServerlessLoader: loader.Loader = function() {
|
|||
const renderOpts = Object.assign(
|
||||
{
|
||||
Component,
|
||||
PageConfig,
|
||||
pageConfig: config,
|
||||
dataOnly: req.headers && (req.headers.accept || '').indexOf('application/amp.bind+json') !== -1,
|
||||
nextExport: fromExport
|
||||
},
|
||||
|
|
|
@ -2,14 +2,16 @@ import mkdirpModule from 'mkdirp'
|
|||
import { promisify } from 'util'
|
||||
import { extname, join, dirname, sep } from 'path'
|
||||
import { renderToHTML } from 'next-server/dist/server/render'
|
||||
import { writeFile, access } from 'fs'
|
||||
import { writeFile, access, readFile } from 'fs'
|
||||
import { Sema } from 'async-sema'
|
||||
import AmpHtmlValidator from 'amphtml-validator'
|
||||
import { loadComponents } from 'next-server/dist/server/load-components'
|
||||
import { inlineGipIdentifier } from '../build/babel/plugins/next-page-config'
|
||||
|
||||
const envConfig = require('next-server/config')
|
||||
const mkdirp = promisify(mkdirpModule)
|
||||
const writeFileP = promisify(writeFile)
|
||||
const readFileP = promisify(readFile)
|
||||
const accessP = promisify(access)
|
||||
|
||||
global.__NEXT_DATA__ = {
|
||||
|
@ -106,6 +108,27 @@ process.on(
|
|||
}
|
||||
}
|
||||
|
||||
// inline pageData for getInitialProps
|
||||
if (curRenderOpts.isPrerender && curRenderOpts.pageData) {
|
||||
const dataStr = JSON.stringify(curRenderOpts.pageData)
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/'/g, "\\'")
|
||||
|
||||
const bundlePath = join(
|
||||
distDir,
|
||||
'static',
|
||||
buildId,
|
||||
'pages',
|
||||
(path === '/' ? 'index' : path) + '.js'
|
||||
)
|
||||
|
||||
const bundleContent = await readFileP(bundlePath, 'utf8')
|
||||
await writeFileP(
|
||||
bundlePath,
|
||||
bundleContent.replace(inlineGipIdentifier, dataStr)
|
||||
)
|
||||
}
|
||||
|
||||
const validateAmp = async (html, page) => {
|
||||
const validator = await AmpHtmlValidator.getInstance()
|
||||
const result = validator.validateString(html)
|
||||
|
|
18
packages/next/types/index.d.ts
vendored
18
packages/next/types/index.d.ts
vendored
|
@ -11,6 +11,8 @@ import {
|
|||
NextApiRequest,
|
||||
} from 'next-server/dist/lib/utils'
|
||||
|
||||
import { PageConfig } from 'next-server/types'
|
||||
|
||||
// Extend the React types with missing properties
|
||||
declare module 'react' {
|
||||
// <html amp=""> support
|
||||
|
@ -43,14 +45,10 @@ export type NextPage<P = {}, IP = P> = {
|
|||
getInitialProps?(ctx: NextPageContext): Promise<IP>
|
||||
}
|
||||
|
||||
/**
|
||||
* `Config` type, use it for export const config
|
||||
*/
|
||||
export type PageConfig = {
|
||||
amp?: boolean | 'hybrid'
|
||||
api?: {
|
||||
bodyParser?: boolean
|
||||
}
|
||||
export {
|
||||
NextPageContext,
|
||||
NextComponentType,
|
||||
NextApiResponse,
|
||||
NextApiRequest,
|
||||
PageConfig,
|
||||
}
|
||||
|
||||
export { NextPageContext, NextComponentType, NextApiResponse, NextApiRequest }
|
||||
|
|
5
test/integration/prerender/next.config.js
Normal file
5
test/integration/prerender/next.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
experimental: {
|
||||
autoExport: true
|
||||
}
|
||||
}
|
7
test/integration/prerender/pages/another.js
Normal file
7
test/integration/prerender/pages/another.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const Page = ({ name }) => <p>Pre-render page {name}</p>
|
||||
|
||||
Page.getInitialProps = async () => ({ name: 'John Deux' })
|
||||
|
||||
export const config = { experimentalPrerender: true }
|
||||
|
||||
export default Page
|
1
test/integration/prerender/pages/index.js
Normal file
1
test/integration/prerender/pages/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export default () => <p>An autoExported page</p>
|
7
test/integration/prerender/pages/nested/hello.js
Normal file
7
test/integration/prerender/pages/nested/hello.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const config = { experimentalPrerender: true }
|
||||
|
||||
const Page = ({ title }) => <p>{title}</p>
|
||||
|
||||
Page.getInitialProps = async () => ({ title: 'something' })
|
||||
|
||||
export default Page
|
5
test/integration/prerender/pages/nested/old-school.js
Normal file
5
test/integration/prerender/pages/nested/old-school.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const Page = () => <p>I'm just an old SSR page</p>
|
||||
|
||||
Page.getInitialProps = () => ({})
|
||||
|
||||
export default Page
|
5
test/integration/prerender/pages/old-school.js
Normal file
5
test/integration/prerender/pages/old-school.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const Page = () => <p>I'm just an old SSR page</p>
|
||||
|
||||
Page.getInitialProps = () => ({})
|
||||
|
||||
export default Page
|
85
test/integration/prerender/test/index.test.js
Normal file
85
test/integration/prerender/test/index.test.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
/* eslint-env jest */
|
||||
/* global jasmine */
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import {
|
||||
nextBuild,
|
||||
nextStart,
|
||||
findPort,
|
||||
killApp,
|
||||
renderViaHTTP
|
||||
} from 'next-test-utils'
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 3
|
||||
|
||||
const appDir = path.join(__dirname, '../')
|
||||
let buildId
|
||||
let appPort
|
||||
let app
|
||||
|
||||
describe('Pre-rendering pages', () => {
|
||||
beforeAll(async () => {
|
||||
await nextBuild(appDir)
|
||||
appPort = await findPort()
|
||||
app = await nextStart(appDir, appPort)
|
||||
buildId = (
|
||||
(await fs.readFile(path.join(appDir, '.next/BUILD_ID'), 'utf8')) || ''
|
||||
).trim()
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
it('should render the correct files', async () => {
|
||||
let files = ['nested/hello', 'another', 'index']
|
||||
|
||||
for (const file of files) {
|
||||
expect(
|
||||
await fs.exists(
|
||||
path.join(
|
||||
appDir,
|
||||
'.next/server/static',
|
||||
buildId,
|
||||
'pages',
|
||||
file + '.html'
|
||||
)
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
|
||||
files = ['nested/old-school', 'old-school']
|
||||
|
||||
for (const file of files) {
|
||||
expect(
|
||||
await fs.exists(
|
||||
path.join(
|
||||
appDir,
|
||||
'.next/server/static',
|
||||
buildId,
|
||||
'pages',
|
||||
file + '.js'
|
||||
)
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('should have called getInitialProps during pre-render', async () => {
|
||||
const hello = await renderViaHTTP(appPort, '/nested/hello')
|
||||
expect(hello).toMatch(/something/)
|
||||
|
||||
const another = await renderViaHTTP(appPort, '/another')
|
||||
expect(another).toMatch(/John Deux/)
|
||||
})
|
||||
|
||||
it('should call getInitialProps for SSR pages', async () => {
|
||||
const oldSchool1 = await renderViaHTTP(appPort, '/old-school')
|
||||
expect(oldSchool1).toMatch(/I.*?m just an old SSR page/)
|
||||
|
||||
const oldSchool2 = await renderViaHTTP(appPort, '/nested/old-school')
|
||||
expect(oldSchool2).toMatch(/I.*?m just an old SSR page/)
|
||||
})
|
||||
|
||||
it('should autoExport correctly', async () => {
|
||||
const index = await renderViaHTTP(appPort, '/')
|
||||
expect(index).toMatch(/An autoExported page/)
|
||||
})
|
||||
})
|
27
test/integration/production/pages/something.js
Normal file
27
test/integration/production/pages/something.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export const config = {
|
||||
experimentalPrerender: 'inline'
|
||||
}
|
||||
|
||||
const Page = ({ data }) => {
|
||||
return (
|
||||
<>
|
||||
<h3>{data}</h3>
|
||||
<Link href='/to-something'>
|
||||
<a id='to-something'>Click to to-something</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Page.getInitialProps = async () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
throw new Error(`this shouldn't be called`)
|
||||
}
|
||||
return {
|
||||
data: 'this is some data to be inlined!!!'
|
||||
}
|
||||
}
|
||||
|
||||
export default Page
|
30
test/integration/production/pages/to-something.js
Normal file
30
test/integration/production/pages/to-something.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const config = {
|
||||
experimentalPrerender: true
|
||||
}
|
||||
|
||||
class Page extends React.Component {
|
||||
static async getInitialProps () {
|
||||
if (typeof window !== 'undefined') {
|
||||
throw new Error(`this shouldn't be called`)
|
||||
}
|
||||
return {
|
||||
title: 'some interesting title'
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<>
|
||||
<h3>{this.props.title}</h3>
|
||||
<Link href='/something'>
|
||||
<a id='something'>Click to something</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Page
|
|
@ -468,6 +468,26 @@ describe('Production Usage', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('should pre-render pages with data correctly', async () => {
|
||||
const toSomething = await renderViaHTTP(appPort, '/to-something')
|
||||
expect(toSomething).toMatch(/some interesting title/)
|
||||
|
||||
const something = await renderViaHTTP(appPort, '/something')
|
||||
expect(something).toMatch(/this is some data to be inlined/)
|
||||
})
|
||||
|
||||
it('should have inlined the data correctly in pre-render', async () => {
|
||||
const browser = await webdriver(appPort, '/to-something')
|
||||
await browser.elementByCss('#something').click()
|
||||
|
||||
let text = await browser.elementByCss('h3').text()
|
||||
expect(text).toMatch(/this is some data to be inlined/)
|
||||
|
||||
await browser.elementByCss('#to-something').click()
|
||||
text = await browser.elementByCss('h3').text()
|
||||
expect(text).toMatch(/some interesting title/)
|
||||
})
|
||||
|
||||
dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||
|
||||
processEnv(context)
|
||||
|
|
27
test/integration/serverless/pages/something.js
Normal file
27
test/integration/serverless/pages/something.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export const config = {
|
||||
experimentalPrerender: 'inline'
|
||||
}
|
||||
|
||||
const Page = ({ data }) => {
|
||||
return (
|
||||
<>
|
||||
<h3>{data}</h3>
|
||||
<Link href='/to-something'>
|
||||
<a id='to-something'>Click to to-something</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Page.getInitialProps = async () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
throw new Error(`this shouldn't be called`)
|
||||
}
|
||||
return {
|
||||
data: 'this is some data to be inlined!!!'
|
||||
}
|
||||
}
|
||||
|
||||
export default Page
|
30
test/integration/serverless/pages/to-something.js
Normal file
30
test/integration/serverless/pages/to-something.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const config = {
|
||||
experimentalPrerender: true
|
||||
}
|
||||
|
||||
class Page extends React.Component {
|
||||
static async getInitialProps () {
|
||||
if (typeof window !== 'undefined') {
|
||||
throw new Error(`this shouldn't be called`)
|
||||
}
|
||||
return {
|
||||
title: 'some interesting title'
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<>
|
||||
<h3>{this.props.title}</h3>
|
||||
<Link href='/something'>
|
||||
<a id='something'>Click to something</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Page
|
|
@ -113,6 +113,26 @@ describe('Serverless', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('should pre-render pages with data correctly', async () => {
|
||||
const toSomething = await renderViaHTTP(appPort, '/to-something')
|
||||
expect(toSomething).toMatch(/some interesting title/)
|
||||
|
||||
const something = await renderViaHTTP(appPort, '/something')
|
||||
expect(something).toMatch(/this is some data to be inlined/)
|
||||
})
|
||||
|
||||
it('should have inlined the data correctly in pre-render', async () => {
|
||||
const browser = await webdriver(appPort, '/to-something')
|
||||
await browser.elementByCss('#something').click()
|
||||
|
||||
let text = await browser.elementByCss('h3').text()
|
||||
expect(text).toMatch(/this is some data to be inlined/)
|
||||
|
||||
await browser.elementByCss('#to-something').click()
|
||||
text = await browser.elementByCss('h3').text()
|
||||
expect(text).toMatch(/some interesting title/)
|
||||
})
|
||||
|
||||
describe('With basic usage', () => {
|
||||
it('should allow etag header support', async () => {
|
||||
const url = `http://localhost:${appPort}/`
|
||||
|
|
Loading…
Reference in a new issue