next-serverless-loader.ts 12.5 KB
Newer Older
J
Joe Haddad 已提交
1 2
import devalue from 'devalue'
import escapeRegexp from '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
T
Tim Neutkens 已提交
29 30
}

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

T
Tim Neutkens 已提交
49
  const buildManifest = join(distDir, BUILD_MANIFEST).replace(/\\/g, '/')
50 51 52 53
  const reactLoadableManifest = join(distDir, REACT_LOADABLE_MANIFEST).replace(
    /\\/g,
    '/'
  )
54 55
  const routesManifest = join(distDir, ROUTES_MANIFEST).replace(/\\/g, '/')

56
  const escapedBuildId = escapeRegexp(buildId)
57
  const pageIsDynamicRoute = isDynamicRoute(page)
58

J
Joe Haddad 已提交
59 60 61 62
  const encodedPreviewProps = devalue(
    JSON.parse(previewProps) as __ApiPreviewProps
  )

63 64
  const runtimeConfigImports = runtimeConfig
    ? `
65
      const { setConfig } = require('next/config')
66 67 68 69 70 71 72 73 74 75
    `
    : ''

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

76 77
  const dynamicRouteImports = pageIsDynamicRoute
    ? `
78 79
    const { getRouteMatcher } = require('next/dist/next-server/lib/router/utils/route-matcher');
      const { getRouteRegex } = require('next/dist/next-server/lib/router/utils/route-regex');
80 81 82 83 84 85 86 87 88 89
  `
    : ''

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

  const rewriteImports = `
90 91
    const { rewrites } = require('${routesManifest}')
    const { pathToRegexp, default: pathMatch } = require('next/dist/next-server/server/lib/path-match')
92 93 94 95
  `

  const handleRewrites = `
    const getCustomRouteMatcher = pathMatch(true)
96
    const {prepareDestination} = require('next/dist/next-server/server/router')
97 98 99 100 101 102 103

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

        if (params) {
104 105 106
          const { parsedDestination } = prepareDestination(
            rewrite.destination,
            params
107
          )
108 109
          Object.assign(parsedUrl.query, parsedDestination.query, params)
          delete parsedDestination.query
110

111
          Object.assign(parsedUrl, parsedDestination)
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133

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

      return parsedUrl
134
    }
135 136 137 138
  `

  if (page.match(API_ROUTE)) {
    return `
139 140
      import initServer from 'next-plugin-loader?middleware=on-init-server!'
      import onError from 'next-plugin-loader?middleware=on-error-server!'
141 142
      ${runtimeConfigImports}
      ${
143 144 145
        /*
          this needs to be called first so its available for any other imports
        */
146 147
        runtimeConfigSetter
      }
148 149 150 151 152 153 154
      ${dynamicRouteImports}
      const { parse } = require('url')
      const { apiResolver } = require('next/dist/next-server/server/api-utils')
      ${rewriteImports}

      ${dynamicRouteMatcher}
      ${handleRewrites}
155

156 157 158
      export default async (req, res) => {
        try {
          await initServer()
T
Tim Neutkens 已提交
159 160 161 162 163 164 165 166 167 168

          ${
            basePath
              ? `
          if(req.url.startsWith('${basePath}')) {
            req.url = req.url.replace('${basePath}', '')
          }
          `
              : ''
          }
169
          const parsedUrl = handleRewrites(parse(req.url, true))
170

171
          const params = ${
172 173
            pageIsDynamicRoute
              ? `dynamicRouteMatcher(parsedUrl.pathname)`
174 175
              : `{}`
          }
176

177
          const resolver = require('${absolutePagePath}')
178 179 180 181 182
          apiResolver(
            req,
            res,
            Object.assign({}, parsedUrl.query, params ),
            resolver,
J
Joe Haddad 已提交
183
            ${encodedPreviewProps},
184 185
            onError
          )
J
JJ Kasper 已提交
186
        } catch (err) {
187
          console.error(err)
J
JJ Kasper 已提交
188
          await onError(err)
189 190
          res.statusCode = 500
          res.end('Internal Server Error')
191 192 193 194 195
        }
      }
    `
  } else {
    return `
196 197
    import initServer from 'next-plugin-loader?middleware=on-init-server!'
    import onError from 'next-plugin-loader?middleware=on-error-server!'
198 199
    ${runtimeConfigImports}
    ${
200
      // this needs to be called first so its available for any other imports
201 202
      runtimeConfigSetter
    }
203 204
    const {parse} = require('url')
    const {parse: parseQs} = require('querystring')
205 206
    const {renderToHTML} = require('next/dist/next-server/server/render');
    const { tryGetPreviewData } = require('next/dist/next-server/server/api-utils');
207
    const {sendHTML} = require('next/dist/next-server/server/send-html');
208
    const {sendPayload} = require('next/dist/next-server/server/send-payload');
209 210 211 212 213 214 215
    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}
216 217

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

219
    const Component = ComponentInfo.default
J
JJ Kasper 已提交
220
    export default Component
221
    export const unstable_getStaticParams = ComponentInfo['unstable_getStaticParam' + 's']
222 223 224 225 226 227
    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']
228
    export const unstable_getStaticPaths = ComponentInfo['unstable_getStaticPath' + 's']
229
    export const unstable_getServerProps = ComponentInfo['unstable_getServerProp' + 's']
230

231 232 233
    ${dynamicRouteMatcher}
    ${handleRewrites}

234
    export const config = ComponentInfo['confi' + 'g'] || {}
J
JJ Kasper 已提交
235
    export const _app = App
236 237
    export async function renderReqToHTML(req, res, renderMode, _renderOpts, _params) {
      const fromExport = renderMode === 'export' || renderMode === true;
T
Tim Neutkens 已提交
238 239 240 241 242 243 244 245 246
      ${
        basePath
          ? `
      if(req.url.startsWith('${basePath}')) {
        req.url = req.url.replace('${basePath}', '')
      }
      `
          : ''
      }
T
Tim Neutkens 已提交
247 248 249 250
      const options = {
        App,
        Document,
        buildManifest,
251 252 253
        getStaticProps,
        getServerSideProps,
        getStaticPaths,
T
Tim Neutkens 已提交
254
        reactLoadableManifest,
255
        canonicalBase: "${canonicalBase}",
256
        buildId: "${buildId}",
J
JJ Kasper 已提交
257
        assetPrefix: "${assetPrefix}",
258
        runtimeConfig: runtimeConfig.publicRuntimeConfig || {},
J
Joe Haddad 已提交
259
        previewProps: ${encodedPreviewProps},
260
        ..._renderOpts
J
JJ Kasper 已提交
261
      }
262
      let _nextData = false
J
JJ Kasper 已提交
263

264 265 266
      const parsedUrl = handleRewrites(parse(req.url, true))

      if (parsedUrl.pathname.match(/_next\\/data/)) {
267
        _nextData = true
268
        parsedUrl.pathname = parsedUrl.pathname
J
JJ Kasper 已提交
269
          .replace(new RegExp('/_next/data/${escapedBuildId}/'), '/')
J
JJ Kasper 已提交
270
          .replace(/\\.json$/, '')
T
Tim Neutkens 已提交
271
      }
272

J
JJ Kasper 已提交
273 274 275
      const renderOpts = Object.assign(
        {
          Component,
276
          pageConfig: config,
277
          nextExport: fromExport
J
JJ Kasper 已提交
278 279 280
        },
        options,
      )
T
Tim Neutkens 已提交
281 282
      try {
        ${page === '/_error' ? `res.statusCode = 404` : ''}
J
Joe Haddad 已提交
283
        ${
284
          pageIsDynamicRoute
285
            ? `const params = fromExport && !getStaticProps && !getServerSideProps ? {} : dynamicRouteMatcher(parsedUrl.pathname) || {};`
J
Joe Haddad 已提交
286 287
            : `const params = {};`
        }
288
        ${
J
Joe Haddad 已提交
289
          // Temporary work around: `x-now-route-matches` is a platform header
290 291 292 293
          // _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).
294
          pageIsDynamicRoute
J
Joe Haddad 已提交
295
            ? `const nowParams = req.headers && req.headers["x-now-route-matches"]
296 297 298 299 300 301 302 303 304 305 306
              ? 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, {
307
                                [key]: obj[key]
308 309 310 311 312 313 314 315
                              }),
                            {}
                          );
                        }
                      },
                      groups
                    };
                  })()
J
Joe Haddad 已提交
316
                )(req.headers["x-now-route-matches"])
317 318
              : null;
          `
319 320
            : `const nowParams = null;`
        }
321 322 323 324
        // 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

325 326
        const isFallback = parsedUrl.query.__nextFallback

327 328 329
        const previewData = tryGetPreviewData(req, res, options.previewProps)
        const isPreviewMode = previewData !== false

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

332 333 334 335 336 337 338 339 340
        if (!renderMode) {
          if (_nextData || getStaticProps || getServerSideProps) {
            sendPayload(res, _nextData ? JSON.stringify(renderOpts.pageData) : result, _nextData ? 'json' : 'html', {
              private: isPreviewMode,
              stateful: !!getServerSideProps,
              revalidate: renderOpts.revalidate,
            })
            return null
          }
341 342 343 344 345
        } else if (isPreviewMode) {
          res.setHeader(
            'Cache-Control',
            'private, no-cache, no-store, max-age=0, must-revalidate'
          )
346
        }
J
JJ Kasper 已提交
347

348
        if (renderMode) return { html: result, renderOpts }
T
Tim Neutkens 已提交
349 350 351 352
        return result
      } catch (err) {
        if (err.code === 'ENOENT') {
          res.statusCode = 404
353
          const result = await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
354 355 356
            getStaticProps: undefined,
            getStaticPaths: undefined,
            getServerSideProps: undefined,
T
Tim Neutkens 已提交
357
            Component: Error
358
          }))
T
Tim Neutkens 已提交
359 360 361 362
          return result
        } else {
          console.error(err)
          res.statusCode = 500
363
          const result = await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
364 365 366
            getStaticProps: undefined,
            getStaticPaths: undefined,
            getServerSideProps: undefined,
T
Tim Neutkens 已提交
367 368
            Component: Error,
            err
369
          }))
T
Tim Neutkens 已提交
370 371 372 373
          return result
        }
      }
    }
374
    export async function render (req, res) {
T
Tim Neutkens 已提交
375
      try {
376
        await initServer()
377
        const html = await renderReqToHTML(req, res)
378 379 380
        if (html) {
          sendHTML(req, res, html, {generateEtags: ${generateEtags}})
        }
T
Tim Neutkens 已提交
381
      } catch(err) {
382
        await onError(err)
T
Tim Neutkens 已提交
383 384 385 386 387 388
        console.error(err)
        res.statusCode = 500
        res.end('Internal Server Error')
      }
    }
  `
389
  }
T
Tim Neutkens 已提交
390 391 392
}

export default nextServerlessLoader