Skip to content

Commit

Permalink
Merge branch 'canary' into error/routes-non-array
Browse files Browse the repository at this point in the history
  • Loading branch information
Timer committed Feb 24, 2020
2 parents 9507701 + f17e170 commit b9ede16
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 154 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 @@ -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
43 changes: 36 additions & 7 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -124,6 +124,9 @@ export default class Server {
redirects: Redirect[]
headers: Header[]
}
protected staticPathsWorker?: import('jest-worker').default & {
loadStaticPaths: typeof import('../../server/static-paths-worker').loadStaticPaths
}

public constructor({
dir = '.',
Expand Down Expand Up @@ -884,6 +887,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 @@ -950,9 +954,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 @@ -1020,6 +1025,30 @@ 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: string[] | undefined

if (!isProduction && hasStaticPaths) {
const __getStaticPaths = async () => {
const paths = await this.staticPathsWorker!.loadStaticPaths(
this.distDir,
this.buildId,
pathname,
!this.renderOpts.dev && this._isLikeServerless
)
return paths
}

staticPaths = (
await withCoalescedInvoke(__getStaticPaths)(
`staticPaths-${pathname}`,
[]
)
).value
}

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

Expand All @@ -1030,20 +1059,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'),
{
maxRetries: 0,
numWorkers: this.nextConfig.experimental.cpus,
}
) 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
40 changes: 40 additions & 0 deletions packages/next/server/static-paths-worker.ts
@@ -0,0 +1,40 @@
import { join } from 'path'
import { buildStaticPaths } from '../build/utils'
import { getPagePath } from '../next-server/server/require'
import { loadComponents } from '../next-server/server/load-components'
import { PAGES_MANIFEST, SERVER_DIRECTORY } from '../next-server/lib/constants'

// we call getStaticPaths in a separate process 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
) {
// we need to clear any modules manually here since the
// require-cache-hot-loader doesn't affect require cache here
// since we're in a separate process
delete require.cache[join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST)]

const pagePath = await getPagePath(pathname, distDir, serverless, true)
delete require.cache[pagePath]

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 b9ede16

Please sign in to comment.