Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add calling getStaticPaths in development before showing fallback #10611

Merged
merged 14 commits into from Feb 24, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
213 changes: 112 additions & 101 deletions packages/next/build/utils.ts
Expand Up @@ -497,6 +497,113 @@ export async function getPageSizeInKb(
return [-1, -1]
}

export async function buildStaticPaths(
page: string,
unstable_getStaticPaths: Unstable_getStaticPaths
): Promise<Array<string>> {
const prerenderPaths = new Set<string>()
const _routeRegex = getRouteRegex(page)
const _routeMatcher = getRouteMatcher(_routeRegex)

// Get the default list of allowed params.
const _validParamKeys = Object.keys(_routeMatcher(page))

const staticPathsResult = await unstable_getStaticPaths()

const expectedReturnVal =
`Expected: { paths: [] }\n` +
`See here for more info: https://err.sh/zeit/next.js/invalid-getstaticpaths-value`

if (
!staticPathsResult ||
typeof staticPathsResult !== 'object' ||
Array.isArray(staticPathsResult)
) {
throw new Error(
`Invalid value returned from unstable_getStaticPaths in ${page}. Received ${typeof staticPathsResult} ${expectedReturnVal}`
)
}

const invalidStaticPathKeys = Object.keys(staticPathsResult).filter(
key => key !== 'paths'
)

if (invalidStaticPathKeys.length > 0) {
throw new Error(
`Extra keys returned from unstable_getStaticPaths in ${page} (${invalidStaticPathKeys.join(
', '
)}) ${expectedReturnVal}`
)
}

const toPrerender = staticPathsResult.paths

if (!Array.isArray(toPrerender)) {
throw new Error(
`Invalid \`paths\` value returned from unstable_getStaticProps in ${page}.\n` +
`\`paths\` must be an array of strings or objects of shape { params: [key: string]: string }`
)
}

toPrerender.forEach(entry => {
// For a string-provided path, we must make sure it matches the dynamic
// route.
if (typeof entry === 'string') {
const result = _routeMatcher(entry)
if (!result) {
throw new Error(
`The provided path \`${entry}\` does not match the page: \`${page}\`.`
)
}

prerenderPaths?.add(entry)
}
// For the object-provided path, we must make sure it specifies all
// required keys.
else {
const invalidKeys = Object.keys(entry).filter(key => key !== 'params')
if (invalidKeys.length) {
throw new Error(
`Additional keys were returned from \`unstable_getStaticPaths\` in page "${page}". ` +
`URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` +
`\n\n\treturn { params: { ${_validParamKeys
.map(k => `${k}: ...`)
.join(', ')} } }` +
`\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n`
)
}

const { params = {} } = entry
let builtPage = page
_validParamKeys.forEach(validParamKey => {
const { repeat } = _routeRegex.groups[validParamKey]
const paramValue = params[validParamKey]
if (
(repeat && !Array.isArray(paramValue)) ||
(!repeat && typeof paramValue !== 'string')
) {
throw new Error(
`A required parameter (${validParamKey}) was not provided as ${
repeat ? 'an array' : 'a string'
} in unstable_getStaticPaths for ${page}`
)
}

builtPage = builtPage.replace(
`[${repeat ? '...' : ''}${validParamKey}]`,
repeat
? (paramValue as string[]).map(encodeURIComponent).join('/')
: encodeURIComponent(paramValue as string)
)
})

prerenderPaths?.add(builtPage)
}
})

return [...prerenderPaths]
}

export async function isPageStatic(
page: string,
serverBundle: string,
Expand Down Expand Up @@ -559,115 +666,19 @@ export async function isPageStatic(
)
}

let prerenderPaths: Set<string> | undefined
let prerenderRoutes: Array<string> | undefined
if (hasStaticProps && hasStaticPaths) {
prerenderPaths = new Set()

const _routeRegex = getRouteRegex(page)
const _routeMatcher = getRouteMatcher(_routeRegex)

// Get the default list of allowed params.
const _validParamKeys = Object.keys(_routeMatcher(page))

const staticPathsResult = await (mod.unstable_getStaticPaths as Unstable_getStaticPaths)()

const expectedReturnVal =
`Expected: { paths: [] }\n` +
`See here for more info: https://err.sh/zeit/next.js/invalid-getstaticpaths-value`

if (
!staticPathsResult ||
typeof staticPathsResult !== 'object' ||
Array.isArray(staticPathsResult)
) {
throw new Error(
`Invalid value returned from unstable_getStaticPaths in ${page}. Received ${typeof staticPathsResult} ${expectedReturnVal}`
)
}

const invalidStaticPathKeys = Object.keys(staticPathsResult).filter(
key => key !== 'paths'
prerenderRoutes = await buildStaticPaths(
page,
mod.unstable_getStaticPaths
)

if (invalidStaticPathKeys.length > 0) {
throw new Error(
`Extra keys returned from unstable_getStaticPaths in ${page} (${invalidStaticPathKeys.join(
', '
)}) ${expectedReturnVal}`
)
}

const toPrerender = staticPathsResult.paths

if (!Array.isArray(toPrerender)) {
throw new Error(
`Invalid \`paths\` value returned from unstable_getStaticProps in ${page}.\n` +
`\`paths\` must be an array of strings or objects of shape { params: [key: string]: string }`
)
}

toPrerender.forEach(entry => {
// For a string-provided path, we must make sure it matches the dynamic
// route.
if (typeof entry === 'string') {
const result = _routeMatcher(entry)
if (!result) {
throw new Error(
`The provided path \`${entry}\` does not match the page: \`${page}\`.`
)
}

prerenderPaths?.add(entry)
}
// For the object-provided path, we must make sure it specifies all
// required keys.
else {
const invalidKeys = Object.keys(entry).filter(key => key !== 'params')
if (invalidKeys.length) {
throw new Error(
`Additional keys were returned from \`unstable_getStaticPaths\` in page "${page}". ` +
`URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` +
`\n\n\treturn { params: { ${_validParamKeys
.map(k => `${k}: ...`)
.join(', ')} } }` +
`\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n`
)
}

const { params = {} } = entry
let builtPage = page
_validParamKeys.forEach(validParamKey => {
const { repeat } = _routeRegex.groups[validParamKey]
const paramValue = params[validParamKey]
if (
(repeat && !Array.isArray(paramValue)) ||
(!repeat && typeof paramValue !== 'string')
) {
throw new Error(
`A required parameter (${validParamKey}) was not provided as ${
repeat ? 'an array' : 'a string'
} in unstable_getStaticPaths for ${page}`
)
}

builtPage = builtPage.replace(
`[${repeat ? '...' : ''}${validParamKey}]`,
repeat
? (paramValue as string[]).map(encodeURIComponent).join('/')
: encodeURIComponent(paramValue as string)
)
})

prerenderPaths?.add(builtPage)
}
})
}

const config = mod.config || {}
return {
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
isHybridAmp: config.amp === 'hybrid',
prerenderRoutes: prerenderPaths && [...prerenderPaths],
prerenderRoutes,
hasStaticProps,
hasServerProps,
}
Expand Down
49 changes: 42 additions & 7 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -124,6 +124,10 @@ export default class Server {
redirects: Redirect[]
headers: Header[]
}
protected staticPathsWorker?: import('jest-worker').default & {
loadStaticPaths: typeof import('../../server/static-paths-worker').loadStaticPaths
}
private staticPathsCache: { [pathname: string]: string[] }

public constructor({
dir = '.',
Expand All @@ -139,6 +143,7 @@ export default class Server {
this.distDir = join(this.dir, this.nextConfig.distDir)
this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
this.staticPathsCache = {}

// Only serverRuntimeConfig needs the default
// publicRuntimeConfig gets it's default in client/index.js
Expand Down Expand Up @@ -879,6 +884,7 @@ export default class Server {
typeof (components.Component as any).renderReqToHTML === 'function'
const isSSG = !!components.unstable_getStaticProps
const isServerProps = !!components.unstable_getServerProps
const hasStaticPaths = !!components.unstable_getStaticPaths

// Toggle whether or not this is a Data request
const isDataReq = query._nextDataReq
Expand Down Expand Up @@ -939,9 +945,10 @@ export default class Server {
const isPreviewMode = previewData !== false

// Compute the SPR cache key
const urlPathname = parseUrl(req.url || '').pathname!
const ssgCacheKey = isPreviewMode
? `__` + nanoid() // Preview mode uses a throw away key to not coalesce preview invokes
: parseUrl(req.url || '').pathname!
: urlPathname

// Complete the response with cached data if its present
const cachedData = isPreviewMode
Expand Down Expand Up @@ -1007,6 +1014,34 @@ export default class Server {
const isProduction = !this.renderOpts.dev
const isDynamicPathname = isDynamicRoute(pathname)
const didRespond = isResSent(res)

// we lazy load the staticPaths to prevent the user
// from waiting on them for the page to load in dev mode
let staticPaths = this.staticPathsCache[pathname]
ijjk marked this conversation as resolved.
Show resolved Hide resolved

if (!isProduction && hasStaticPaths) {
// this is the first call so we need to block since getStaticPaths
ijjk marked this conversation as resolved.
Show resolved Hide resolved
// has not been called yet and we don't want to inaccurately render
// the fallback
const __getStaticPaths = async () => {
// TODO: bubble any errors from calling this to the client
const paths = await this.staticPathsWorker!.loadStaticPaths(
this.distDir,
this.buildId,
pathname,
!this.renderOpts.dev && this._isLikeServerless
)
this.staticPathsCache[pathname] = paths
ijjk marked this conversation as resolved.
Show resolved Hide resolved
return paths
}

if (!staticPaths) {
staticPaths = await __getStaticPaths()
} else {
withCoalescedInvoke(__getStaticPaths)(`staticPaths-${pathname}`, [])
ijjk marked this conversation as resolved.
Show resolved Hide resolved
}
}

// const isForcedBlocking =
// req.headers['X-Prerender-Bypass-Mode'] !== 'Blocking'

Expand All @@ -1017,20 +1052,20 @@ export default class Server {
//
// * Preview mode toggles all pages to be resolved in a blocking manner.
//
// * Non-dynamic pages should block (though this is an be an impossible
// * Non-dynamic pages should block (though this is an impossible
// case in production).
//
// * Dynamic pages should return their skeleton, then finish the data
// request on the client-side.
// * Dynamic pages should return their skeleton if not defined in
// getStaticPaths, then finish the data request on the client-side.
//
if (
!didRespond &&
!isDataReq &&
!isPreviewMode &&
isDynamicPathname &&
// TODO: development should trigger fallback when the path is not in
// `getStaticPaths`, for now, let's assume it is.
isProduction
// Development should trigger fallback when the path is not in
// `getStaticPaths`
(isProduction || !staticPaths || !staticPaths.includes(urlPathname))
) {
let html: string

Expand Down
13 changes: 13 additions & 0 deletions packages/next/server/next-dev-server.ts
Expand Up @@ -30,6 +30,7 @@ import { Telemetry } from '../telemetry/storage'
import ErrorDebug from './error-debug'
import HotReloader from './hot-reloader'
import { findPageFile } from './lib/find-page-file'
import Worker from 'jest-worker'

if (typeof React.Suspense === 'undefined') {
throw new Error(
Expand Down Expand Up @@ -79,6 +80,18 @@ export default class DevServer extends Server {
}
this.isCustomServer = !options.isNextDevCommand
this.pagesDir = findPagesDir(this.dir)
this.staticPathsWorker = new Worker(
require.resolve('./static-paths-worker'),
{
numWorkers: 1,
maxRetries: 0,
}
) as Worker & {
loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths
}

this.staticPathsWorker.getStdout().pipe(process.stdout)
this.staticPathsWorker.getStderr().pipe(process.stderr)
}

protected currentPhase() {
Expand Down