utils.ts 7.2 KB
Newer Older
1
import { format, UrlObject, URLFormatOptions } from 'url'
L
Lukáš Huvar 已提交
2
import { ServerResponse, IncomingMessage } from 'http'
3 4
import { ComponentType } from 'react'
import { ParsedUrlQuery } from 'querystring'
5
import { ManifestItem } from '../server/render'
6
import { NextRouter } from './router/router'
G
Gerald Monaco 已提交
7
import { DocumentContext as DocumentComponentContext } from './document-context'
8 9 10 11

/**
 * Types used by both next and next-server
 */
L
Lukáš Huvar 已提交
12 13 14 15 16
export type NextComponentType<
  C extends BaseContext = NextPageContext,
  IP = {},
  P = {}
> = ComponentType<P> & {
17
  getInitialProps?(context: C): IP | Promise<IP>
18 19
}

L
Lukáš Huvar 已提交
20 21 22 23
export type DocumentType = NextComponentType<
  DocumentContext,
  DocumentInitialProps,
  DocumentProps
G
Gerald Monaco 已提交
24 25 26 27 28 29
> & {
  renderDocument(
    Document: DocumentType,
    props: DocumentProps
  ): React.ReactElement
}
30

L
Lukáš Huvar 已提交
31 32 33 34 35
export type AppType = NextComponentType<
  AppContextType,
  AppInitialProps,
  AppPropsType
>
36

37 38 39 40
export type AppTreeType = ComponentType<
  AppInitialProps & { [name: string]: any }
>

41 42 43
export type Enhancer<C> = (Component: C) => C

export type ComponentsEnhancer =
L
Lukáš Huvar 已提交
44 45
  | {
      enhanceApp?: Enhancer<AppType>
46
      enhanceComponent?: Enhancer<NextComponentType>
L
Lukáš Huvar 已提交
47
    }
48 49
  | Enhancer<NextComponentType>

L
Lukáš Huvar 已提交
50 51 52
export type RenderPageResult = {
  html: string
  head?: Array<JSX.Element | null>
53
  dataOnly?: true
L
Lukáš Huvar 已提交
54
}
55

L
Lukáš Huvar 已提交
56
export type RenderPage = (
57
  options?: ComponentsEnhancer
L
Lukáš Huvar 已提交
58
) => RenderPageResult | Promise<RenderPageResult>
59

60
export type BaseContext = {
61
  res?: ServerResponse
62
  [k: string]: any
63 64
}

65
export type NEXT_DATA = {
66 67 68 69 70 71 72 73
  dataManager: string
  props: any
  page: string
  query: ParsedUrlQuery
  buildId: string
  assetPrefix?: string
  runtimeConfig?: { [key: string]: any }
  nextExport?: boolean
74
  autoExport?: boolean
75
  skeleton?: boolean
76
  dynamicIds?: string[]
77
  err?: Error & { statusCode?: number }
78 79
}

L
Lukáš Huvar 已提交
80 81 82
/**
 * `Next` context
 */
83 84
// tslint:disable-next-line interface-name
export interface NextPageContext {
L
Lukáš Huvar 已提交
85 86 87
  /**
   * Error object if encountered during rendering
   */
88
  err?: Error & { statusCode?: number } | null
L
Lukáš Huvar 已提交
89 90 91
  /**
   * `HTTP` request object.
   */
92
  req?: IncomingMessage
L
Lukáš Huvar 已提交
93 94 95
  /**
   * `HTTP` response object.
   */
96
  res?: ServerResponse
L
Lukáš Huvar 已提交
97 98 99
  /**
   * Path section of `URL`.
   */
100
  pathname: string
L
Lukáš Huvar 已提交
101 102 103
  /**
   * Query string section of `URL` parsed as an object.
   */
104
  query: ParsedUrlQuery
L
Lukáš Huvar 已提交
105 106 107
  /**
   * `String` of the actual path including query.
   */
108
  asPath?: string
109 110 111
  /**
   * `Component` the tree of the App to use if needing to render separately
   */
112
  AppTree: AppTreeType
113 114
}

115
export type AppContextType<R extends NextRouter = NextRouter> = {
116
  Component: NextComponentType<NextPageContext>
117
  AppTree: AppTreeType
118
  ctx: NextPageContext
119
  router: R
120 121
}

122
export type AppInitialProps = {
123
  pageProps: any
124 125
}

L
Lukáš Huvar 已提交
126
export type AppPropsType<
127
  R extends NextRouter = NextRouter,
L
Lukáš Huvar 已提交
128 129
  P = {}
> = AppInitialProps & {
130
  Component: NextComponentType<NextPageContext, any, P>
131
  router: R
132 133
}

134
export type DocumentContext = NextPageContext & {
135
  renderPage: RenderPage
136 137
}

138
export type DocumentInitialProps = RenderPageResult & {
139
  styles?: React.ReactElement[] | React.ReactFragment
140 141
}

142
export type DocumentProps = DocumentInitialProps & {
143
  __NEXT_DATA__: NEXT_DATA
144 145
  dangerousAsPath: string
  ampPath: string
146 147
  inAmpMode: boolean
  hybridAmp: boolean
148
  staticMarkup: boolean
J
Joe Haddad 已提交
149 150
  isDevelopment: boolean
  hasCssMode: boolean
151 152
  devFiles: string[]
  files: string[]
153
  polyfillFiles: string[]
154
  dynamicImports: ManifestItem[]
155 156
  assetPrefix?: string
  canonicalBase: string
157 158 159
  htmlProps: any
  bodyTags: any[]
  headTags: any[]
160 161 162
}

/**
L
Lukáš Huvar 已提交
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
 * Next `API` route request
 */
export type NextApiRequest = IncomingMessage & {
  /**
   * Object of `query` values from url
   */
  query: {
    [key: string]: string | string[]
  }
  /**
   * Object of `cookies` from header
   */
  cookies: {
    [key: string]: string
  }

  body: any
}

/**
 * Send body of response
184
 */
185
type Send<T> = (body: T) => void
186

L
Lukáš Huvar 已提交
187 188 189
/**
 * Next `API` route response
 */
190
export type NextApiResponse<T = any> = ServerResponse & {
L
Lukáš Huvar 已提交
191
  /**
192
   * Send data `any` data in response
L
Lukáš Huvar 已提交
193
   */
194
  send: Send<T>
L
Lukáš Huvar 已提交
195
  /**
196
   * Send data `json` data in response
L
Lukáš Huvar 已提交
197
   */
198 199
  json: Send<T>
  status: (statusCode: number) => NextApiResponse<T>
L
Lukáš Huvar 已提交
200 201 202 203 204
}

/**
 * Utils
 */
205
export function execOnce(this: any, fn: (...args: any) => any) {
206
  let used = false
207 208
  let result: any = null

209 210 211
  return (...args: any) => {
    if (!used) {
      used = true
212
      result = fn.apply(this, args)
213
    }
214
    return result
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
  }
}

export function getLocationOrigin() {
  const { protocol, hostname, port } = window.location
  return `${protocol}//${hostname}${port ? ':' + port : ''}`
}

export function getURL() {
  const { href } = window.location
  const origin = getLocationOrigin()
  return href.substring(origin.length)
}

export function getDisplayName(Component: ComponentType<any>) {
L
Lukáš Huvar 已提交
230 231 232
  return typeof Component === 'string'
    ? Component
    : Component.displayName || Component.name || 'Unknown'
233 234 235 236 237 238
}

export function isResSent(res: ServerResponse) {
  return res.finished || res.headersSent
}

L
Lukáš Huvar 已提交
239 240 241 242
export async function loadGetInitialProps<
  C extends BaseContext,
  IP = {},
  P = {}
243
>(App: NextComponentType<C, IP, P>, ctx: C): Promise<IP> {
244
  if (process.env.NODE_ENV !== 'production') {
245
    if (App.prototype && App.prototype.getInitialProps) {
L
Lukáš Huvar 已提交
246
      const message = `"${getDisplayName(
247
        App
L
Lukáš Huvar 已提交
248
      )}.getInitialProps()" is defined as an instance method - visit https://err.sh/zeit/next.js/get-initial-props-as-an-instance-method for more information.`
249 250 251
      throw new Error(message)
    }
  }
252 253
  // when called from _app `ctx` is nested in `ctx`
  const res = ctx.res || (ctx.ctx && ctx.ctx.res)
254

255 256 257 258 259 260 261
  if (!App.getInitialProps) {
    if (ctx.ctx && ctx.Component) {
      // @ts-ignore pageProps default
      return {
        pageProps: await loadGetInitialProps(ctx.Component, ctx.ctx),
      }
    }
262
    return {} as any
263
  }
264

265
  const props = await App.getInitialProps(ctx)
266

267
  if (res && isResSent(res)) {
268 269 270 271
    return props
  }

  if (!props) {
L
Lukáš Huvar 已提交
272
    const message = `"${getDisplayName(
273
      App
L
Lukáš Huvar 已提交
274
    )}.getInitialProps()" should resolve to an object. But found "${props}" instead.`
275 276 277
    throw new Error(message)
  }

278 279 280 281
  if (process.env.NODE_ENV !== 'production') {
    if (Object.keys(props).length === 0 && !ctx.ctx) {
      console.warn(
        `${getDisplayName(
282
          App
283
        )} returned an empty object from \`getInitialProps\`. This de-optimizes and prevents automatic static optimization. https://err.sh/zeit/next.js/empty-object-getInitialProps`
284 285 286 287
      )
    }
  }

288 289 290
  return props
}

L
Lukáš Huvar 已提交
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
export const urlObjectKeys = [
  'auth',
  'hash',
  'host',
  'hostname',
  'href',
  'path',
  'pathname',
  'port',
  'protocol',
  'query',
  'search',
  'slashes',
]

export function formatWithValidation(
  url: UrlObject,
308
  options?: URLFormatOptions
L
Lukáš Huvar 已提交
309
) {
310 311
  if (process.env.NODE_ENV === 'development') {
    if (url !== null && typeof url === 'object') {
312
      Object.keys(url).forEach(key => {
313
        if (urlObjectKeys.indexOf(key) === -1) {
L
Lukáš Huvar 已提交
314
          console.warn(
315
            `Unknown key passed via urlObject into url.format: ${key}`
L
Lukáš Huvar 已提交
316
          )
317 318 319 320 321 322 323
        }
      })
    }
  }

  return format(url as any, options)
}
324 325 326 327 328 329

export const SUPPORTS_PERFORMANCE = typeof performance !== 'undefined'
export const SUPPORTS_PERFORMANCE_USER_TIMING =
  SUPPORTS_PERFORMANCE &&
  typeof performance.mark === 'function' &&
  typeof performance.measure === 'function'