rsnext/packages/next/build/webpack/loaders/next-serverless-loader.ts
JJ Kasper 85e720a092 Add experimental SPR support (#8832)
* initial commit for SPRv2

* Add initial SPR cache handling

* update SPR handling

* Implement SPR handling in render

* Update tests, handle caching with serverless next
start, add TODOs, and update manifest generating

* Handle no prerender-manifest from not being used

* Fix url.parse error

* Apply suggestions from code review

Co-Authored-By: Joe Haddad <joe.haddad@zeit.co>

* Replace set with constants in next-page-config

* simplify sprStatus.used

* Add error if getStaticProps is used with getInitialProps

* Remove stale TODO

* Update revalidate values in SPR cache for non-seeded routes

* Apply suggestions from code review

* Remove concurrency type

* Rename variable for clarity

* Add copying prerender files during export

* Add comment for clarity

* Fix exporting

* Update comment

* Add additional note

* Rename variable

* Update to not re-export SPR pages from build

* Hard navigate when fetching data fails

* Remove default extension

* Add brackets

* Add checking output files to prerender tests

* Adjust export move logic

* Clarify behavior of export aggregation

* Update variable names for clarity

* Update tests

* Add comment

* s/an oxymoron/contradictory/

* rename

* Extract error case

* Add tests for exporting SPR pages and update
/_next/data endpoint to end with .json

* Relocate variable

* Adjust route building

* Rename to unstable

* Rename unstable_getStaticParams

* Fix linting

* Only add this when a data request

* Update prerender data tests

* s/isServerless/isLikeServerless/

* Don't rely on query for `next start` in serverless mode

* Rename var

* Update renderedDuringBuild check

* Add test for dynamic param with bracket

* Fix serverless next start handling

* remove todo

* Adjust comment

* Update calculateRevalidate

* Remove cache logic from render.tsx

* Remove extra imports

* Move SPR cache logic to next-server

* Remove old isDynamic prop

* Add calling App getInitialProps for SPR pages

* Update revalidate logic

* Add isStale to SprCacheValue

* Update headers for SPR

* add awaiting pendingRevalidation

* Dont return null for revalidation render

* Adjust logic

* Be sure to remove coalesced render

* Fix data for serverless

* Create a method coalescing utility

* Remove TODO

* Extract send payload helper

* Wrap in-line

* Move around some code

* Add tests for de-duping and revalidating

* Update prerender manifest test
2019-09-24 10:50:04 +02:00

169 lines
5.3 KiB
TypeScript

import { loader } from 'webpack'
import { join } from 'path'
import { parse } from 'querystring'
import {
BUILD_MANIFEST,
REACT_LOADABLE_MANIFEST,
} from '../../../next-server/lib/constants'
import { isDynamicRoute } from '../../../next-server/lib/router/utils'
import { API_ROUTE } from '../../../lib/constants'
export type ServerlessLoaderQuery = {
page: string
distDir: string
absolutePagePath: string
absoluteAppPath: string
absoluteDocumentPath: string
absoluteErrorPath: string
buildId: string
assetPrefix: string
ampBindInitData: boolean | string
generateEtags: string
canonicalBase: string
}
const nextServerlessLoader: loader.Loader = function() {
const {
distDir,
absolutePagePath,
page,
buildId,
canonicalBase,
assetPrefix,
ampBindInitData,
absoluteAppPath,
absoluteDocumentPath,
absoluteErrorPath,
generateEtags,
}: ServerlessLoaderQuery =
typeof this.query === 'string' ? parse(this.query.substr(1)) : this.query
const buildManifest = join(distDir, BUILD_MANIFEST).replace(/\\/g, '/')
const reactLoadableManifest = join(distDir, REACT_LOADABLE_MANIFEST).replace(
/\\/g,
'/'
)
if (page.match(API_ROUTE)) {
return `
${
isDynamicRoute(page)
? `
import { getRouteMatcher } from 'next/dist/next-server/lib/router/utils/route-matcher';
import { getRouteRegex } from 'next/dist/next-server/lib/router/utils/route-regex';
`
: ``
}
import { parse } from 'url'
import { apiResolver } from 'next/dist/next-server/server/api-utils'
export default (req, res) => {
const params = ${
isDynamicRoute(page)
? `getRouteMatcher(getRouteRegex('${page}'))(parse(req.url).pathname)`
: `{}`
}
const resolver = require('${absolutePagePath}')
apiResolver(req, res, params, resolver)
}
`
} else {
return `
import {parse} from 'url'
import {renderToHTML} from 'next/dist/next-server/server/render';
import {sendHTML} from 'next/dist/next-server/server/send-html';
${
isDynamicRoute(page)
? `import {getRouteMatcher, getRouteRegex} from 'next/dist/next-server/lib/router/utils';`
: ''
}
import buildManifest from '${buildManifest}';
import reactLoadableManifest from '${reactLoadableManifest}';
import Document from '${absoluteDocumentPath}';
import Error from '${absoluteErrorPath}';
import App from '${absoluteAppPath}';
import * as ComponentInfo from '${absolutePagePath}';
const Component = ComponentInfo.default
export default Component
export const unstable_getStaticProps = ComponentInfo['unstable_getStaticProp' + 's']
${
isDynamicRoute(page)
? "export const unstable_getStaticParams = ComponentInfo['unstable_getStaticParam' + 's']"
: ''
}
export const config = ComponentInfo['confi' + 'g'] || {}
export const _app = App
export async function renderReqToHTML(req, res, fromExport) {
const options = {
App,
Document,
buildManifest,
unstable_getStaticProps,
reactLoadableManifest,
canonicalBase: "${canonicalBase}",
buildId: "${buildId}",
assetPrefix: "${assetPrefix}",
ampBindInitData: ${ampBindInitData === true ||
ampBindInitData === 'true'},
}
let sprData = false
if (req.url.match(/_next\\/data/)) {
sprData = true
req.url = req.url
.replace(/\\/_next\\/data\\//, '/')
.replace(/\\.json$/, '')
}
const parsedUrl = parse(req.url, true)
const renderOpts = Object.assign(
{
Component,
pageConfig: config,
dataOnly: req.headers && (req.headers.accept || '').indexOf('application/amp.bind+json') !== -1,
nextExport: fromExport
},
options,
)
try {
${page === '/_error' ? `res.statusCode = 404` : ''}
${
isDynamicRoute(page)
? `const params = fromExport && !unstable_getStaticProps ? {} : getRouteMatcher(getRouteRegex("${page}"))(parsedUrl.pathname) || {};`
: `const params = {};`
}
const result = await renderToHTML(req, res, "${page}", Object.assign({}, unstable_getStaticProps ? {} : parsedUrl.query, params, sprData ? { _nextSprData: '1' } : {}), renderOpts)
if (fromExport) return { html: result, renderOpts }
return result
} catch (err) {
if (err.code === 'ENOENT') {
res.statusCode = 404
const result = await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
Component: Error
}))
return result
} else {
console.error(err)
res.statusCode = 500
const result = await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
Component: Error,
err
}))
return result
}
}
}
export async function render (req, res) {
try {
const html = await renderReqToHTML(req, res)
sendHTML(req, res, html, {generateEtags: ${generateEtags}})
} catch(err) {
console.error(err)
res.statusCode = 500
res.end('Internal Server Error')
}
}
`
}
}
export default nextServerlessLoader