Skip to content

Commit

Permalink
Add calling getStaticPaths in development before showing fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
ijjk committed Feb 20, 2020
1 parent f0c8230 commit 487e20d
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 152 deletions.
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 @@ -550,115 +657,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
57 changes: 50 additions & 7 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -59,9 +59,20 @@ import {
setSprCache,
} from './spr-cache'
import { isBlockedPage } from './utils'
import Worker from 'jest-worker'

const getCustomRouteMatcher = pathMatch(true)

const staticPathsWorker = new Worker(require.resolve('./static-paths-worker'), {
numWorkers: 1,
maxRetries: 0,
}) as Worker & {
loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths
}

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

type NextConfig = any

type Middleware = (
Expand Down Expand Up @@ -124,6 +135,7 @@ export default class Server {
redirects: Redirect[]
headers: Header[]
}
private staticPathsCache: { [pathname: string]: string[] }

public constructor({
dir = '.',
Expand All @@ -139,6 +151,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 +892,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 +953,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 +1022,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]

if (!isProduction && hasStaticPaths) {
// this is the first call so we need to block since getStaticPaths
// 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 staticPathsWorker.loadStaticPaths(
this.distDir,
this.buildId,
pathname,
!this.renderOpts.dev && this._isLikeServerless
)
this.staticPathsCache[pathname] = paths
return paths
}

if (!staticPaths) {
staticPaths = await __getStaticPaths()
} else {
withCoalescedInvoke(__getStaticPaths)(`staticPaths-${pathname}`, [])
}
}

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

Expand All @@ -1017,20 +1060,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
28 changes: 28 additions & 0 deletions packages/next/next-server/server/static-paths-worker.ts
@@ -0,0 +1,28 @@
import { loadComponents } from './load-components'
import { buildStaticPaths } from '../../build/utils'
// we call getStaticPaths in a separate thread to ensure
// side-effects aren't relied on in dev that will break
// during a production build
export async function loadStaticPaths(
distDir: string,
buildId: string,
pathname: string,
serverless: boolean
) {
const components = await loadComponents(
distDir,
buildId,
pathname,
serverless
)

if (!components.unstable_getStaticPaths) {
// we shouldn't get to this point since the worker should
// only be called for SSG pages with getStaticPaths
throw new Error(
`Invariant: failed to load page with unstable_getStaticPaths for ${pathname}`
)
}

return buildStaticPaths(pathname, components.unstable_getStaticPaths)
}

0 comments on commit 487e20d

Please sign in to comment.