Add initial support for unstable_getServerProps (#10077)
* Add support for unstable_getServerProps * Apply suggestions from review * Add no-cache header and update types * Revert sharing of load-components type * Add catchall test and update routes-manifest field * Update header check * Update to pass query for getServerProps data requests * Update to not cache getServerProps requests * Rename server side props identifier * Update to nest props for getServerProps * Add no-cache header in serverless-loader also * Update to throw error for mixed SSG/serverProps earlier * Add comment explaining params chosing in serverless-loader * Update invalidKeysMsg to return a string and inline throwing * Inline throwing mixed SSG/serverProps error * Update setting cache header in serverless-loader * Add separate getServerData method in router * Update checkIsSSG -> isDataIdentifier * Refactor router getData back to ternary * Apply suggestions to build/index.ts * drop return * De-dupe extra escape regex * Add param test
This commit is contained in:
parent
abd69ec4be
commit
c24daa2172
26 changed files with 1070 additions and 77 deletions
|
@ -1,20 +1,25 @@
|
|||
import { NodePath, PluginObj } from '@babel/core'
|
||||
import * as BabelTypes from '@babel/types'
|
||||
import { SERVER_PROPS_SSG_CONFLICT } from '../../../lib/constants'
|
||||
|
||||
const pageComponentVar = '__NEXT_COMP'
|
||||
const prerenderId = '__N_SSG'
|
||||
const serverPropsId = '__N_SSP'
|
||||
|
||||
export const EXPORT_NAME_GET_STATIC_PROPS = 'unstable_getStaticProps'
|
||||
export const EXPORT_NAME_GET_STATIC_PATHS = 'unstable_getStaticPaths'
|
||||
export const EXPORT_NAME_GET_SERVER_PROPS = 'unstable_getServerProps'
|
||||
|
||||
const ssgExports = new Set([
|
||||
EXPORT_NAME_GET_STATIC_PROPS,
|
||||
EXPORT_NAME_GET_STATIC_PATHS,
|
||||
EXPORT_NAME_GET_SERVER_PROPS,
|
||||
])
|
||||
|
||||
type PluginState = {
|
||||
refs: Set<NodePath<BabelTypes.Identifier>>
|
||||
isPrerender: boolean
|
||||
isServerProps: boolean
|
||||
done: boolean
|
||||
}
|
||||
|
||||
|
@ -44,7 +49,7 @@ function decorateSsgExport(
|
|||
'=',
|
||||
t.memberExpression(
|
||||
t.identifier(pageComponentVar),
|
||||
t.identifier(prerenderId)
|
||||
t.identifier(state.isPrerender ? prerenderId : serverPropsId)
|
||||
),
|
||||
t.booleanLiteral(true)
|
||||
),
|
||||
|
@ -55,6 +60,24 @@ function decorateSsgExport(
|
|||
})
|
||||
}
|
||||
|
||||
const isDataIdentifier = (name: string, state: PluginState): boolean => {
|
||||
if (ssgExports.has(name)) {
|
||||
if (name === EXPORT_NAME_GET_SERVER_PROPS) {
|
||||
if (state.isPrerender) {
|
||||
throw new Error(SERVER_PROPS_SSG_CONFLICT)
|
||||
}
|
||||
state.isServerProps = true
|
||||
} else {
|
||||
if (state.isServerProps) {
|
||||
throw new Error(SERVER_PROPS_SSG_CONFLICT)
|
||||
}
|
||||
state.isPrerender = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export default function nextTransformSsg({
|
||||
types: t,
|
||||
}: {
|
||||
|
@ -134,10 +157,11 @@ export default function nextTransformSsg({
|
|||
enter(_, state) {
|
||||
state.refs = new Set<NodePath<BabelTypes.Identifier>>()
|
||||
state.isPrerender = false
|
||||
state.isServerProps = false
|
||||
state.done = false
|
||||
},
|
||||
exit(path, state) {
|
||||
if (!state.isPrerender) {
|
||||
if (!state.isPrerender && !state.isServerProps) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -239,8 +263,7 @@ export default function nextTransformSsg({
|
|||
const specifiers = path.get('specifiers')
|
||||
if (specifiers.length) {
|
||||
specifiers.forEach(s => {
|
||||
if (ssgExports.has(s.node.exported.name)) {
|
||||
state.isPrerender = true
|
||||
if (isDataIdentifier(s.node.exported.name, state)) {
|
||||
s.remove()
|
||||
}
|
||||
})
|
||||
|
@ -259,8 +282,7 @@ export default function nextTransformSsg({
|
|||
switch (decl.node.type) {
|
||||
case 'FunctionDeclaration': {
|
||||
const name = decl.node.id!.name
|
||||
if (ssgExports.has(name)) {
|
||||
state.isPrerender = true
|
||||
if (isDataIdentifier(name, state)) {
|
||||
path.remove()
|
||||
}
|
||||
break
|
||||
|
@ -274,8 +296,7 @@ export default function nextTransformSsg({
|
|||
return
|
||||
}
|
||||
const name = d.node.id.name
|
||||
if (ssgExports.has(name)) {
|
||||
state.isPrerender = true
|
||||
if (isDataIdentifier(name, state)) {
|
||||
d.remove()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -63,6 +63,7 @@ import {
|
|||
} from './utils'
|
||||
import getBaseWebpackConfig from './webpack-config'
|
||||
import { writeBuildId } from './write-build-id'
|
||||
import escapeStringRegexp from 'escape-string-regexp'
|
||||
|
||||
const fsAccess = promisify(fs.access)
|
||||
const fsUnlink = promisify(fs.unlink)
|
||||
|
@ -258,22 +259,23 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
const routesManifestPath = path.join(distDir, ROUTES_MANIFEST)
|
||||
const routesManifest: any = {
|
||||
version: 1,
|
||||
basePath: config.experimental.basePath,
|
||||
redirects: redirects.map(r => buildCustomRoute(r, 'redirect')),
|
||||
rewrites: rewrites.map(r => buildCustomRoute(r, 'rewrite')),
|
||||
headers: headers.map(r => buildCustomRoute(r, 'header')),
|
||||
dynamicRoutes: getSortedRoutes(dynamicRoutes).map(page => ({
|
||||
page,
|
||||
regex: getRouteRegex(page).re.source,
|
||||
})),
|
||||
}
|
||||
|
||||
await mkdirp(distDir)
|
||||
await fsWriteFile(
|
||||
path.join(distDir, ROUTES_MANIFEST),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
basePath: config.experimental.basePath,
|
||||
redirects: redirects.map(r => buildCustomRoute(r, 'redirect')),
|
||||
rewrites: rewrites.map(r => buildCustomRoute(r, 'rewrite')),
|
||||
headers: headers.map(r => buildCustomRoute(r, 'header')),
|
||||
dynamicRoutes: getSortedRoutes(dynamicRoutes).map(page => ({
|
||||
page,
|
||||
regex: getRouteRegex(page).re.source,
|
||||
})),
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
// We need to write the manifest with rewrites before build
|
||||
// so serverless can import the manifest
|
||||
await fsWriteFile(routesManifestPath, JSON.stringify(routesManifest), 'utf8')
|
||||
|
||||
const configs = await Promise.all([
|
||||
getBaseWebpackConfig(dir, {
|
||||
|
@ -405,6 +407,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
const staticPages = new Set<string>()
|
||||
const invalidPages = new Set<string>()
|
||||
const hybridAmpPages = new Set<string>()
|
||||
const serverPropsPages = new Set<string>()
|
||||
const additionalSsgPaths = new Map<string, Array<string>>()
|
||||
const pageInfos = new Map<string, PageInfo>()
|
||||
const pagesManifest = JSON.parse(await fsReadFile(manifestPath, 'utf8'))
|
||||
|
@ -502,6 +505,8 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
additionalSsgPaths.set(page, result.prerenderRoutes)
|
||||
ssgPageRoutes = result.prerenderRoutes
|
||||
}
|
||||
} else if (result.hasServerProps) {
|
||||
serverPropsPages.add(page)
|
||||
} else if (result.isStatic && customAppGetInitialProps === false) {
|
||||
staticPages.add(page)
|
||||
isStatic = true
|
||||
|
@ -525,6 +530,41 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
)
|
||||
staticCheckWorkers.end()
|
||||
|
||||
if (serverPropsPages.size > 0) {
|
||||
// We update the routes manifest after the build with the
|
||||
// serverProps routes since we can't determine this until after build
|
||||
routesManifest.serverPropsRoutes = {}
|
||||
|
||||
for (const page of serverPropsPages) {
|
||||
const dataRoute = path.posix.join(
|
||||
'/_next/data',
|
||||
buildId,
|
||||
`${page === '/' ? '/index' : page}.json`
|
||||
)
|
||||
|
||||
routesManifest.serverPropsRoutes[page] = {
|
||||
page,
|
||||
dataRouteRegex: isDynamicRoute(page)
|
||||
? getRouteRegex(dataRoute.replace(/\.json$/, '')).re.source.replace(
|
||||
/\(\?:\\\/\)\?\$$/,
|
||||
'\\.json$'
|
||||
)
|
||||
: new RegExp(
|
||||
`^${path.posix.join(
|
||||
'/_next/data',
|
||||
escapeStringRegexp(buildId),
|
||||
`${page === '/' ? '/index' : page}.json`
|
||||
)}$`
|
||||
).source,
|
||||
}
|
||||
}
|
||||
|
||||
await fsWriteFile(
|
||||
routesManifestPath,
|
||||
JSON.stringify(routesManifest),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
// Since custom _app.js can wrap the 404 page we have to opt-out of static optimization if it has getInitialProps
|
||||
// Only export the static 404 when there is no /_error present
|
||||
const useStatic404 =
|
||||
|
|
|
@ -9,7 +9,11 @@ import {
|
|||
Rewrite,
|
||||
getRedirectStatus,
|
||||
} from '../lib/check-custom-routes'
|
||||
import { SSG_GET_INITIAL_PROPS_CONFLICT } from '../lib/constants'
|
||||
import {
|
||||
SSG_GET_INITIAL_PROPS_CONFLICT,
|
||||
SERVER_PROPS_GET_INIT_PROPS_CONFLICT,
|
||||
SERVER_PROPS_SSG_CONFLICT,
|
||||
} from '../lib/constants'
|
||||
import prettyBytes from '../lib/pretty-bytes'
|
||||
import { recursiveReadDir } from '../lib/recursive-readdir'
|
||||
import { getRouteMatcher, getRouteRegex } from '../next-server/lib/router/utils'
|
||||
|
@ -481,6 +485,7 @@ export async function isPageStatic(
|
|||
): Promise<{
|
||||
isStatic?: boolean
|
||||
isHybridAmp?: boolean
|
||||
hasServerProps?: boolean
|
||||
hasStaticProps?: boolean
|
||||
prerenderRoutes?: string[] | undefined
|
||||
}> {
|
||||
|
@ -496,6 +501,7 @@ export async function isPageStatic(
|
|||
const hasGetInitialProps = !!(Comp as any).getInitialProps
|
||||
const hasStaticProps = !!mod.unstable_getStaticProps
|
||||
const hasStaticPaths = !!mod.unstable_getStaticPaths
|
||||
const hasServerProps = !!mod.unstable_getServerProps
|
||||
const hasLegacyStaticParams = !!mod.unstable_getStaticParams
|
||||
|
||||
if (hasLegacyStaticParams) {
|
||||
|
@ -510,6 +516,14 @@ export async function isPageStatic(
|
|||
throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT)
|
||||
}
|
||||
|
||||
if (hasGetInitialProps && hasServerProps) {
|
||||
throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT)
|
||||
}
|
||||
|
||||
if (hasStaticProps && hasServerProps) {
|
||||
throw new Error(SERVER_PROPS_SSG_CONFLICT)
|
||||
}
|
||||
|
||||
// A page cannot have static parameters if it is not a dynamic page.
|
||||
if (hasStaticProps && hasStaticPaths && !isDynamicRoute(page)) {
|
||||
throw new Error(
|
||||
|
@ -593,10 +607,11 @@ export async function isPageStatic(
|
|||
|
||||
const config = mod.config || {}
|
||||
return {
|
||||
isStatic: !hasStaticProps && !hasGetInitialProps,
|
||||
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
|
||||
isHybridAmp: config.amp === 'hybrid',
|
||||
prerenderRoutes: prerenderPaths,
|
||||
hasStaticProps,
|
||||
hasServerProps,
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') return {}
|
||||
|
|
|
@ -186,6 +186,7 @@ const nextServerlessLoader: loader.Loader = function() {
|
|||
export const unstable_getStaticProps = ComponentInfo['unstable_getStaticProp' + 's']
|
||||
export const unstable_getStaticParams = ComponentInfo['unstable_getStaticParam' + 's']
|
||||
export const unstable_getStaticPaths = ComponentInfo['unstable_getStaticPath' + 's']
|
||||
export const unstable_getServerProps = ComponentInfo['unstable_getServerProp' + 's']
|
||||
|
||||
${dynamicRouteMatcher}
|
||||
${handleRewrites}
|
||||
|
@ -207,6 +208,7 @@ const nextServerlessLoader: loader.Loader = function() {
|
|||
Document,
|
||||
buildManifest,
|
||||
unstable_getStaticProps,
|
||||
unstable_getServerProps,
|
||||
unstable_getStaticPaths,
|
||||
reactLoadableManifest,
|
||||
canonicalBase: "${canonicalBase}",
|
||||
|
@ -237,7 +239,7 @@ const nextServerlessLoader: loader.Loader = function() {
|
|||
${page === '/_error' ? `res.statusCode = 404` : ''}
|
||||
${
|
||||
pageIsDynamicRoute
|
||||
? `const params = fromExport && !unstable_getStaticProps ? {} : dynamicRouteMatcher(parsedUrl.pathname) || {};`
|
||||
? `const params = fromExport && !unstable_getStaticProps && !unstable_getServerProps ? {} : dynamicRouteMatcher(parsedUrl.pathname) || {};`
|
||||
: `const params = {};`
|
||||
}
|
||||
${
|
||||
|
@ -273,15 +275,22 @@ const nextServerlessLoader: loader.Loader = function() {
|
|||
`
|
||||
: `const nowParams = null;`
|
||||
}
|
||||
// make sure to set renderOpts to the correct params e.g. _params
|
||||
// if provided from worker or params if we're parsing them here
|
||||
renderOpts.params = _params || params
|
||||
|
||||
let result = await renderToHTML(req, res, "${page}", Object.assign({}, unstable_getStaticProps ? {} : parsedUrl.query, nowParams ? nowParams : params, _params), renderOpts)
|
||||
|
||||
if (_nextData && !fromExport) {
|
||||
const payload = JSON.stringify(renderOpts.pageData)
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Content-Length', Buffer.byteLength(payload))
|
||||
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
\`s-maxage=\${renderOpts.revalidate}, stale-while-revalidate\`
|
||||
unstable_getServerProps
|
||||
? \`no-cache, no-store, must-revalidate\`
|
||||
: \`s-maxage=\${renderOpts.revalidate}, stale-while-revalidate\`
|
||||
)
|
||||
res.end(payload)
|
||||
return null
|
||||
|
@ -295,6 +304,7 @@ const nextServerlessLoader: loader.Loader = function() {
|
|||
const result = await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
|
||||
unstable_getStaticProps: undefined,
|
||||
unstable_getStaticPaths: undefined,
|
||||
unstable_getServerProps: undefined,
|
||||
Component: Error
|
||||
}))
|
||||
return result
|
||||
|
@ -304,6 +314,7 @@ const nextServerlessLoader: loader.Loader = function() {
|
|||
const result = await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
|
||||
unstable_getStaticProps: undefined,
|
||||
unstable_getStaticPaths: undefined,
|
||||
unstable_getServerProps: undefined,
|
||||
Component: Error,
|
||||
err
|
||||
}))
|
||||
|
|
|
@ -191,7 +191,7 @@ export default async function({
|
|||
html = components.Component
|
||||
queryWithAutoExportWarn()
|
||||
} else {
|
||||
curRenderOpts = { ...components, ...renderOpts, ampPath }
|
||||
curRenderOpts = { ...components, ...renderOpts, ampPath, params }
|
||||
html = await renderMethod(req, res, page, query, curRenderOpts)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,3 +25,7 @@ export const DOT_NEXT_ALIAS = 'private-dot-next'
|
|||
export const PUBLIC_DIR_MIDDLEWARE_CONFLICT = `You can not have a '_next' folder inside of your public folder. This conflicts with the internal '/_next' route. https://err.sh/zeit/next.js/public-next-folder-conflict`
|
||||
|
||||
export const SSG_GET_INITIAL_PROPS_CONFLICT = `You can not use getInitialProps with unstable_getStaticProps. To use SSG, please remove your getInitialProps`
|
||||
|
||||
export const SERVER_PROPS_GET_INIT_PROPS_CONFLICT = `You can not use getInitialProps with unstable_getServerProps. Please remove one or the other`
|
||||
|
||||
export const SERVER_PROPS_SSG_CONFLICT = `You can not use unstable_getStaticProps with unstable_getServerProps. To use SSG, please remove your unstable_getServerProps`
|
||||
|
|
|
@ -27,6 +27,9 @@ function toRoute(path: string): string {
|
|||
return path.replace(/\/$/, '') || '/'
|
||||
}
|
||||
|
||||
const prepareRoute = (path: string) =>
|
||||
toRoute(!path || path === '/' ? '/index' : path)
|
||||
|
||||
type Url = UrlObject | string
|
||||
|
||||
export type BaseRouter = {
|
||||
|
@ -61,6 +64,33 @@ type BeforePopStateCallback = (state: any) => boolean
|
|||
|
||||
type ComponentLoadCancel = (() => void) | null
|
||||
|
||||
const fetchNextData = (
|
||||
pathname: string,
|
||||
query: ParsedUrlQuery | null,
|
||||
cb?: (...args: any) => any
|
||||
) => {
|
||||
return fetch(
|
||||
formatWithValidation({
|
||||
// @ts-ignore __NEXT_DATA__
|
||||
pathname: `/_next/data/${__NEXT_DATA__.buildId}${pathname}.json`,
|
||||
query,
|
||||
})
|
||||
)
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load static props`)
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.then(data => {
|
||||
return cb ? cb(data) : data
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
;(err as any).code = 'PAGE_LOAD_ERROR'
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
export default class Router implements BaseRouter {
|
||||
route: string
|
||||
pathname: string
|
||||
|
@ -464,6 +494,8 @@ export default class Router implements BaseRouter {
|
|||
return this._getData<RouteInfo>(() =>
|
||||
(Component as any).__N_SSG
|
||||
? this._getStaticData(as)
|
||||
: (Component as any).__N_SSP
|
||||
? this._getServerData(as)
|
||||
: this.getInitialProps(
|
||||
Component,
|
||||
// we provide AppTree later so this needs to be `any`
|
||||
|
@ -676,31 +708,18 @@ export default class Router implements BaseRouter {
|
|||
})
|
||||
}
|
||||
|
||||
_getStaticData = (asPath: string, _cachedData?: object): Promise<object> => {
|
||||
let pathname = parse(asPath).pathname
|
||||
pathname = toRoute(!pathname || pathname === '/' ? '/index' : pathname)
|
||||
_getStaticData = (asPath: string): Promise<object> => {
|
||||
const pathname = prepareRoute(parse(asPath).pathname!)
|
||||
|
||||
return process.env.NODE_ENV === 'production' &&
|
||||
(_cachedData = this.sdc[pathname])
|
||||
? Promise.resolve(_cachedData)
|
||||
: fetch(
|
||||
// @ts-ignore __NEXT_DATA__
|
||||
`/_next/data/${__NEXT_DATA__.buildId}${pathname}.json`
|
||||
)
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load static props`)
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.then(data => {
|
||||
this.sdc[pathname!] = data
|
||||
return data
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
;(err as any).code = 'PAGE_LOAD_ERROR'
|
||||
throw err
|
||||
})
|
||||
return process.env.NODE_ENV === 'production' && this.sdc[pathname]
|
||||
? Promise.resolve(this.sdc[pathname])
|
||||
: fetchNextData(pathname, null, data => (this.sdc[pathname] = data))
|
||||
}
|
||||
|
||||
_getServerData = (asPath: string): Promise<object> => {
|
||||
let { pathname, query } = parse(asPath, true)
|
||||
pathname = prepareRoute(pathname!)
|
||||
return fetchNextData(pathname, query)
|
||||
}
|
||||
|
||||
getInitialProps(
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import {
|
||||
BUILD_MANIFEST,
|
||||
CLIENT_STATIC_FILES_PATH,
|
||||
|
@ -6,7 +8,6 @@ import {
|
|||
} from '../lib/constants'
|
||||
import { join } from 'path'
|
||||
import { requirePage } from './require'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { BuildManifest } from './get-page-files'
|
||||
import { AppType, DocumentType } from '../lib/utils'
|
||||
import { PageConfig, NextPageContext } from 'next/types'
|
||||
|
@ -33,6 +34,13 @@ type Unstable_getStaticProps = (params: {
|
|||
|
||||
type Unstable_getStaticPaths = () => Promise<Array<string | ParsedUrlQuery>>
|
||||
|
||||
type Unstable_getServerProps = (context: {
|
||||
params: ParsedUrlQuery | undefined
|
||||
req: IncomingMessage
|
||||
res: ServerResponse
|
||||
query: ParsedUrlQuery
|
||||
}) => Promise<{ [key: string]: any }>
|
||||
|
||||
export type LoadComponentsReturnType = {
|
||||
Component: React.ComponentType
|
||||
pageConfig?: PageConfig
|
||||
|
@ -43,6 +51,7 @@ export type LoadComponentsReturnType = {
|
|||
App: AppType
|
||||
unstable_getStaticProps?: Unstable_getStaticProps
|
||||
unstable_getStaticPaths?: Unstable_getStaticPaths
|
||||
unstable_getServerProps?: Unstable_getServerProps
|
||||
}
|
||||
|
||||
export async function loadComponents(
|
||||
|
@ -106,6 +115,7 @@ export async function loadComponents(
|
|||
DocumentMiddleware,
|
||||
reactLoadableManifest,
|
||||
pageConfig: ComponentMod.config || {},
|
||||
unstable_getServerProps: ComponentMod.unstable_getServerProps,
|
||||
unstable_getStaticProps: ComponentMod.unstable_getStaticProps,
|
||||
unstable_getStaticPaths: ComponentMod.unstable_getStaticPaths,
|
||||
}
|
||||
|
|
|
@ -382,7 +382,7 @@ export default class Server {
|
|||
req,
|
||||
res,
|
||||
pathname,
|
||||
{ _nextDataReq: '1' },
|
||||
{ ..._parsedUrl.query, _nextDataReq: '1' },
|
||||
parsedUrl
|
||||
)
|
||||
return {
|
||||
|
@ -823,7 +823,9 @@ export default class Server {
|
|||
if (revalidate) {
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
`s-maxage=${revalidate}, stale-while-revalidate`
|
||||
revalidate < 0
|
||||
? `no-cache, no-store, must-revalidate`
|
||||
: `s-maxage=${revalidate}, stale-while-revalidate`
|
||||
)
|
||||
} else if (revalidate === false) {
|
||||
res.setHeader(
|
||||
|
@ -865,25 +867,62 @@ export default class Server {
|
|||
typeof result.Component === 'object' &&
|
||||
typeof (result.Component as any).renderReqToHTML === 'function'
|
||||
const isSSG = !!result.unstable_getStaticProps
|
||||
const isServerProps = !!result.unstable_getServerProps
|
||||
|
||||
// Toggle whether or not this is a Data request
|
||||
const isDataReq = query._nextDataReq
|
||||
delete query._nextDataReq
|
||||
|
||||
// Serverless requests need its URL transformed back into the original
|
||||
// request path (to emulate lambda behavior in production)
|
||||
if (isLikeServerless && isDataReq) {
|
||||
let { pathname } = parseUrl(req.url || '', true)
|
||||
pathname = !pathname || pathname === '/' ? '/index' : pathname
|
||||
req.url = formatUrl({
|
||||
pathname: `/_next/data/${this.buildId}${pathname}.json`,
|
||||
query,
|
||||
})
|
||||
}
|
||||
|
||||
// non-spr requests should render like normal
|
||||
if (!isSSG) {
|
||||
// handle serverless
|
||||
if (isLikeServerless) {
|
||||
if (isDataReq) {
|
||||
const renderResult = await (result.Component as any).renderReqToHTML(
|
||||
req,
|
||||
res,
|
||||
true
|
||||
)
|
||||
|
||||
this.__sendPayload(
|
||||
res,
|
||||
JSON.stringify(renderResult?.renderOpts?.pageData),
|
||||
'application/json',
|
||||
-1
|
||||
)
|
||||
return null
|
||||
}
|
||||
this.prepareServerlessUrl(req, query)
|
||||
return (result.Component as any).renderReqToHTML(req, res)
|
||||
}
|
||||
|
||||
if (isDataReq && isServerProps) {
|
||||
const props = await renderToHTML(req, res, pathname, query, {
|
||||
...result,
|
||||
...opts,
|
||||
isDataReq,
|
||||
})
|
||||
this.__sendPayload(res, JSON.stringify(props), 'application/json', -1)
|
||||
return null
|
||||
}
|
||||
|
||||
return renderToHTML(req, res, pathname, query, {
|
||||
...result,
|
||||
...opts,
|
||||
})
|
||||
}
|
||||
|
||||
// Toggle whether or not this is an SPR Data request
|
||||
const isDataReq = query._nextDataReq
|
||||
delete query._nextDataReq
|
||||
|
||||
// Compute the SPR cache key
|
||||
const ssgCacheKey = parseUrl(req.url || '').pathname!
|
||||
|
||||
|
@ -909,14 +948,6 @@ export default class Server {
|
|||
|
||||
// If we're here, that means data is missing or it's stale.
|
||||
|
||||
// Serverless requests need its URL transformed back into the original
|
||||
// request path (to emulate lambda behavior in production)
|
||||
if (isLikeServerless && isDataReq) {
|
||||
let { pathname } = parseUrl(req.url || '', true)
|
||||
pathname = !pathname || pathname === '/' ? '/index' : pathname
|
||||
req.url = `/_next/data/${this.buildId}${pathname}.json`
|
||||
}
|
||||
|
||||
const doRender = withCoalescedInvoke(async function(): Promise<{
|
||||
html: string | null
|
||||
pageData: any
|
||||
|
@ -1033,6 +1064,7 @@ export default class Server {
|
|||
result,
|
||||
{
|
||||
...this.renderOpts,
|
||||
params,
|
||||
amphtml,
|
||||
hasAmp,
|
||||
}
|
||||
|
|
|
@ -24,7 +24,11 @@ import { AmpStateContext } from '../lib/amp-context'
|
|||
import optimizeAmp from './optimize-amp'
|
||||
import { isInAmpMode } from '../lib/amp'
|
||||
import { isDynamicRoute } from '../lib/router/utils/is-dynamic'
|
||||
import { SSG_GET_INITIAL_PROPS_CONFLICT } from '../../lib/constants'
|
||||
import {
|
||||
SSG_GET_INITIAL_PROPS_CONFLICT,
|
||||
SERVER_PROPS_GET_INIT_PROPS_CONFLICT,
|
||||
SERVER_PROPS_SSG_CONFLICT,
|
||||
} from '../../lib/constants'
|
||||
import { AMP_RENDER_TARGET } from '../lib/constants'
|
||||
import { LoadComponentsReturnType, ManifestItem } from './load-components'
|
||||
|
||||
|
@ -129,6 +133,8 @@ type RenderOpts = LoadComponentsReturnType & {
|
|||
ErrorDebug?: React.ComponentType<{ error: Error }>
|
||||
ampValidator?: (html: string, pathname: string) => Promise<void>
|
||||
documentMiddlewareEnabled?: boolean
|
||||
isDataReq?: boolean
|
||||
params?: ParsedUrlQuery
|
||||
}
|
||||
|
||||
function renderDocument(
|
||||
|
@ -222,6 +228,14 @@ function renderDocument(
|
|||
)
|
||||
}
|
||||
|
||||
const invalidKeysMsg = (methodName: string, invalidKeys: string[]) => {
|
||||
return (
|
||||
`Additional keys were returned from \`${methodName}\`. Properties intended for your component must be nested under the \`props\` key, e.g.:` +
|
||||
`\n\n\treturn { props: { title: 'My Title', content: '...' } }` +
|
||||
`\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.`
|
||||
)
|
||||
}
|
||||
|
||||
export async function renderToHTML(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
|
@ -246,6 +260,9 @@ export async function renderToHTML(
|
|||
ErrorDebug,
|
||||
unstable_getStaticProps,
|
||||
unstable_getStaticPaths,
|
||||
unstable_getServerProps,
|
||||
isDataReq,
|
||||
params,
|
||||
} = renderOpts
|
||||
|
||||
const callMiddleware = async (method: string, args: any[], props = false) => {
|
||||
|
@ -281,7 +298,10 @@ export async function renderToHTML(
|
|||
const hasPageGetInitialProps = !!(Component as any).getInitialProps
|
||||
|
||||
const isAutoExport =
|
||||
!hasPageGetInitialProps && defaultAppGetInitialProps && !isSpr
|
||||
!hasPageGetInitialProps &&
|
||||
defaultAppGetInitialProps &&
|
||||
!isSpr &&
|
||||
!unstable_getServerProps
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
|
@ -301,6 +321,14 @@ export async function renderToHTML(
|
|||
throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT + ` ${pathname}`)
|
||||
}
|
||||
|
||||
if (hasPageGetInitialProps && unstable_getServerProps) {
|
||||
throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT + ` ${pathname}`)
|
||||
}
|
||||
|
||||
if (unstable_getServerProps && isSpr) {
|
||||
throw new Error(SERVER_PROPS_SSG_CONFLICT + ` ${pathname}`)
|
||||
}
|
||||
|
||||
if (!!unstable_getStaticPaths && !isSpr) {
|
||||
throw new Error(
|
||||
`unstable_getStaticPaths was added without a unstable_getStaticProps in ${pathname}. Without unstable_getStaticProps, unstable_getStaticPaths does nothing`
|
||||
|
@ -395,7 +423,7 @@ export async function renderToHTML(
|
|||
|
||||
if (isSpr) {
|
||||
const data = await unstable_getStaticProps!({
|
||||
params: isDynamicRoute(pathname) ? query : undefined,
|
||||
params: isDynamicRoute(pathname) ? (query as any) : undefined,
|
||||
})
|
||||
|
||||
const invalidKeys = Object.keys(data).filter(
|
||||
|
@ -403,11 +431,7 @@ export async function renderToHTML(
|
|||
)
|
||||
|
||||
if (invalidKeys.length) {
|
||||
throw new Error(
|
||||
`Additional keys were returned from \`getStaticProps\`. Properties intended for your component must be nested under the \`props\` key, e.g.:` +
|
||||
`\n\n\treturn { props: { title: 'My Title', content: '...' } }` +
|
||||
`\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.`
|
||||
)
|
||||
throw new Error(invalidKeysMsg('getStaticProps', invalidKeys))
|
||||
}
|
||||
|
||||
if (typeof data.revalidate === 'number') {
|
||||
|
@ -452,6 +476,27 @@ export async function renderToHTML(
|
|||
renderOpts.err = err
|
||||
}
|
||||
|
||||
if (unstable_getServerProps) {
|
||||
const data = await unstable_getServerProps({
|
||||
params,
|
||||
query,
|
||||
req,
|
||||
res,
|
||||
})
|
||||
|
||||
const invalidKeys = Object.keys(data).filter(key => key !== 'props')
|
||||
|
||||
if (invalidKeys.length) {
|
||||
throw new Error(invalidKeysMsg('getServerProps', invalidKeys))
|
||||
}
|
||||
|
||||
props.pageProps = data.props
|
||||
;(renderOpts as any).pageData = props
|
||||
}
|
||||
// We only need to do this if we want to support calling
|
||||
// _app's getInitialProps for getServerProps if not this can be removed
|
||||
if (isDataReq) return props
|
||||
|
||||
// the response might be finished on the getInitialProps call
|
||||
if (isResSent(res) && !isSpr) return null
|
||||
|
||||
|
@ -504,7 +549,10 @@ export async function renderToHTML(
|
|||
)
|
||||
}
|
||||
const documentCtx = { ...ctx, renderPage }
|
||||
const docProps = await loadGetInitialProps(Document, documentCtx)
|
||||
const docProps: DocumentInitialProps = await loadGetInitialProps(
|
||||
Document,
|
||||
documentCtx
|
||||
)
|
||||
// the response might be finished on the getInitialProps call
|
||||
if (isResSent(res) && !isSpr) return null
|
||||
|
||||
|
@ -519,7 +567,7 @@ export async function renderToHTML(
|
|||
const dynamicImports: ManifestItem[] = []
|
||||
|
||||
for (const mod of reactLoadableModules) {
|
||||
const manifestItem = reactLoadableManifest[mod]
|
||||
const manifestItem: ManifestItem[] = reactLoadableManifest[mod]
|
||||
|
||||
if (manifestItem) {
|
||||
manifestItem.forEach(item => {
|
||||
|
|
38
test/integration/getserverprops/pages/another/index.js
Normal file
38
test/integration/getserverprops/pages/another/index.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Link from 'next/link'
|
||||
import fs from 'fs'
|
||||
import findUp from 'find-up'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps() {
|
||||
const text = fs
|
||||
.readFileSync(
|
||||
findUp.sync('world.txt', {
|
||||
// prevent webpack from intercepting
|
||||
// eslint-disable-next-line no-eval
|
||||
cwd: eval(`__dirname`),
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
.trim()
|
||||
|
||||
return {
|
||||
props: {
|
||||
world: text,
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ world, time }) => (
|
||||
<>
|
||||
<p>hello {world}</p>
|
||||
<span id="anotherTime">time: {time}</span>
|
||||
<Link href="/">
|
||||
<a id="home">to home</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/something">
|
||||
<a id="something">to something</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps({ query }) {
|
||||
return {
|
||||
props: {
|
||||
post: query.post,
|
||||
comment: query.comment,
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ post, comment, time }) => {
|
||||
return (
|
||||
<>
|
||||
<p>Post: {post}</p>
|
||||
<p>Comment: {comment}</p>
|
||||
<span>time: {time}</span>
|
||||
<Link href="/">
|
||||
<a id="home">to home</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
38
test/integration/getserverprops/pages/blog/[post]/index.js
Normal file
38
test/integration/getserverprops/pages/blog/[post]/index.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps({ params }) {
|
||||
if (params.post === 'post-10') {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => resolve(), 1000)
|
||||
})
|
||||
}
|
||||
|
||||
if (params.post === 'post-100') {
|
||||
throw new Error('such broken..')
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
params,
|
||||
post: params.post,
|
||||
time: (await import('perf_hooks')).performance.now(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ post, time, params }) => {
|
||||
return (
|
||||
<>
|
||||
<p>Post: {post}</p>
|
||||
<span>time: {time}</span>
|
||||
<div id="params">{JSON.stringify(params)}</div>
|
||||
<div id="query">{JSON.stringify(useRouter().query)}</div>
|
||||
<Link href="/">
|
||||
<a id="home">to home</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
24
test/integration/getserverprops/pages/blog/index.js
Normal file
24
test/integration/getserverprops/pages/blog/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps() {
|
||||
return {
|
||||
props: {
|
||||
slugs: ['post-1', 'post-2'],
|
||||
time: (await import('perf_hooks')).performance.now(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ slugs, time }) => {
|
||||
return (
|
||||
<>
|
||||
<p>Posts: {JSON.stringify(slugs)}</p>
|
||||
<span>time: {time}</span>
|
||||
<Link href="/">
|
||||
<a id="home">to home</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
34
test/integration/getserverprops/pages/catchall/[...path].js
Normal file
34
test/integration/getserverprops/pages/catchall/[...path].js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
params: params || {},
|
||||
time: new Date().getTime(),
|
||||
random: Math.random(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ world, time, params, random }) => {
|
||||
return (
|
||||
<>
|
||||
<p>hello: {world}</p>
|
||||
<span>time: {time}</span>
|
||||
<div id="random">{random}</div>
|
||||
<div id="params">{JSON.stringify(params)}</div>
|
||||
<div id="query">{JSON.stringify(useRouter().query)}</div>
|
||||
<Link href="/">
|
||||
<a id="home">to home</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/another">
|
||||
<a id="another">to another</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
25
test/integration/getserverprops/pages/default-revalidate.js
Normal file
25
test/integration/getserverprops/pages/default-revalidate.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ world, time }) => (
|
||||
<>
|
||||
<p>hello {world}</p>
|
||||
<span>time: {time}</span>
|
||||
<Link href="/">
|
||||
<a id="home">to home</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/something">
|
||||
<a id="something">to something</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
47
test/integration/getserverprops/pages/index.js
Normal file
47
test/integration/getserverprops/pages/index.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const Page = ({ world, time }) => {
|
||||
return (
|
||||
<>
|
||||
<p>hello {world}</p>
|
||||
<span>time: {time}</span>
|
||||
<Link href="/another">
|
||||
<a id="another">to another</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/something">
|
||||
<a id="something">to something</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/normal">
|
||||
<a id="normal">to normal</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/blog/[post]" as="/blog/post-1">
|
||||
<a id="post-1">to dynamic</a>
|
||||
</Link>
|
||||
<Link href="/blog/[post]" as="/blog/post-100">
|
||||
<a id="broken-post">to broken</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/blog/[post]/[comment]" as="/blog/post-1/comment-1">
|
||||
<a id="comment-1">to another dynamic</a>
|
||||
</Link>
|
||||
<Link href="/something?another=thing">
|
||||
<a id="something-query">to something?another=thing</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
34
test/integration/getserverprops/pages/invalid-keys.js
Normal file
34
test/integration/getserverprops/pages/invalid-keys.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps({ params, query }) {
|
||||
return {
|
||||
world: 'world',
|
||||
query: query || {},
|
||||
params: params || {},
|
||||
time: new Date().getTime(),
|
||||
random: Math.random(),
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ world, time, params, random, query }) => {
|
||||
return (
|
||||
<>
|
||||
<p>hello: {world}</p>
|
||||
<span>time: {time}</span>
|
||||
<div id="random">{random}</div>
|
||||
<div id="params">{JSON.stringify(params)}</div>
|
||||
<div id="initial-query">{JSON.stringify(query)}</div>
|
||||
<div id="query">{JSON.stringify(useRouter().query)}</div>
|
||||
<Link href="/">
|
||||
<a id="home">to home</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/another">
|
||||
<a id="another">to another</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
1
test/integration/getserverprops/pages/normal.js
Normal file
1
test/integration/getserverprops/pages/normal.js
Normal file
|
@ -0,0 +1 @@
|
|||
export default () => <p id="normal-text">a normal page</p>
|
36
test/integration/getserverprops/pages/something.js
Normal file
36
test/integration/getserverprops/pages/something.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps({ params, query }) {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
query: query || {},
|
||||
params: params || {},
|
||||
time: new Date().getTime(),
|
||||
random: Math.random(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ world, time, params, random, query }) => {
|
||||
return (
|
||||
<>
|
||||
<p>hello: {world}</p>
|
||||
<span>time: {time}</span>
|
||||
<div id="random">{random}</div>
|
||||
<div id="params">{JSON.stringify(params)}</div>
|
||||
<div id="initial-query">{JSON.stringify(query)}</div>
|
||||
<div id="query">{JSON.stringify(useRouter().query)}</div>
|
||||
<Link href="/">
|
||||
<a id="home">to home</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/another">
|
||||
<a id="another">to another</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
24
test/integration/getserverprops/pages/user/[user]/profile.js
Normal file
24
test/integration/getserverprops/pages/user/[user]/profile.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getServerProps({ query }) {
|
||||
return {
|
||||
props: {
|
||||
user: query.user,
|
||||
time: (await import('perf_hooks')).performance.now(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ user, time }) => {
|
||||
return (
|
||||
<>
|
||||
<p>User: {user}</p>
|
||||
<span>time: {time}</span>
|
||||
<Link href="/">
|
||||
<a id="home">to home</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
407
test/integration/getserverprops/test/index.test.js
Normal file
407
test/integration/getserverprops/test/index.test.js
Normal file
|
@ -0,0 +1,407 @@
|
|||
/* eslint-env jest */
|
||||
/* global jasmine */
|
||||
import fs from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import webdriver from 'next-webdriver'
|
||||
import cheerio from 'cheerio'
|
||||
import escapeRegex from 'escape-string-regexp'
|
||||
import {
|
||||
renderViaHTTP,
|
||||
fetchViaHTTP,
|
||||
findPort,
|
||||
launchApp,
|
||||
killApp,
|
||||
waitFor,
|
||||
nextBuild,
|
||||
nextStart,
|
||||
normalizeRegEx,
|
||||
} from 'next-test-utils'
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2
|
||||
const appDir = join(__dirname, '..')
|
||||
const nextConfig = join(appDir, 'next.config.js')
|
||||
let app
|
||||
let appPort
|
||||
let buildId
|
||||
|
||||
const expectedManifestRoutes = () => ({
|
||||
'/something': {
|
||||
page: '/something',
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/something.json$`
|
||||
),
|
||||
},
|
||||
'/blog/[post]': {
|
||||
page: '/blog/[post]',
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/blog\\/([^/]+?)\\.json$`
|
||||
),
|
||||
},
|
||||
'/': {
|
||||
page: '/',
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/index.json$`
|
||||
),
|
||||
},
|
||||
'/default-revalidate': {
|
||||
page: '/default-revalidate',
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/default-revalidate.json$`
|
||||
),
|
||||
},
|
||||
'/catchall/[...path]': {
|
||||
page: '/catchall/[...path]',
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/catchall\\/(.+?)\\.json$`
|
||||
),
|
||||
},
|
||||
'/blog': {
|
||||
page: '/blog',
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/blog.json$`
|
||||
),
|
||||
},
|
||||
'/blog/[post]/[comment]': {
|
||||
page: '/blog/[post]/[comment]',
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(
|
||||
buildId
|
||||
)}\\/blog\\/([^/]+?)\\/([^/]+?)\\.json$`
|
||||
),
|
||||
},
|
||||
'/user/[user]/profile': {
|
||||
page: '/user/[user]/profile',
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(
|
||||
buildId
|
||||
)}\\/user\\/([^/]+?)\\/profile\\.json$`
|
||||
),
|
||||
},
|
||||
'/another': {
|
||||
page: '/another',
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/another.json$`
|
||||
),
|
||||
},
|
||||
'/invalid-keys': {
|
||||
dataRouteRegex: normalizeRegEx(
|
||||
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/invalid-keys.json$`
|
||||
),
|
||||
page: '/invalid-keys',
|
||||
},
|
||||
})
|
||||
|
||||
const navigateTest = (dev = false) => {
|
||||
it('should navigate between pages successfully', async () => {
|
||||
const toBuild = [
|
||||
'/',
|
||||
'/another',
|
||||
'/something',
|
||||
'/normal',
|
||||
'/blog/post-1',
|
||||
'/blog/post-1/comment-1',
|
||||
]
|
||||
|
||||
await Promise.all(toBuild.map(pg => renderViaHTTP(appPort, pg)))
|
||||
|
||||
const browser = await webdriver(appPort, '/')
|
||||
let text = await browser.elementByCss('p').text()
|
||||
expect(text).toMatch(/hello.*?world/)
|
||||
|
||||
// hydration
|
||||
await waitFor(2500)
|
||||
|
||||
// go to /another
|
||||
async function goFromHomeToAnother() {
|
||||
await browser.elementByCss('#another').click()
|
||||
await browser.waitForElementByCss('#home')
|
||||
text = await browser.elementByCss('p').text()
|
||||
expect(text).toMatch(/hello.*?world/)
|
||||
}
|
||||
await goFromHomeToAnother()
|
||||
|
||||
// go to /
|
||||
async function goFromAnotherToHome() {
|
||||
await browser.eval('window.didTransition = 1')
|
||||
await browser.elementByCss('#home').click()
|
||||
await browser.waitForElementByCss('#another')
|
||||
text = await browser.elementByCss('p').text()
|
||||
expect(text).toMatch(/hello.*?world/)
|
||||
expect(await browser.eval('window.didTransition')).toBe(1)
|
||||
}
|
||||
await goFromAnotherToHome()
|
||||
|
||||
await goFromHomeToAnother()
|
||||
const snapTime = await browser.elementByCss('#anotherTime').text()
|
||||
|
||||
// Re-visit page
|
||||
await goFromAnotherToHome()
|
||||
await goFromHomeToAnother()
|
||||
|
||||
const nextTime = await browser.elementByCss('#anotherTime').text()
|
||||
expect(snapTime).not.toMatch(nextTime)
|
||||
|
||||
// Reset to Home for next test
|
||||
await goFromAnotherToHome()
|
||||
|
||||
// go to /something
|
||||
await browser.elementByCss('#something').click()
|
||||
await browser.waitForElementByCss('#home')
|
||||
text = await browser.elementByCss('p').text()
|
||||
expect(text).toMatch(/hello.*?world/)
|
||||
expect(await browser.eval('window.didTransition')).toBe(1)
|
||||
|
||||
// go to /
|
||||
await browser.elementByCss('#home').click()
|
||||
await browser.waitForElementByCss('#post-1')
|
||||
|
||||
// go to /blog/post-1
|
||||
await browser.elementByCss('#post-1').click()
|
||||
await browser.waitForElementByCss('#home')
|
||||
text = await browser.elementByCss('p').text()
|
||||
expect(text).toMatch(/Post:.*?post-1/)
|
||||
expect(await browser.eval('window.didTransition')).toBe(1)
|
||||
|
||||
// go to /
|
||||
await browser.elementByCss('#home').click()
|
||||
await browser.waitForElementByCss('#comment-1')
|
||||
|
||||
// go to /blog/post-1/comment-1
|
||||
await browser.elementByCss('#comment-1').click()
|
||||
await browser.waitForElementByCss('#home')
|
||||
text = await browser.elementByCss('p:nth-child(2)').text()
|
||||
expect(text).toMatch(/Comment:.*?comment-1/)
|
||||
expect(await browser.eval('window.didTransition')).toBe(1)
|
||||
|
||||
await browser.close()
|
||||
})
|
||||
}
|
||||
|
||||
const runTests = (dev = false) => {
|
||||
navigateTest(dev)
|
||||
|
||||
it('should SSR normal page correctly', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/')
|
||||
expect(html).toMatch(/hello.*?world/)
|
||||
})
|
||||
|
||||
it('should SSR getServerProps page correctly', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/blog/post-1')
|
||||
expect(html).toMatch(/Post:.*?post-1/)
|
||||
})
|
||||
|
||||
it('should supply query values SSR', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/blog/post-1?hello=world')
|
||||
const $ = cheerio.load(html)
|
||||
const params = $('#params').text()
|
||||
expect(JSON.parse(params)).toEqual({ post: 'post-1' })
|
||||
const query = $('#query').text()
|
||||
expect(JSON.parse(query)).toEqual({ hello: 'world', post: 'post-1' })
|
||||
})
|
||||
|
||||
it('should supply params values for catchall correctly', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/catchall/first')
|
||||
const $ = cheerio.load(html)
|
||||
const params = $('#params').text()
|
||||
expect(JSON.parse(params)).toEqual({ path: ['first'] })
|
||||
const query = $('#query').text()
|
||||
expect(JSON.parse(query)).toEqual({ path: ['first'] })
|
||||
|
||||
const data = JSON.parse(
|
||||
await renderViaHTTP(appPort, `/_next/data/${buildId}/catchall/first.json`)
|
||||
)
|
||||
|
||||
expect(data.pageProps.params).toEqual({ path: ['first'] })
|
||||
})
|
||||
|
||||
it('should return data correctly', async () => {
|
||||
const data = JSON.parse(
|
||||
await renderViaHTTP(appPort, `/_next/data/${buildId}/something.json`)
|
||||
)
|
||||
expect(data.pageProps.world).toBe('world')
|
||||
})
|
||||
|
||||
it('should pass query for data request', async () => {
|
||||
const data = JSON.parse(
|
||||
await renderViaHTTP(
|
||||
appPort,
|
||||
`/_next/data/${buildId}/something.json?another=thing`
|
||||
)
|
||||
)
|
||||
expect(data.pageProps.query.another).toBe('thing')
|
||||
})
|
||||
|
||||
it('should return data correctly for dynamic page', async () => {
|
||||
const data = JSON.parse(
|
||||
await renderViaHTTP(appPort, `/_next/data/${buildId}/blog/post-1.json`)
|
||||
)
|
||||
expect(data.pageProps.post).toBe('post-1')
|
||||
})
|
||||
|
||||
it('should navigate to a normal page and back', async () => {
|
||||
const browser = await webdriver(appPort, '/')
|
||||
let text = await browser.elementByCss('p').text()
|
||||
expect(text).toMatch(/hello.*?world/)
|
||||
|
||||
await browser.elementByCss('#normal').click()
|
||||
await browser.waitForElementByCss('#normal-text')
|
||||
text = await browser.elementByCss('#normal-text').text()
|
||||
expect(text).toMatch(/a normal page/)
|
||||
})
|
||||
|
||||
it('should provide correct query value for dynamic page', async () => {
|
||||
const html = await renderViaHTTP(
|
||||
appPort,
|
||||
'/blog/post-1?post=something-else'
|
||||
)
|
||||
const $ = cheerio.load(html)
|
||||
const query = JSON.parse($('#query').text())
|
||||
expect(query.post).toBe('post-1')
|
||||
})
|
||||
|
||||
it('should parse query values on mount correctly', async () => {
|
||||
const browser = await webdriver(appPort, '/blog/post-1?another=value')
|
||||
await waitFor(2000)
|
||||
const text = await browser.elementByCss('#query').text()
|
||||
expect(text).toMatch(/another.*?value/)
|
||||
expect(text).toMatch(/post.*?post-1/)
|
||||
})
|
||||
|
||||
it('should pass query for data request on navigation', async () => {
|
||||
const browser = await webdriver(appPort, '/')
|
||||
await browser.eval('window.beforeNav = true')
|
||||
await browser.elementByCss('#something-query').click()
|
||||
await browser.waitForElementByCss('#initial-query')
|
||||
const query = JSON.parse(
|
||||
await browser.elementByCss('#initial-query').text()
|
||||
)
|
||||
expect(await browser.eval('window.beforeNav')).toBe(true)
|
||||
expect(query.another).toBe('thing')
|
||||
})
|
||||
|
||||
it('should reload page on failed data request', async () => {
|
||||
const browser = await webdriver(appPort, '/')
|
||||
await waitFor(500)
|
||||
await browser.eval('window.beforeClick = true')
|
||||
await browser.elementByCss('#broken-post').click()
|
||||
await waitFor(1000)
|
||||
expect(await browser.eval('window.beforeClick')).not.toBe('true')
|
||||
})
|
||||
|
||||
it('should always call getServerProps without caching', async () => {
|
||||
const initialRes = await fetchViaHTTP(appPort, '/something')
|
||||
const initialHtml = await initialRes.text()
|
||||
expect(initialHtml).toMatch(/hello.*?world/)
|
||||
|
||||
const newRes = await fetchViaHTTP(appPort, '/something')
|
||||
const newHtml = await newRes.text()
|
||||
expect(newHtml).toMatch(/hello.*?world/)
|
||||
expect(initialHtml !== newHtml).toBe(true)
|
||||
|
||||
const newerRes = await fetchViaHTTP(appPort, '/something')
|
||||
const newerHtml = await newerRes.text()
|
||||
expect(newerHtml).toMatch(/hello.*?world/)
|
||||
expect(newHtml !== newerHtml).toBe(true)
|
||||
})
|
||||
|
||||
it('should not re-call getServerProps when updating query', async () => {
|
||||
const browser = await webdriver(appPort, '/something?hello=world')
|
||||
await waitFor(2000)
|
||||
|
||||
const query = await browser.elementByCss('#query').text()
|
||||
expect(JSON.parse(query)).toEqual({ hello: 'world' })
|
||||
|
||||
const {
|
||||
props: {
|
||||
pageProps: { random: initialRandom },
|
||||
},
|
||||
} = await browser.eval('window.__NEXT_DATA__')
|
||||
|
||||
const curRandom = await browser.elementByCss('#random').text()
|
||||
expect(curRandom).toBe(initialRandom + '')
|
||||
})
|
||||
|
||||
if (dev) {
|
||||
it('should show error for extra keys returned from getServerProps', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/invalid-keys')
|
||||
expect(html).toContain(
|
||||
`Additional keys were returned from \`getServerProps\`. Properties intended for your component must be nested under the \`props\` key, e.g.:`
|
||||
)
|
||||
expect(html).toContain(
|
||||
`Keys that need to be moved: world, query, params, time, random`
|
||||
)
|
||||
})
|
||||
} else {
|
||||
it('should not fetch data on mount', async () => {
|
||||
const browser = await webdriver(appPort, '/blog/post-100')
|
||||
await browser.eval('window.thisShouldStay = true')
|
||||
await waitFor(2 * 1000)
|
||||
const val = await browser.eval('window.thisShouldStay')
|
||||
expect(val).toBe(true)
|
||||
})
|
||||
|
||||
it('should output routes-manifest correctly', async () => {
|
||||
const { serverPropsRoutes } = await fs.readJSON(
|
||||
join(appDir, '.next/routes-manifest.json')
|
||||
)
|
||||
for (const key of Object.keys(serverPropsRoutes)) {
|
||||
const val = serverPropsRoutes[key].dataRouteRegex
|
||||
serverPropsRoutes[key].dataRouteRegex = normalizeRegEx(val)
|
||||
}
|
||||
|
||||
expect(serverPropsRoutes).toEqual(expectedManifestRoutes())
|
||||
})
|
||||
|
||||
it('should set no-cache, no-store, must-revalidate header', async () => {
|
||||
const res = await fetchViaHTTP(
|
||||
appPort,
|
||||
`/_next/data/${escapeRegex(buildId)}/something.json`
|
||||
)
|
||||
expect(res.headers.get('cache-control')).toContain('no-cache')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe('unstable_getServerProps', () => {
|
||||
describe('dev mode', () => {
|
||||
beforeAll(async () => {
|
||||
appPort = await findPort()
|
||||
app = await launchApp(appDir, appPort)
|
||||
buildId = 'development'
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
runTests(true)
|
||||
})
|
||||
|
||||
describe('serverless mode', () => {
|
||||
beforeAll(async () => {
|
||||
await fs.writeFile(
|
||||
nextConfig,
|
||||
`module.exports = { target: 'serverless' }`,
|
||||
'utf8'
|
||||
)
|
||||
await nextBuild(appDir)
|
||||
appPort = await findPort()
|
||||
app = await nextStart(appDir, appPort)
|
||||
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
runTests()
|
||||
})
|
||||
|
||||
describe('production mode', () => {
|
||||
beforeAll(async () => {
|
||||
await fs.remove(nextConfig)
|
||||
await nextBuild(appDir, [], { stdout: true })
|
||||
|
||||
appPort = await findPort()
|
||||
app = await nextStart(appDir, appPort)
|
||||
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
runTests()
|
||||
})
|
||||
})
|
1
test/integration/getserverprops/world.txt
Normal file
1
test/integration/getserverprops/world.txt
Normal file
|
@ -0,0 +1 @@
|
|||
world
|
13
test/integration/mixed-ssg-serverprops-error/pages/index.js
Normal file
13
test/integration/mixed-ssg-serverprops-error/pages/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export const unstable_getStaticProps = async () => {
|
||||
return {
|
||||
props: { world: 'world' },
|
||||
}
|
||||
}
|
||||
|
||||
export const unstable_getServerProps = async () => {
|
||||
return {
|
||||
props: { world: 'world' },
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ world }) => <p>Hello {world}</p>
|
|
@ -0,0 +1,13 @@
|
|||
export const unstable_getStaticPaths = async () => {
|
||||
return {
|
||||
props: { world: 'world' }
|
||||
}
|
||||
}
|
||||
|
||||
export const unstable_getServerProps = async () => {
|
||||
return {
|
||||
props: { world: 'world' }
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ world }) => <p>Hello {world}</p>
|
|
@ -0,0 +1,32 @@
|
|||
/* eslint-env jest */
|
||||
/* global jasmine */
|
||||
import fs from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { nextBuild } from 'next-test-utils'
|
||||
import { SERVER_PROPS_SSG_CONFLICT } from 'next/dist/lib/constants'
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 1
|
||||
const appDir = join(__dirname, '..')
|
||||
const indexPage = join(appDir, 'pages/index.js')
|
||||
const indexPageAlt = `${indexPage}.alt`
|
||||
const indexPageBak = `${indexPage}.bak`
|
||||
|
||||
describe('Mixed getStaticProps and getServerProps error', () => {
|
||||
it('should error when exporting both getStaticProps and getServerProps', async () => {
|
||||
const { stderr } = await nextBuild(appDir, [], { stderr: true })
|
||||
expect(stderr).toContain(SERVER_PROPS_SSG_CONFLICT)
|
||||
})
|
||||
|
||||
it('should error when exporting both getStaticPaths and getServerProps', async () => {
|
||||
await fs.move(indexPage, indexPageBak)
|
||||
await fs.move(indexPageAlt, indexPage)
|
||||
|
||||
const { stderr, code } = await nextBuild(appDir, [], { stderr: true })
|
||||
|
||||
await fs.move(indexPage, indexPageAlt)
|
||||
await fs.move(indexPageBak, indexPage)
|
||||
|
||||
expect(code).toBe(1)
|
||||
expect(stderr).toContain(SERVER_PROPS_SSG_CONFLICT)
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue