Skip to content

Commit

Permalink
Update to detect GSSP with edge runtime during build (#40076)
Browse files Browse the repository at this point in the history
This updates to handle detecting `getStaticProps`/`getServerSideProps` correctly during build when `experimental-edge` is being used. This also fixes not parsing dynamic route params correctly with the edge runtime and sets up the handling needed for the static generation for app opened in the below mentioned PR.

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

Fixes: [slack thread](https://vercel.slack.com/archives/C0289CGVAR2/p1661554455121189)
x-ref: #39884
  • Loading branch information
ijjk committed Aug 30, 2022
1 parent 0b57a01 commit 3bf8b2b
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 82 deletions.
59 changes: 42 additions & 17 deletions packages/next/build/index.ts
Expand Up @@ -1162,16 +1162,17 @@ export default async function build(
const errorPageStaticResult = nonStaticErrorPageSpan.traceAsyncFn(
async () =>
hasCustomErrorPage &&
staticWorkers.isPageStatic(
'/_error',
staticWorkers.isPageStatic({
page: '/_error',
distDir,
isLikeServerless,
serverless: isLikeServerless,
configFileName,
runtimeEnvConfig,
config.httpAgentOptions,
config.i18n?.locales,
config.i18n?.defaultLocale
)
httpAgentOptions: config.httpAgentOptions,
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
pageRuntime: config.experimental.runtime,
})
)

// we don't output _app in serverless mode so use _app export
Expand Down Expand Up @@ -1274,29 +1275,53 @@ export default async function build(
// Only calculate page static information if the page is not an
// app page.
pageType !== 'app' &&
!isReservedPage(page) &&
// We currently don't support static optimization in the Edge runtime.
pageRuntime !== SERVER_RUNTIME.edge
!isReservedPage(page)
) {
try {
let edgeInfo: any

if (pageRuntime === SERVER_RUNTIME.edge) {
const manifest = require(join(
distDir,
serverDir,
MIDDLEWARE_MANIFEST
))

edgeInfo = manifest.functions[page]
}

let isPageStaticSpan =
checkPageSpan.traceChild('is-page-static')
let workerResult = await isPageStaticSpan.traceAsyncFn(
() => {
return staticWorkers.isPageStatic(
return staticWorkers.isPageStatic({
page,
distDir,
isLikeServerless,
serverless: isLikeServerless,
configFileName,
runtimeEnvConfig,
config.httpAgentOptions,
config.i18n?.locales,
config.i18n?.defaultLocale,
isPageStaticSpan.id
)
httpAgentOptions: config.httpAgentOptions,
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
parentId: isPageStaticSpan.id,
pageRuntime,
edgeInfo,
})
}
)

if (pageRuntime === SERVER_RUNTIME.edge) {
if (workerResult.hasStaticProps) {
console.warn(
`"getStaticProps" is not yet supported fully with "experimental-edge", detected on ${page}`
)
}
// TODO: add handling for statically rendering edge
// pages and allow edge with Prerender outputs
workerResult.isStatic = false
workerResult.hasStaticProps = false
}

if (config.outputFileTracing) {
pageTraceIncludes.set(
page,
Expand Down
89 changes: 67 additions & 22 deletions packages/next/build/utils.ts
Expand Up @@ -34,7 +34,10 @@ import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-
import { UnwrapPromise } from '../lib/coalesced-function'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import * as Log from './output/log'
import { loadComponents } from '../server/load-components'
import {
loadComponents,
LoadComponentsReturnType,
} from '../server/load-components'
import { trace } from '../trace'
import { setHttpAgentOptions } from '../server/config'
import { recursiveDelete } from '../lib/recursive-delete'
Expand All @@ -43,6 +46,7 @@ import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
import { getRuntimeContext } from '../server/web/sandbox'

export type ROUTER_TYPE = 'pages' | 'app'

Expand Down Expand Up @@ -1008,17 +1012,31 @@ export async function buildStaticPaths(
}
}

export async function isPageStatic(
page: string,
distDir: string,
serverless: boolean,
configFileName: string,
runtimeEnvConfig: any,
httpAgentOptions: NextConfigComplete['httpAgentOptions'],
locales?: string[],
defaultLocale?: string,
export async function isPageStatic({
page,
distDir,
serverless,
configFileName,
runtimeEnvConfig,
httpAgentOptions,
locales,
defaultLocale,
parentId,
pageRuntime,
edgeInfo,
}: {
page: string
distDir: string
serverless: boolean
configFileName: string
runtimeEnvConfig: any
httpAgentOptions: NextConfigComplete['httpAgentOptions']
locales?: string[]
defaultLocale?: string
parentId?: any
): Promise<{
edgeInfo?: any
pageRuntime: ServerRuntime
}): Promise<{
isStatic?: boolean
isAmpOnly?: boolean
isHybridAmp?: boolean
Expand All @@ -1037,24 +1055,51 @@ export async function isPageStatic(
require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig)
setHttpAgentOptions(httpAgentOptions)

const mod = await loadComponents(distDir, page, serverless)
const Comp = mod.Component
let componentsResult: LoadComponentsReturnType

if (pageRuntime === SERVER_RUNTIME.edge) {
const runtime = await getRuntimeContext({
paths: edgeInfo.files.map((file: string) => path.join(distDir, file)),
env: edgeInfo.env,
edgeFunctionEntry: edgeInfo,
name: edgeInfo.name,
useCache: true,
distDir,
})
const mod =
runtime.context._ENTRIES[`middleware_${edgeInfo.name}`].ComponentMod

componentsResult = {
Component: mod.default,
ComponentMod: mod,
pageConfig: mod.config || {},
// @ts-expect-error this is not needed during require
buildManifest: {},
reactLoadableManifest: {},
getServerSideProps: mod.getServerSideProps,
getStaticPaths: mod.getStaticPaths,
getStaticProps: mod.getStaticProps,
}
} else {
componentsResult = await loadComponents(distDir, page, serverless)
}
const Comp = componentsResult.Component

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 = !!(await mod.ComponentMod
const hasStaticProps = !!componentsResult.getStaticProps
const hasStaticPaths = !!componentsResult.getStaticPaths
const hasServerProps = !!componentsResult.getServerSideProps
const hasLegacyServerProps = !!(await componentsResult.ComponentMod
.unstable_getServerProps)
const hasLegacyStaticProps = !!(await mod.ComponentMod
const hasLegacyStaticProps = !!(await componentsResult.ComponentMod
.unstable_getStaticProps)
const hasLegacyStaticPaths = !!(await mod.ComponentMod
const hasLegacyStaticPaths = !!(await componentsResult.ComponentMod
.unstable_getStaticPaths)
const hasLegacyStaticParams = !!(await mod.ComponentMod
const hasLegacyStaticParams = !!(await componentsResult.ComponentMod
.unstable_getStaticParams)

if (hasLegacyStaticParams) {
Expand Down Expand Up @@ -1121,15 +1166,15 @@ export async function isPageStatic(
encodedPaths: encodedPrerenderRoutes,
} = await buildStaticPaths(
page,
mod.getStaticPaths!,
componentsResult.getStaticPaths!,
configFileName,
locales,
defaultLocale
))
}

const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED
const config: PageConfig = mod.pageConfig
const config: PageConfig = componentsResult.pageConfig
return {
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
isHybridAmp: config.amp === 'hybrid',
Expand Down
Expand Up @@ -112,6 +112,8 @@ export default async function edgeSSRLoader(this: any) {
config: ${stringifiedConfig},
buildId: ${JSON.stringify(buildId)},
})
export const ComponentMod = pageMod
export default function(opts) {
return adapter({
Expand Down
79 changes: 43 additions & 36 deletions packages/next/build/webpack/loaders/next-serverless-loader/utils.ts
Expand Up @@ -68,6 +68,45 @@ export type ServerlessHandlerCtx = {
i18n?: NextConfig['i18n']
}

export function interpolateDynamicPath(
pathname: string,
params: ParsedUrlQuery,
defaultRouteRegex?: ReturnType<typeof getNamedRouteRegex> | undefined
) {
if (!defaultRouteRegex) return pathname

for (const param of Object.keys(defaultRouteRegex.groups)) {
const { optional, repeat } = defaultRouteRegex.groups[param]
let builtParam = `[${repeat ? '...' : ''}${param}]`

if (optional) {
builtParam = `[${builtParam}]`
}

const paramIdx = pathname!.indexOf(builtParam)

if (paramIdx > -1) {
let paramValue: string

if (Array.isArray(params[param])) {
paramValue = (params[param] as string[])
.map((v) => v && encodeURIComponent(v))
.join('/')
} else {
paramValue =
params[param] && encodeURIComponent(params[param] as string)
}

pathname =
pathname.slice(0, paramIdx) +
(paramValue || '') +
pathname.slice(paramIdx + builtParam.length)
}
}

return pathname
}

export function getUtils({
page,
i18n,
Expand Down Expand Up @@ -297,41 +336,6 @@ export function getUtils({
)(req.headers['x-now-route-matches'] as string) as ParsedUrlQuery
}

function interpolateDynamicPath(pathname: string, params: ParsedUrlQuery) {
if (!defaultRouteRegex) return pathname

for (const param of Object.keys(defaultRouteRegex.groups)) {
const { optional, repeat } = defaultRouteRegex.groups[param]
let builtParam = `[${repeat ? '...' : ''}${param}]`

if (optional) {
builtParam = `[${builtParam}]`
}

const paramIdx = pathname!.indexOf(builtParam)

if (paramIdx > -1) {
let paramValue: string

if (Array.isArray(params[param])) {
paramValue = (params[param] as string[])
.map((v) => v && encodeURIComponent(v))
.join('/')
} else {
paramValue =
params[param] && encodeURIComponent(params[param] as string)
}

pathname =
pathname.slice(0, paramIdx) +
(paramValue || '') +
pathname.slice(paramIdx + builtParam.length)
}
}

return pathname
}

function normalizeVercelUrl(
req: BaseNextRequest | IncomingMessage,
trustQuery: boolean,
Expand Down Expand Up @@ -570,8 +574,11 @@ export function getUtils({
normalizeVercelUrl,
dynamicRouteMatcher,
defaultRouteMatches,
interpolateDynamicPath,
getParamsFromRouteMatches,
normalizeDynamicRouteParams,
interpolateDynamicPath: (
pathname: string,
params: Record<string, string | string[]>
) => interpolateDynamicPath(pathname, params, defaultRouteRegex),
}
}
3 changes: 2 additions & 1 deletion packages/next/server/base-server.ts
Expand Up @@ -946,7 +946,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {

// Toggle whether or not this is a Data request
const isDataReq =
!!query.__nextDataReq && (isSSG || hasServerProps || isServerComponent)
!!(query.__nextDataReq || req.headers['x-nextjs-data']) &&
(isSSG || hasServerProps || isServerComponent)

delete query.__nextDataReq

Expand Down
29 changes: 28 additions & 1 deletion packages/next/server/next-server.ts
Expand Up @@ -96,6 +96,8 @@ import { checkIsManualRevalidate } from './api-utils'
import { shouldUseReactRoot, isTargetLikeServerless } from './utils'
import ResponseCache from './response-cache'
import { IncrementalCache } from './lib/incremental-cache'
import { interpolateDynamicPath } from '../build/webpack/loaders/next-serverless-loader/utils'
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'

if (shouldUseReactRoot) {
;(process.env as any).__NEXT_REACT_ROOT = 'true'
Expand Down Expand Up @@ -1951,7 +1953,32 @@ export default class NextNodeServer extends BaseServer {
}

// For middleware to "fetch" we must always provide an absolute URL
const url = getRequestMeta(params.req, '__NEXT_INIT_URL')!
const isDataReq = !!params.query.__nextDataReq
const query = urlQueryToSearchParams(
Object.assign({}, getRequestMeta(params.req, '__NEXT_INIT_QUERY') || {})
).toString()
const locale = params.query.__nextLocale
let normalizedPathname = params.page

if (isDataReq) {
params.req.headers['x-nextjs-data'] = '1'
}

if (isDynamicRoute(normalizedPathname)) {
const routeRegex = getNamedRouteRegex(params.page)
normalizedPathname = interpolateDynamicPath(
params.page,
Object.assign({}, params.params, params.query),
routeRegex
)
}

const url = `${getRequestMeta(params.req, '_protocol')}://${
this.hostname
}:${this.port}${locale ? `/${locale}` : ''}${normalizedPathname}${
query ? `?${query}` : ''
}`

if (!url.startsWith('http')) {
throw new Error(
'To use middleware you must provide a `hostname` and `port` to the Next.js Server'
Expand Down

0 comments on commit 3bf8b2b

Please sign in to comment.