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 authored Feb 11, 2022
1 parent a104cf6 commit 6bc7c4d
Show file tree
Hide file tree
Showing 17 changed files with 696 additions and 672 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 })
},
})
}
Loading

0 comments on commit 6bc7c4d

Please sign in to comment.