Skip to content

Commit

Permalink
Optimize the web server size (#34242)
Browse files Browse the repository at this point in the history
Related to #34185, this PR reduces the size of chunk that contains web-server.ts from 1.14mb to 210.8kb, by splitting base-http and api-utils into different environments.

Only affected thing is we can't have SSG preview mode for the web runtime via `getStaticProps`.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`
  • Loading branch information
shuding committed Feb 11, 2022
1 parent a104cf6 commit 6bc7c4d
Show file tree
Hide file tree
Showing 17 changed files with 696 additions and 672 deletions.
Expand Up @@ -7,7 +7,10 @@ import { NextRequest } from '../../../../server/web/spec-extension/request'
import { toNodeHeaders } from '../../../../server/web/utils'

import WebServer from '../../../../server/web-server'
import { WebNextRequest, WebNextResponse } from '../../../../server/base-http'
import {
WebNextRequest,
WebNextResponse,
} from '../../../../server/base-http/web'

const createHeaders = (args?: any) => ({
...args,
Expand Down
@@ -1,9 +1,12 @@
import { parse as parseUrl } from 'url'
import { IncomingMessage, ServerResponse } from 'http'
import { apiResolver } from '../../../../server/api-utils'
import { apiResolver } from '../../../../server/api-utils/node'
import { getUtils, vercelHeader, ServerlessHandlerCtx } from './utils'
import { DecodeError } from '../../../../shared/lib/utils'
import { NodeNextResponse, NodeNextRequest } from '../../../../server/base-http'
import {
NodeNextResponse,
NodeNextRequest,
} from '../../../../server/base-http/node'

export function getApiHandler(ctx: ServerlessHandlerCtx) {
const { pageModule, encodedPreviewProps, pageIsDynamic } = ctx
Expand Down
Expand Up @@ -5,7 +5,7 @@ import { sendRenderResult } from '../../../../server/send-payload'
import { getUtils, vercelHeader, ServerlessHandlerCtx } from './utils'

import { renderToHTML } from '../../../../server/render'
import { tryGetPreviewData } from '../../../../server/api-utils'
import { tryGetPreviewData } from '../../../../server/api-utils/node'
import { denormalizePagePath } from '../../../../server/denormalize-page-path'
import { setLazyProp, getCookieParser } from '../../../../server/api-utils'
import { getRedirectStatus } from '../../../../lib/load-custom-routes'
Expand Down
Expand Up @@ -7,6 +7,7 @@ import type {
GetStaticPaths,
GetStaticProps,
} from '../../../../types'
import type { BaseNextRequest } from '../../../../server/base-http'

import { format as formatUrl, UrlWithParsedQuery, parse as parseUrl } from 'url'
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
Expand All @@ -26,7 +27,6 @@ import { denormalizePagePath } from '../../../../server/denormalize-page-path'
import cookie from 'next/dist/compiled/cookie'
import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants'
import { addRequestMeta } from '../../../../server/request-meta'
import { BaseNextRequest } from '../../../../server/base-http'

const getCustomRouteMatcher = pathMatch(true)

Expand Down
191 changes: 191 additions & 0 deletions packages/next/server/api-utils/index.ts
@@ -0,0 +1,191 @@
import type { IncomingMessage } from 'http'
import type { BaseNextRequest } from '../base-http'

import { NextApiRequest, NextApiResponse } from '../../shared/lib/utils'

export type NextApiRequestCookies = { [key: string]: string }
export type NextApiRequestQuery = { [key: string]: string | string[] }

export type __ApiPreviewProps = {
previewModeId: string
previewModeEncryptionKey: string
previewModeSigningKey: string
}

/**
* Parse cookies from the `headers` of request
* @param req request object
*/
export function getCookieParser(headers: {
[key: string]: undefined | string | string[]
}): () => NextApiRequestCookies {
return function parseCookie(): NextApiRequestCookies {
const header: undefined | string | string[] = headers.cookie

if (!header) {
return {}
}

const { parse: parseCookieFn } = require('next/dist/compiled/cookie')
return parseCookieFn(Array.isArray(header) ? header.join(';') : header)
}
}

/**
*
* @param res response object
* @param statusCode `HTTP` status code of response
*/
export function sendStatusCode(
res: NextApiResponse,
statusCode: number
): NextApiResponse<any> {
res.statusCode = statusCode
return res
}

/**
*
* @param res response object
* @param [statusOrUrl] `HTTP` status code of redirect
* @param url URL of redirect
*/
export function redirect(
res: NextApiResponse,
statusOrUrl: string | number,
url?: string
): NextApiResponse<any> {
if (typeof statusOrUrl === 'string') {
url = statusOrUrl
statusOrUrl = 307
}
if (typeof statusOrUrl !== 'number' || typeof url !== 'string') {
throw new Error(
`Invalid redirect arguments. Please use a single argument URL, e.g. res.redirect('/destination') or use a status code and URL, e.g. res.redirect(307, '/destination').`
)
}
res.writeHead(statusOrUrl, { Location: url })
res.write(url)
res.end()
return res
}

export const PRERENDER_REVALIDATE_HEADER = 'x-prerender-revalidate'

export function checkIsManualRevalidate(
req: IncomingMessage | BaseNextRequest,
previewProps: __ApiPreviewProps
): boolean {
return req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId
}

export const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass`
export const COOKIE_NAME_PRERENDER_DATA = `__next_preview_data`

export const SYMBOL_PREVIEW_DATA = Symbol(COOKIE_NAME_PRERENDER_DATA)
export const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS)

export function clearPreviewData<T>(
res: NextApiResponse<T>
): NextApiResponse<T> {
if (SYMBOL_CLEARED_COOKIES in res) {
return res
}

const { serialize } =
require('next/dist/compiled/cookie') as typeof import('cookie')
const previous = res.getHeader('Set-Cookie')
res.setHeader(`Set-Cookie`, [
...(typeof previous === 'string'
? [previous]
: Array.isArray(previous)
? previous
: []),
serialize(COOKIE_NAME_PRERENDER_BYPASS, '', {
// To delete a cookie, set `expires` to a date in the past:
// https://tools.ietf.org/html/rfc6265#section-4.1.1
// `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted.
expires: new Date(0),
httpOnly: true,
sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
secure: process.env.NODE_ENV !== 'development',
path: '/',
}),
serialize(COOKIE_NAME_PRERENDER_DATA, '', {
// To delete a cookie, set `expires` to a date in the past:
// https://tools.ietf.org/html/rfc6265#section-4.1.1
// `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted.
expires: new Date(0),
httpOnly: true,
sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
secure: process.env.NODE_ENV !== 'development',
path: '/',
}),
])

Object.defineProperty(res, SYMBOL_CLEARED_COOKIES, {
value: true,
enumerable: false,
})
return res
}

/**
* Custom error class
*/
export class ApiError extends Error {
readonly statusCode: number

constructor(statusCode: number, message: string) {
super(message)
this.statusCode = statusCode
}
}

/**
* Sends error in `response`
* @param res response object
* @param statusCode of response
* @param message of response
*/
export function sendError(
res: NextApiResponse,
statusCode: number,
message: string
): void {
res.statusCode = statusCode
res.statusMessage = message
res.end(message)
}

interface LazyProps {
req: NextApiRequest
}

/**
* Execute getter function only if its needed
* @param LazyProps `req` and `params` for lazyProp
* @param prop name of property
* @param getter function to get data
*/
export function setLazyProp<T>(
{ req }: LazyProps,
prop: string,
getter: () => T
): void {
const opts = { configurable: true, enumerable: true }
const optsReset = { ...opts, writable: true }

Object.defineProperty(req, prop, {
...opts,
get: () => {
const value = getter()
// we set the property on the object to avoid recalculating it
Object.defineProperty(req, prop, { ...optsReset, value })
return value
},
set: (value) => {
Object.defineProperty(req, prop, { ...optsReset, value })
},
})
}

0 comments on commit 6bc7c4d

Please sign in to comment.