next-serverless-loader.ts 17.0 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
import {
  BUILD_MANIFEST,
P
Prateek Bhatnagar 已提交
9
  FONT_MANIFEST,
10
  REACT_LOADABLE_MANIFEST,
J
Joe Haddad 已提交
11
  ROUTES_MANIFEST,
12 13
} from '../../../next-server/lib/constants'
import { isDynamicRoute } from '../../../next-server/lib/router/utils'
J
Joe Haddad 已提交
14
import { __ApiPreviewProps } from '../../../next-server/server/api-utils'
T
Tim Neutkens 已提交
15 16

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

34 35
const vercelHeader = 'x-vercel-id'

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

T
Tim Neutkens 已提交
56
  const buildManifest = join(distDir, BUILD_MANIFEST).replace(/\\/g, '/')
57 58 59 60
  const reactLoadableManifest = join(distDir, REACT_LOADABLE_MANIFEST).replace(
    /\\/g,
    '/'
  )
61
  const routesManifest = join(distDir, ROUTES_MANIFEST).replace(/\\/g, '/')
P
Prateek Bhatnagar 已提交
62 63 64 65
  const fontManifest = join(distDir, 'serverless', FONT_MANIFEST).replace(
    /\\/g,
    '/'
  )
66

67
  const escapedBuildId = escapeRegexp(buildId)
68
  const pageIsDynamicRoute = isDynamicRoute(page)
69

J
Joe Haddad 已提交
70 71 72 73
  const encodedPreviewProps = devalue(
    JSON.parse(previewProps) as __ApiPreviewProps
  )

74 75 76 77 78 79 80 81 82
  const collectDynamicRouteParams = pageIsDynamicRoute
    ? `
      function collectDynamicRouteParams(query) {
        const routeRegex = getRouteRegex("${page}")

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

83 84 85 86 87 88 89 90 91
            ${
              ''
              // non-provided optional values should be undefined so normalize
              // them to undefined
            }
            if(routeRegex.groups[key].optional && !value) {
              value = undefined
              delete query[key]
            }
92 93 94 95 96
            ${
              ''
              // query values from the proxy aren't already split into arrays
              // so make sure to normalize catch-all values
            }
97 98 99 100 101
            if (
              value &&
              typeof value === 'string' &&
              routeRegex.groups[key].repeat
            ) {
102 103 104
              value = value.split('/')
            }

105 106 107
            if (value) {
              prev[key] = value
            }
108 109 110 111 112
            return prev
          }, {})
      }
    `
    : ''
113 114
  const envLoading = `
    const { processEnv } = require('next/dist/lib/load-env-config')
115
    processEnv(${Buffer.from(loadedEnvFiles, 'base64').toString()})
116 117
  `

118 119
  const runtimeConfigImports = runtimeConfig
    ? `
120
      const { setConfig } = require('next/config')
121 122 123 124 125 126 127 128 129 130
    `
    : ''

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

131 132
  const dynamicRouteImports = pageIsDynamicRoute
    ? `
133 134
    const { getRouteMatcher } = require('next/dist/next-server/lib/router/utils/route-matcher');
      const { getRouteRegex } = require('next/dist/next-server/lib/router/utils/route-regex');
135 136 137 138 139 140 141 142 143 144
  `
    : ''

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

  const rewriteImports = `
145 146
    const { rewrites } = require('${routesManifest}')
    const { pathToRegexp, default: pathMatch } = require('next/dist/next-server/server/lib/path-match')
147 148 149 150
  `

  const handleRewrites = `
    const getCustomRouteMatcher = pathMatch(true)
151
    const {prepareDestination} = require('next/dist/next-server/server/router')
152 153 154 155 156 157 158

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

        if (params) {
159 160
          const { parsedDestination } = prepareDestination(
            rewrite.destination,
161
            params,
162 163 164
            parsedUrl.query,
            true,
            "${basePath}"
165
          )
166

167 168
          Object.assign(parsedUrl.query, parsedDestination.query, params)
          delete parsedDestination.query
169

170
          Object.assign(parsedUrl, parsedDestination)
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192

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

      return parsedUrl
193
    }
194 195
  `

196 197 198
  const handleBasePath = basePath
    ? `
    // always strip the basePath if configured since it is required
199
    req.url = req.url.replace(new RegExp('^${basePath}'), '') || '/'
200
    parsedUrl.pathname = parsedUrl.pathname.replace(new RegExp('^${basePath}'), '') || '/'
201 202 203
  `
    : ''

204 205
  if (page.match(API_ROUTE)) {
    return `
206 207
      import initServer from 'next-plugin-loader?middleware=on-init-server!'
      import onError from 'next-plugin-loader?middleware=on-error-server!'
208 209
      import 'next/dist/next-server/server/node-polyfill-fetch'

210
      ${envLoading}
211 212
      ${runtimeConfigImports}
      ${
213 214 215
        /*
          this needs to be called first so its available for any other imports
        */
216 217
        runtimeConfigSetter
      }
218 219 220 221 222 223
      ${dynamicRouteImports}
      const { parse } = require('url')
      const { apiResolver } = require('next/dist/next-server/server/api-utils')
      ${rewriteImports}

      ${dynamicRouteMatcher}
224
      ${collectDynamicRouteParams}
225

226
      ${handleRewrites}
227

228 229 230
      export default async (req, res) => {
        try {
          await initServer()
T
Tim Neutkens 已提交
231

232 233 234
          // We need to trust the dynamic route params from the proxy
          // to ensure we are using the correct values
          const trustQuery = req.headers['${vercelHeader}']
235
          const parsedUrl = handleRewrites(parse(req.url, true))
236

237 238
          ${handleBasePath}

239
          const params = ${
240
            pageIsDynamicRoute
241 242 243 244 245
              ? `
              trustQuery
                ? collectDynamicRouteParams(parsedUrl.query)
                : dynamicRouteMatcher(parsedUrl.pathname)
              `
246 247
              : `{}`
          }
248

249
          const resolver = require('${absolutePagePath}')
250
          await apiResolver(
251 252 253 254
            req,
            res,
            Object.assign({}, parsedUrl.query, params ),
            resolver,
J
Joe Haddad 已提交
255
            ${encodedPreviewProps},
256
            true,
257 258
            onError
          )
J
JJ Kasper 已提交
259
        } catch (err) {
260
          console.error(err)
J
JJ Kasper 已提交
261
          await onError(err)
262

263
          // TODO: better error for DECODE_FAILED?
264 265 266 267
          if (err.code === 'DECODE_FAILED') {
            res.statusCode = 400
            res.end('Bad Request')
          } else {
268 269
            // Throw the error to crash the serverless function
            throw err
270
          }
271 272 273 274 275
        }
      }
    `
  } else {
    return `
276 277
    import initServer from 'next-plugin-loader?middleware=on-init-server!'
    import onError from 'next-plugin-loader?middleware=on-error-server!'
278
    import 'next/dist/next-server/server/node-polyfill-fetch'
279
    const {isResSent} = require('next/dist/next-server/lib/utils');
280

281
    ${envLoading}
282 283
    ${runtimeConfigImports}
    ${
284
      // this needs to be called first so its available for any other imports
285 286
      runtimeConfigSetter
    }
287 288
    const {parse} = require('url')
    const {parse: parseQs} = require('querystring')
P
Prateek Bhatnagar 已提交
289
    const { renderToHTML } = require('next/dist/next-server/server/render');
290
    const { tryGetPreviewData } = require('next/dist/next-server/server/api-utils');
291
    const {sendPayload} = require('next/dist/next-server/server/send-payload');
292 293 294 295 296
    const buildManifest = require('${buildManifest}');
    const reactLoadableManifest = require('${reactLoadableManifest}');
    const Document = require('${absoluteDocumentPath}').default;
    const Error = require('${absoluteErrorPath}').default;
    const App = require('${absoluteAppPath}').default;
P
Prateek Bhatnagar 已提交
297

298 299
    ${dynamicRouteImports}
    ${rewriteImports}
300 301

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

303
    const Component = ComponentInfo.default
J
JJ Kasper 已提交
304
    export default Component
305
    export const unstable_getStaticParams = ComponentInfo['unstable_getStaticParam' + 's']
306 307 308 309 310 311
    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']
312
    export const unstable_getStaticPaths = ComponentInfo['unstable_getStaticPath' + 's']
313
    export const unstable_getServerProps = ComponentInfo['unstable_getServerProp' + 's']
314

315
    ${dynamicRouteMatcher}
316
    ${collectDynamicRouteParams}
317 318
    ${handleRewrites}

319
    export const config = ComponentInfo['confi' + 'g'] || {}
J
JJ Kasper 已提交
320
    export const _app = App
321 322
    export async function renderReqToHTML(req, res, renderMode, _renderOpts, _params) {
      const fromExport = renderMode === 'export' || renderMode === true;
323

T
Tim Neutkens 已提交
324 325 326 327
      const options = {
        App,
        Document,
        buildManifest,
328 329 330
        getStaticProps,
        getServerSideProps,
        getStaticPaths,
T
Tim Neutkens 已提交
331
        reactLoadableManifest,
332
        canonicalBase: "${canonicalBase}",
333
        buildId: "${buildId}",
J
JJ Kasper 已提交
334
        assetPrefix: "${assetPrefix}",
335
        runtimeConfig: runtimeConfig.publicRuntimeConfig || {},
J
Joe Haddad 已提交
336
        previewProps: ${encodedPreviewProps},
337
        env: process.env,
338
        basePath: "${basePath}",
339
        ..._renderOpts
J
JJ Kasper 已提交
340
      }
341
      let _nextData = false
342
      let parsedUrl
J
JJ Kasper 已提交
343

344
      try {
345 346 347 348
        // 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))
349

350 351
        ${handleBasePath}

352
        if (parsedUrl.pathname.match(/_next\\/data/)) {
353 354 355 356 357 358 359 360 361 362 363
          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'
          );
364 365 366 367 368 369
        }

        const renderOpts = Object.assign(
          {
            Component,
            pageConfig: config,
370 371
            nextExport: fromExport,
            isDataReq: _nextData,
372 373 374 375 376 377 378 379 380 381 382 383 384
          },
          options,
        )

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

J
Joe Haddad 已提交
386
        ${
387
          pageIsDynamicRoute
388 389 390 391 392 393 394 395 396 397
            ? `
            const params = (
              fromExport &&
              !getStaticProps &&
              !getServerSideProps
            ) ? {}
              : trustQuery
                ? collectDynamicRouteParams(parsedUrl.query)
                : dynamicRouteMatcher(parsedUrl.pathname) || {};
            `
J
Joe Haddad 已提交
398 399
            : `const params = {};`
        }
400
        ${
J
Joe Haddad 已提交
401
          // Temporary work around: `x-now-route-matches` is a platform header
402 403 404 405
          // _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).
406
          pageIsDynamicRoute
J
Joe Haddad 已提交
407
            ? `const nowParams = req.headers && req.headers["x-now-route-matches"]
408 409 410 411 412 413 414 415 416 417 418
              ? 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, {
419
                                [key]: obj[key]
420 421 422 423 424 425 426 427
                              }),
                            {}
                          );
                        }
                      },
                      groups
                    };
                  })()
J
Joe Haddad 已提交
428
                )(req.headers["x-now-route-matches"])
429 430
              : null;
          `
431 432
            : `const nowParams = null;`
        }
433 434 435 436
        // 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

437 438
        const isFallback = parsedUrl.query.__nextFallback

439 440 441
        const previewData = tryGetPreviewData(req, res, options.previewProps)
        const isPreviewMode = previewData !== false

P
Prateek Bhatnagar 已提交
442 443 444 445 446
        if (process.env.__NEXT_OPTIMIZE_FONTS) {
          renderOpts.optimizeFonts = true
          renderOpts.fontManifest = require('${fontManifest}')
          process.env['__NEXT_OPTIMIZE_FONT'+'S'] = true
        }
447
        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)
448

449 450
        if (!renderMode) {
          if (_nextData || getStaticProps || getServerSideProps) {
451 452 453
            sendPayload(req, res, _nextData ? JSON.stringify(renderOpts.pageData) : result, _nextData ? 'json' : 'html', ${
              generateEtags === 'true' ? true : false
            }, {
454 455 456 457 458 459
              private: isPreviewMode,
              stateful: !!getServerSideProps,
              revalidate: renderOpts.revalidate,
            })
            return null
          }
460 461 462 463 464
        } else if (isPreviewMode) {
          res.setHeader(
            'Cache-Control',
            'private, no-cache, no-store, max-age=0, must-revalidate'
          )
465
        }
J
JJ Kasper 已提交
466

467
        if (renderMode) return { html: result, renderOpts }
T
Tim Neutkens 已提交
468 469
        return result
      } catch (err) {
470 471 472 473
        if (!parsedUrl) {
          parsedUrl = parse(req.url, true)
        }

T
Tim Neutkens 已提交
474 475
        if (err.code === 'ENOENT') {
          res.statusCode = 404
476
        } else if (err.code === 'DECODE_FAILED') {
477
          // TODO: better error?
478
          res.statusCode = 400
T
Tim Neutkens 已提交
479
        } else {
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
          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 已提交
507
        }
508 509 510 511 512 513 514 515 516

        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 已提交
517 518
      }
    }
519
    export async function render (req, res) {
T
Tim Neutkens 已提交
520
      try {
521
        await initServer()
522
        const html = await renderReqToHTML(req, res)
523
        if (html) {
524 525 526
          sendPayload(req, res, html, 'html', {generateEtags: ${JSON.stringify(
            generateEtags === 'true'
          )}, poweredByHeader: ${JSON.stringify(poweredByHeader === 'true')}})
527
        }
T
Tim Neutkens 已提交
528 529
      } catch(err) {
        console.error(err)
530 531 532
        await onError(err)
        // Throw the error to crash the serverless function
        throw err
T
Tim Neutkens 已提交
533 534 535
      }
    }
  `
536
  }
T
Tim Neutkens 已提交
537 538 539
}

export default nextServerlessLoader