Skip to content

Commit

Permalink
Add initial support for unstable_getServerProps (#10077)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ijjk authored and Timer committed Jan 27, 2020
1 parent abd69ec commit c24daa2
Show file tree
Hide file tree
Showing 26 changed files with 1,070 additions and 77 deletions.
37 changes: 29 additions & 8 deletions packages/next/build/babel/plugins/next-ssg-transform.ts
@@ -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
}

Expand Down Expand Up @@ -44,7 +49,7 @@ function decorateSsgExport(
'=',
t.memberExpression(
t.identifier(pageComponentVar),
t.identifier(prerenderId)
t.identifier(state.isPrerender ? prerenderId : serverPropsId)
),
t.booleanLiteral(true)
),
Expand All @@ -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,
}: {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
}
})
Expand All @@ -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
Expand All @@ -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()
}
})
Expand Down
70 changes: 55 additions & 15 deletions packages/next/build/index.ts
Expand Up @@ -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)
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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
Expand All @@ -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 =
Expand Down
19 changes: 17 additions & 2 deletions packages/next/build/utils.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -481,6 +485,7 @@ export async function isPageStatic(
): Promise<{
isStatic?: boolean
isHybridAmp?: boolean
hasServerProps?: boolean
hasStaticProps?: boolean
prerenderRoutes?: string[] | undefined
}> {
Expand All @@ -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) {
Expand All @@ -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(
Expand Down Expand Up @@ -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 {}
Expand Down
15 changes: 13 additions & 2 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Expand Up @@ -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}
Expand All @@ -207,6 +208,7 @@ const nextServerlessLoader: loader.Loader = function() {
Document,
buildManifest,
unstable_getStaticProps,
unstable_getServerProps,
unstable_getStaticPaths,
reactLoadableManifest,
canonicalBase: "${canonicalBase}",
Expand Down Expand Up @@ -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 = {};`
}
${
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}))
Expand Down
2 changes: 1 addition & 1 deletion packages/next/export/worker.js
Expand Up @@ -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)
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/next/lib/constants.ts
Expand Up @@ -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`

0 comments on commit c24daa2

Please sign in to comment.