next-serverless-loader.ts 16.2 KB
Newer Older
G
devalue  
Guy Bedford 已提交
1
import devalue from 'next/dist/compiled/devalue'
G
Guy Bedford 已提交
2
import escapeRegexp from 'next/dist/compiled/escape-string-regexp'
3 4
import { join } from 'path'
import { parse } from 'querystring'
J
Joe Haddad 已提交
5 6
import { loader } from 'webpack'
import { API_ROUTE } from '../../../lib/constants'
7 8 9
import {
  BUILD_MANIFEST,
  REACT_LOADABLE_MANIFEST,
J
Joe Haddad 已提交
10
  ROUTES_MANIFEST,
11 12
} from '../../../next-server/lib/constants'
import { isDynamicRoute } from '../../../next-server/lib/router/utils'
J
Joe Haddad 已提交
13
import { __ApiPreviewProps } from '../../../next-server/server/api-utils'
T
Tim Neutkens 已提交
14 15

export type ServerlessLoaderQuery = {
16 17 18 19 20 21
  page: string
  distDir: string
  absolutePagePath: string
  absoluteAppPath: string
  absoluteDocumentPath: string
  absoluteErrorPath: string
22
  buildId: string
23
  assetPrefix: string
T
Tim Neutkens 已提交
24
  generateEtags: string
25
  canonicalBase: string
T
Tim Neutkens 已提交
26
  basePath: string
27
  runtimeConfig: string
J
Joe Haddad 已提交
28
  previewProps: string
29
  loadedEnvFiles: string
T
Tim Neutkens 已提交
30 31
}

32 33
const vercelHeader = 'x-vercel-id'

J
Joe Haddad 已提交
34
const nextServerlessLoader: loader.Loader = function () {
T
Tim Neutkens 已提交
35 36 37 38
  const {
    distDir,
    absolutePagePath,
    page,
39
    buildId,
40
    canonicalBase,
T
Tim Neutkens 已提交
41 42 43 44
    assetPrefix,
    absoluteAppPath,
    absoluteDocumentPath,
    absoluteErrorPath,
45
    generateEtags,
T
Tim Neutkens 已提交
46
    basePath,
47
    runtimeConfig,
J
Joe Haddad 已提交
48
    previewProps,
49
    loadedEnvFiles,
50 51
  }: ServerlessLoaderQuery =
    typeof this.query === 'string' ? parse(this.query.substr(1)) : this.query
T
Tim Neutkens 已提交
52

T
Tim Neutkens 已提交
53
  const buildManifest = join(distDir, BUILD_MANIFEST).replace(/\\/g, '/')
54 55 56 57
  const reactLoadableManifest = join(distDir, REACT_LOADABLE_MANIFEST).replace(
    /\\/g,
    '/'
  )
58 59
  const routesManifest = join(distDir, ROUTES_MANIFEST).replace(/\\/g, '/')

60
  const escapedBuildId = escapeRegexp(buildId)
61
  const pageIsDynamicRoute = isDynamicRoute(page)
62

J
Joe Haddad 已提交
63 64 65 66
  const encodedPreviewProps = devalue(
    JSON.parse(previewProps) as __ApiPreviewProps
  )

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
  const collectDynamicRouteParams = pageIsDynamicRoute
    ? `
      function collectDynamicRouteParams(query) {
        const routeRegex = getRouteRegex("${page}")

        return Object.keys(routeRegex.groups)
          .reduce((prev, key) => {
            let value = query[key]

            ${
              ''
              // query values from the proxy aren't already split into arrays
              // so make sure to normalize catch-all values
            }
            if (routeRegex.groups[key].repeat) {
              value = value.split('/')
            }

            prev[key] = value
            return prev
          }, {})
      }
    `
    : ''
91 92
  const envLoading = `
    const { processEnv } = require('next/dist/lib/load-env-config')
93
    processEnv(${Buffer.from(loadedEnvFiles, 'base64').toString()})
94 95
  `

96 97
  const runtimeConfigImports = runtimeConfig
    ? `
98
      const { setConfig } = require('next/config')
99 100 101 102 103 104 105 106 107 108
    `
    : ''

  const runtimeConfigSetter = runtimeConfig
    ? `
      const runtimeConfig = ${runtimeConfig}
      setConfig(runtimeConfig)
    `
    : 'const runtimeConfig = {}'

109 110
  const dynamicRouteImports = pageIsDynamicRoute
    ? `
111 112
    const { getRouteMatcher } = require('next/dist/next-server/lib/router/utils/route-matcher');
      const { getRouteRegex } = require('next/dist/next-server/lib/router/utils/route-regex');
113 114 115 116 117 118 119 120 121 122
  `
    : ''

  const dynamicRouteMatcher = pageIsDynamicRoute
    ? `
    const dynamicRouteMatcher = getRouteMatcher(getRouteRegex("${page}"))
  `
    : ''

  const rewriteImports = `
123 124
    const { rewrites } = require('${routesManifest}')
    const { pathToRegexp, default: pathMatch } = require('next/dist/next-server/server/lib/path-match')
125 126 127 128
  `

  const handleRewrites = `
    const getCustomRouteMatcher = pathMatch(true)
129
    const {prepareDestination} = require('next/dist/next-server/server/router')
130 131 132 133 134 135 136

    function handleRewrites(parsedUrl) {
      for (const rewrite of rewrites) {
        const matcher = getCustomRouteMatcher(rewrite.source)
        const params = matcher(parsedUrl.pathname)

        if (params) {
137 138
          const { parsedDestination } = prepareDestination(
            rewrite.destination,
139
            params,
140 141 142
            parsedUrl.query,
            true,
            "${basePath}"
143
          )
144

145 146
          Object.assign(parsedUrl.query, parsedDestination.query, params)
          delete parsedDestination.query
147

148
          Object.assign(parsedUrl, parsedDestination)
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170

          if (parsedUrl.pathname === '${page}'){
            break
          }
          ${
            pageIsDynamicRoute
              ? `
            const dynamicParams = dynamicRouteMatcher(parsedUrl.pathname);\
            if (dynamicParams) {
              parsedUrl.query = {
                ...parsedUrl.query,
                ...dynamicParams
              }
              break
            }
          `
              : ''
          }
        }
      }

      return parsedUrl
171
    }
172 173
  `

174 175 176
  const handleBasePath = basePath
    ? `
    // always strip the basePath if configured since it is required
177
    req.url = req.url.replace(new RegExp('^${basePath}'), '') || '/'
178
    parsedUrl.pathname = parsedUrl.pathname.replace(new RegExp('^${basePath}'), '') || '/'
179 180 181
  `
    : ''

182 183
  if (page.match(API_ROUTE)) {
    return `
184 185
      import initServer from 'next-plugin-loader?middleware=on-init-server!'
      import onError from 'next-plugin-loader?middleware=on-error-server!'
186 187
      import 'next/dist/next-server/server/node-polyfill-fetch'

188
      ${envLoading}
189 190
      ${runtimeConfigImports}
      ${
191 192 193
        /*
          this needs to be called first so its available for any other imports
        */
194 195
        runtimeConfigSetter
      }
196 197 198 199 200 201
      ${dynamicRouteImports}
      const { parse } = require('url')
      const { apiResolver } = require('next/dist/next-server/server/api-utils')
      ${rewriteImports}

      ${dynamicRouteMatcher}
202
      ${collectDynamicRouteParams}
203

204
      ${handleRewrites}
205

206 207 208
      export default async (req, res) => {
        try {
          await initServer()
T
Tim Neutkens 已提交
209

210 211 212
          // We need to trust the dynamic route params from the proxy
          // to ensure we are using the correct values
          const trustQuery = req.headers['${vercelHeader}']
213
          const parsedUrl = handleRewrites(parse(req.url, true))
214

215 216
          ${handleBasePath}

217
          const params = ${
218
            pageIsDynamicRoute
219 220 221 222 223
              ? `
              trustQuery
                ? collectDynamicRouteParams(parsedUrl.query)
                : dynamicRouteMatcher(parsedUrl.pathname)
              `
224 225
              : `{}`
          }
226

227
          const resolver = require('${absolutePagePath}')
228
          await apiResolver(
229 230 231 232
            req,
            res,
            Object.assign({}, parsedUrl.query, params ),
            resolver,
J
Joe Haddad 已提交
233
            ${encodedPreviewProps},
234
            true,
235 236
            onError
          )
J
JJ Kasper 已提交
237
        } catch (err) {
238
          console.error(err)
J
JJ Kasper 已提交
239
          await onError(err)
240

241
          // TODO: better error for DECODE_FAILED?
242 243 244 245
          if (err.code === 'DECODE_FAILED') {
            res.statusCode = 400
            res.end('Bad Request')
          } else {
246 247
            // Throw the error to crash the serverless function
            throw err
248
          }
249 250 251 252 253
        }
      }
    `
  } else {
    return `
254 255
    import initServer from 'next-plugin-loader?middleware=on-init-server!'
    import onError from 'next-plugin-loader?middleware=on-error-server!'
256
    import 'next/dist/next-server/server/node-polyfill-fetch'
257
    const {isResSent} = require('next/dist/next-server/lib/utils');
258

259
    ${envLoading}
260 261
    ${runtimeConfigImports}
    ${
262
      // this needs to be called first so its available for any other imports
263 264
      runtimeConfigSetter
    }
265 266
    const {parse} = require('url')
    const {parse: parseQs} = require('querystring')
267 268
    const {renderToHTML} = require('next/dist/next-server/server/render');
    const { tryGetPreviewData } = require('next/dist/next-server/server/api-utils');
269
    const {sendHTML} = require('next/dist/next-server/server/send-html');
270
    const {sendPayload} = require('next/dist/next-server/server/send-payload');
271 272 273 274 275 276 277
    const buildManifest = require('${buildManifest}');
    const reactLoadableManifest = require('${reactLoadableManifest}');
    const Document = require('${absoluteDocumentPath}').default;
    const Error = require('${absoluteErrorPath}').default;
    const App = require('${absoluteAppPath}').default;
    ${dynamicRouteImports}
    ${rewriteImports}
278 279

    const ComponentInfo = require('${absolutePagePath}')
280

281
    const Component = ComponentInfo.default
J
JJ Kasper 已提交
282
    export default Component
283
    export const unstable_getStaticParams = ComponentInfo['unstable_getStaticParam' + 's']
284 285 286 287 288 289
    export const getStaticProps = ComponentInfo['getStaticProp' + 's']
    export const getStaticPaths = ComponentInfo['getStaticPath' + 's']
    export const getServerSideProps = ComponentInfo['getServerSideProp' + 's']

    // kept for detecting legacy exports
    export const unstable_getStaticProps = ComponentInfo['unstable_getStaticProp' + 's']
290
    export const unstable_getStaticPaths = ComponentInfo['unstable_getStaticPath' + 's']
291
    export const unstable_getServerProps = ComponentInfo['unstable_getServerProp' + 's']
292

293
    ${dynamicRouteMatcher}
294
    ${collectDynamicRouteParams}
295 296
    ${handleRewrites}

297
    export const config = ComponentInfo['confi' + 'g'] || {}
J
JJ Kasper 已提交
298
    export const _app = App
299 300
    export async function renderReqToHTML(req, res, renderMode, _renderOpts, _params) {
      const fromExport = renderMode === 'export' || renderMode === true;
301

T
Tim Neutkens 已提交
302 303 304 305
      const options = {
        App,
        Document,
        buildManifest,
306 307 308
        getStaticProps,
        getServerSideProps,
        getStaticPaths,
T
Tim Neutkens 已提交
309
        reactLoadableManifest,
310
        canonicalBase: "${canonicalBase}",
311
        buildId: "${buildId}",
J
JJ Kasper 已提交
312
        assetPrefix: "${assetPrefix}",
313
        runtimeConfig: runtimeConfig.publicRuntimeConfig || {},
J
Joe Haddad 已提交
314
        previewProps: ${encodedPreviewProps},
315
        env: process.env,
316
        basePath: "${basePath}",
317
        ..._renderOpts
J
JJ Kasper 已提交
318
      }
319
      let _nextData = false
320
      let parsedUrl
J
JJ Kasper 已提交
321

322
      try {
323 324 325 326
        // We need to trust the dynamic route params from the proxy
        // to ensure we are using the correct values
        const trustQuery = !getStaticProps && req.headers['${vercelHeader}']
        const parsedUrl = handleRewrites(parse(req.url, true))
327

328 329
        ${handleBasePath}

330
        if (parsedUrl.pathname.match(/_next\\/data/)) {
331 332 333 334 335 336 337 338 339 340 341
          const {
            default: getRouteFromAssetPath,
          } = require('next/dist/next-server/lib/router/utils/get-route-from-asset-path');
          _nextData = true;
          parsedUrl.pathname = getRouteFromAssetPath(
            parsedUrl.pathname.replace(
              new RegExp('/_next/data/${escapedBuildId}/'),
              '/'
            ),
            '.json'
          );
342 343 344 345 346 347
        }

        const renderOpts = Object.assign(
          {
            Component,
            pageConfig: config,
348 349
            nextExport: fromExport,
            isDataReq: _nextData,
350 351 352 353 354 355 356 357 358 359 360 361 362
          },
          options,
        )

        ${
          page === '/_error'
            ? `
          if (!res.statusCode) {
            res.statusCode = 404
          }
        `
            : ''
        }
363

J
Joe Haddad 已提交
364
        ${
365
          pageIsDynamicRoute
366 367 368 369 370 371 372 373 374 375
            ? `
            const params = (
              fromExport &&
              !getStaticProps &&
              !getServerSideProps
            ) ? {}
              : trustQuery
                ? collectDynamicRouteParams(parsedUrl.query)
                : dynamicRouteMatcher(parsedUrl.pathname) || {};
            `
J
Joe Haddad 已提交
376 377
            : `const params = {};`
        }
378
        ${
J
Joe Haddad 已提交
379
          // Temporary work around: `x-now-route-matches` is a platform header
380 381 382 383
          // _only_ set for `Prerender` requests. We should move this logic
          // into our builder to ensure we're decoupled. However, this entails
          // removing reliance on `req.url` and using `req.query` instead
          // (which is needed for "custom routes" anyway).
384
          pageIsDynamicRoute
J
Joe Haddad 已提交
385
            ? `const nowParams = req.headers && req.headers["x-now-route-matches"]
386 387 388 389 390 391 392 393 394 395 396
              ? getRouteMatcher(
                  (function() {
                    const { re, groups } = getRouteRegex("${page}");
                    return {
                      re: {
                        // Simulate a RegExp match from the \`req.url\` input
                        exec: str => {
                          const obj = parseQs(str);
                          return Object.keys(obj).reduce(
                            (prev, key) =>
                              Object.assign(prev, {
397
                                [key]: obj[key]
398 399 400 401 402 403 404 405
                              }),
                            {}
                          );
                        }
                      },
                      groups
                    };
                  })()
J
Joe Haddad 已提交
406
                )(req.headers["x-now-route-matches"])
407 408
              : null;
          `
409 410
            : `const nowParams = null;`
        }
411 412 413 414
        // 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

415 416
        const isFallback = parsedUrl.query.__nextFallback

417 418 419
        const previewData = tryGetPreviewData(req, res, options.previewProps)
        const isPreviewMode = previewData !== false

420
        let result = await renderToHTML(req, res, "${page}", Object.assign({}, getStaticProps ? { ...(parsedUrl.query.amp ? { amp: '1' } : {}) } : parsedUrl.query, nowParams ? nowParams : params, _params, isFallback ? { __nextFallback: 'true' } : {}), renderOpts)
421

422 423
        if (!renderMode) {
          if (_nextData || getStaticProps || getServerSideProps) {
424 425 426
            sendPayload(req, res, _nextData ? JSON.stringify(renderOpts.pageData) : result, _nextData ? 'json' : 'html', ${
              generateEtags === 'true' ? true : false
            }, {
427 428 429 430 431 432
              private: isPreviewMode,
              stateful: !!getServerSideProps,
              revalidate: renderOpts.revalidate,
            })
            return null
          }
433 434 435 436 437
        } else if (isPreviewMode) {
          res.setHeader(
            'Cache-Control',
            'private, no-cache, no-store, max-age=0, must-revalidate'
          )
438
        }
J
JJ Kasper 已提交
439

440
        if (renderMode) return { html: result, renderOpts }
T
Tim Neutkens 已提交
441 442
        return result
      } catch (err) {
443 444 445 446
        if (!parsedUrl) {
          parsedUrl = parse(req.url, true)
        }

T
Tim Neutkens 已提交
447 448
        if (err.code === 'ENOENT') {
          res.statusCode = 404
449
        } else if (err.code === 'DECODE_FAILED') {
450
          // TODO: better error?
451
          res.statusCode = 400
T
Tim Neutkens 已提交
452
        } else {
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
          console.error('Unhandled error during request:', err)

          // Backwards compat (call getInitialProps in custom error):
          try {
            await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
              getStaticProps: undefined,
              getStaticPaths: undefined,
              getServerSideProps: undefined,
              Component: Error,
              err: err,
              // Short-circuit rendering:
              isDataReq: true
            }))
          } catch (underErrorErr) {
            console.error('Failed call /_error subroutine, continuing to crash function:', underErrorErr)
          }

          // Throw the error to crash the serverless function
          if (isResSent(res)) {
            console.error('!!! WARNING !!!')
            console.error(
              'Your function crashed, but closed the response before allowing the function to exit.\\n' +
              'This may cause unexpected behavior for the next request.'
            )
            console.error('!!! WARNING !!!')
          }
          throw err
T
Tim Neutkens 已提交
480
        }
481 482 483 484 485 486 487 488 489

        const result = await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
          getStaticProps: undefined,
          getStaticPaths: undefined,
          getServerSideProps: undefined,
          Component: Error,
          err: res.statusCode === 404 ? undefined : err
        }))
        return result
T
Tim Neutkens 已提交
490 491
      }
    }
492
    export async function render (req, res) {
T
Tim Neutkens 已提交
493
      try {
494
        await initServer()
495
        const html = await renderReqToHTML(req, res)
496
        if (html) {
497 498 499
          sendHTML(req, res, html, {generateEtags: ${
            generateEtags === 'true' ? true : false
          }})
500
        }
T
Tim Neutkens 已提交
501 502
      } catch(err) {
        console.error(err)
503 504 505
        await onError(err)
        // Throw the error to crash the serverless function
        throw err
T
Tim Neutkens 已提交
506 507 508
      }
    }
  `
509
  }
T
Tim Neutkens 已提交
510 511 512
}

export default nextServerlessLoader