next-serverless-loader.ts 11.7 KB
Newer Older
1
import { loader } from 'webpack'
2 3
import { join } from 'path'
import { parse } from 'querystring'
4 5
import {
  BUILD_MANIFEST,
6
  ROUTES_MANIFEST,
7 8 9
  REACT_LOADABLE_MANIFEST,
} from '../../../next-server/lib/constants'
import { isDynamicRoute } from '../../../next-server/lib/router/utils'
10
import { API_ROUTE } from '../../../lib/constants'
11
import escapeRegexp from 'escape-string-regexp'
T
Tim Neutkens 已提交
12 13

export type ServerlessLoaderQuery = {
14 15 16 17 18 19
  page: string
  distDir: string
  absolutePagePath: string
  absoluteAppPath: string
  absoluteDocumentPath: string
  absoluteErrorPath: string
20
  buildId: string
21
  assetPrefix: string
T
Tim Neutkens 已提交
22
  generateEtags: string
23
  canonicalBase: string
T
Tim Neutkens 已提交
24
  basePath: string
25
  runtimeConfig: string
T
Tim Neutkens 已提交
26 27
}

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

T
Tim Neutkens 已提交
45
  const buildManifest = join(distDir, BUILD_MANIFEST).replace(/\\/g, '/')
46 47 48 49
  const reactLoadableManifest = join(distDir, REACT_LOADABLE_MANIFEST).replace(
    /\\/g,
    '/'
  )
50 51
  const routesManifest = join(distDir, ROUTES_MANIFEST).replace(/\\/g, '/')

52
  const escapedBuildId = escapeRegexp(buildId)
53
  const pageIsDynamicRoute = isDynamicRoute(page)
54

55 56
  const runtimeConfigImports = runtimeConfig
    ? `
57
      const { setConfig } = require('next/dist/next-server/lib/runtime-config')
58 59 60 61 62 63 64 65 66 67
    `
    : ''

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

68 69
  const dynamicRouteImports = pageIsDynamicRoute
    ? `
70 71
    const { getRouteMatcher } = require('next/dist/next-server/lib/router/utils/route-matcher');
      const { getRouteRegex } = require('next/dist/next-server/lib/router/utils/route-regex');
72 73 74 75 76 77 78 79 80 81
  `
    : ''

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

  const rewriteImports = `
82 83
    const { rewrites } = require('${routesManifest}')
    const { pathToRegexp, default: pathMatch } = require('next/dist/next-server/server/lib/path-match')
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
  `

  const handleRewrites = `
    const getCustomRouteMatcher = pathMatch(true)

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

        if (params) {
          parsedUrl.query = {
            ...parsedUrl.query,
            ...params
          }
          const parsedDest = parse(rewrite.destination)
          const destCompiler = pathToRegexp.compile(
            \`\${parsedDest.pathname}\${parsedDest.hash || ''}\`
          )
          const newUrl = destCompiler(params)
          const parsedNewUrl = parse(newUrl)

          parsedUrl.pathname = parsedNewUrl.pathname
          parsedUrl.hash = parsedNewUrl.hash

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

      return parsedUrl
130
    }
131 132 133 134
  `

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

      ${dynamicRouteMatcher}
      ${handleRewrites}
151

152 153 154
      export default async (req, res) => {
        try {
          await initServer()
T
Tim Neutkens 已提交
155 156 157 158 159 160 161 162 163 164

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

167
          const params = ${
168 169
            pageIsDynamicRoute
              ? `dynamicRouteMatcher(parsedUrl.pathname)`
170 171
              : `{}`
          }
172

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

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

212
    const Component = ComponentInfo.default
J
JJ Kasper 已提交
213
    export default Component
J
JJ Kasper 已提交
214
    export const unstable_getStaticProps = ComponentInfo['unstable_getStaticProp' + 's']
215 216
    export const unstable_getStaticParams = ComponentInfo['unstable_getStaticParam' + 's']
    export const unstable_getStaticPaths = ComponentInfo['unstable_getStaticPath' + 's']
217
    export const unstable_getServerProps = ComponentInfo['unstable_getServerProp' + 's']
218

219 220 221
    ${dynamicRouteMatcher}
    ${handleRewrites}

222
    export const config = ComponentInfo['confi' + 'g'] || {}
J
JJ Kasper 已提交
223
    export const _app = App
224
    export async function renderReqToHTML(req, res, fromExport, _renderOpts, _params) {
T
Tim Neutkens 已提交
225 226 227 228 229 230 231 232 233
      ${
        basePath
          ? `
      if(req.url.startsWith('${basePath}')) {
        req.url = req.url.replace('${basePath}', '')
      }
      `
          : ''
      }
T
Tim Neutkens 已提交
234 235 236 237
      const options = {
        App,
        Document,
        buildManifest,
J
JJ Kasper 已提交
238
        unstable_getStaticProps,
239
        unstable_getServerProps,
240
        unstable_getStaticPaths,
T
Tim Neutkens 已提交
241
        reactLoadableManifest,
242
        canonicalBase: "${canonicalBase}",
243
        buildId: "${buildId}",
J
JJ Kasper 已提交
244
        assetPrefix: "${assetPrefix}",
245
        runtimeConfig: runtimeConfig.publicRuntimeConfig || {},
246
        ..._renderOpts
J
JJ Kasper 已提交
247
      }
248
      let _nextData = false
J
JJ Kasper 已提交
249

250 251 252
      const parsedUrl = handleRewrites(parse(req.url, true))

      if (parsedUrl.pathname.match(/_next\\/data/)) {
253
        _nextData = true
254
        parsedUrl.pathname = parsedUrl.pathname
J
JJ Kasper 已提交
255
          .replace(new RegExp('/_next/data/${escapedBuildId}/'), '/')
J
JJ Kasper 已提交
256
          .replace(/\\.json$/, '')
T
Tim Neutkens 已提交
257
      }
258

J
JJ Kasper 已提交
259 260 261
      const renderOpts = Object.assign(
        {
          Component,
262
          pageConfig: config,
263
          nextExport: fromExport
J
JJ Kasper 已提交
264 265 266
        },
        options,
      )
T
Tim Neutkens 已提交
267 268
      try {
        ${page === '/_error' ? `res.statusCode = 404` : ''}
J
Joe Haddad 已提交
269
        ${
270
          pageIsDynamicRoute
271
            ? `const params = fromExport && !unstable_getStaticProps && !unstable_getServerProps ? {} : dynamicRouteMatcher(parsedUrl.pathname) || {};`
J
Joe Haddad 已提交
272 273
            : `const params = {};`
        }
274
        ${
J
Joe Haddad 已提交
275
          // Temporary work around: `x-now-route-matches` is a platform header
276 277 278 279
          // _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).
280
          pageIsDynamicRoute
J
Joe Haddad 已提交
281
            ? `const nowParams = req.headers && req.headers["x-now-route-matches"]
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
              ? 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, {
                                [key]: encodeURIComponent(obj[key])
                              }),
                            {}
                          );
                        }
                      },
                      groups
                    };
                  })()
J
Joe Haddad 已提交
302
                )(req.headers["x-now-route-matches"])
303 304
              : null;
          `
305 306
            : `const nowParams = null;`
        }
307 308 309 310
        // 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

311 312 313
        const isFallback = parsedUrl.query.__nextFallback

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

315 316
        if (_nextData && !fromExport) {
          const payload = JSON.stringify(renderOpts.pageData)
317 318
          res.setHeader('Content-Type', 'application/json')
          res.setHeader('Content-Length', Buffer.byteLength(payload))
319

320 321
          res.setHeader(
            'Cache-Control',
322 323 324
            unstable_getServerProps
              ? \`no-cache, no-store, must-revalidate\`
              : \`s-maxage=\${renderOpts.revalidate}, stale-while-revalidate\`
325 326 327 328
          )
          res.end(payload)
          return null
        }
J
JJ Kasper 已提交
329 330

        if (fromExport) return { html: result, renderOpts }
T
Tim Neutkens 已提交
331 332 333 334
        return result
      } catch (err) {
        if (err.code === 'ENOENT') {
          res.statusCode = 404
335
          const result = await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
336 337
            unstable_getStaticProps: undefined,
            unstable_getStaticPaths: undefined,
338
            unstable_getServerProps: undefined,
T
Tim Neutkens 已提交
339
            Component: Error
340
          }))
T
Tim Neutkens 已提交
341 342 343 344
          return result
        } else {
          console.error(err)
          res.statusCode = 500
345
          const result = await renderToHTML(req, res, "/_error", parsedUrl.query, Object.assign({}, options, {
346 347
            unstable_getStaticProps: undefined,
            unstable_getStaticPaths: undefined,
348
            unstable_getServerProps: undefined,
T
Tim Neutkens 已提交
349 350
            Component: Error,
            err
351
          }))
T
Tim Neutkens 已提交
352 353 354 355
          return result
        }
      }
    }
356
    export async function render (req, res) {
T
Tim Neutkens 已提交
357
      try {
358
        await initServer()
359
        const html = await renderReqToHTML(req, res)
360 361 362
        if (html) {
          sendHTML(req, res, html, {generateEtags: ${generateEtags}})
        }
T
Tim Neutkens 已提交
363
      } catch(err) {
364
        await onError(err)
T
Tim Neutkens 已提交
365 366 367 368 369 370
        console.error(err)
        res.statusCode = 500
        res.end('Internal Server Error')
      }
    }
  `
371
  }
T
Tim Neutkens 已提交
372 373 374
}

export default nextServerlessLoader