Port next.js to TypeScript (#51438)
This commit is contained in:
@@ -50,8 +50,8 @@ import productExamples from './context/product-examples'
|
|||||||
import productGroups from './context/product-groups'
|
import productGroups from './context/product-groups'
|
||||||
import featuredLinks from '@/landings/middleware/featured-links'
|
import featuredLinks from '@/landings/middleware/featured-links'
|
||||||
import learningTrack from '@/learning-track/middleware/learning-track'
|
import learningTrack from '@/learning-track/middleware/learning-track'
|
||||||
import next from './next.js'
|
import next from './next'
|
||||||
import renderPage from './render-page.js'
|
import renderPage from './render-page'
|
||||||
import assetPreprocessing from '@/assets/middleware/asset-preprocessing'
|
import assetPreprocessing from '@/assets/middleware/asset-preprocessing'
|
||||||
import archivedAssetRedirects from '@/archives/middleware/archived-asset-redirects'
|
import archivedAssetRedirects from '@/archives/middleware/archived-asset-redirects'
|
||||||
import favicons from './favicons'
|
import favicons from './favicons'
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import next from 'next'
|
import next from 'next'
|
||||||
|
|
||||||
|
import type { Response, NextFunction } from 'express'
|
||||||
|
|
||||||
|
import type { ExtendedRequest } from '@/types'
|
||||||
|
|
||||||
const { NODE_ENV } = process.env
|
const { NODE_ENV } = process.env
|
||||||
const isDevelopment = NODE_ENV === 'development'
|
const isDevelopment = NODE_ENV === 'development'
|
||||||
|
|
||||||
@@ -7,7 +11,7 @@ export const nextApp = next({ dev: isDevelopment })
|
|||||||
export const nextHandleRequest = nextApp.getRequestHandler()
|
export const nextHandleRequest = nextApp.getRequestHandler()
|
||||||
await nextApp.prepare()
|
await nextApp.prepare()
|
||||||
|
|
||||||
function renderPageWithNext(req, res, next) {
|
function renderPageWithNext(req: ExtendedRequest, res: Response, next: NextFunction) {
|
||||||
if (req.path.startsWith('/_next') && !req.path.startsWith('/_next/data')) {
|
if (req.path.startsWith('/_next') && !req.path.startsWith('/_next/data')) {
|
||||||
return nextHandleRequest(req, res)
|
return nextHandleRequest(req, res)
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import http from 'http'
|
import http from 'http'
|
||||||
|
|
||||||
import { get } from 'lodash-es'
|
import { get } from 'lodash-es'
|
||||||
|
import type { Response } from 'express'
|
||||||
|
import type { Failbot } from '@github/failbot'
|
||||||
|
|
||||||
import FailBot from '#src/observability/lib/failbot.js'
|
import type { ExtendedRequest } from '@/types'
|
||||||
import patterns from '#src/frame/lib/patterns.js'
|
import FailBot from '@/observability/lib/failbot.js'
|
||||||
import getMiniTocItems from '#src/frame/lib/get-mini-toc-items.js'
|
import patterns from '@/frame/lib/patterns.js'
|
||||||
import { pathLanguagePrefixed } from '#src/languages/lib/languages.js'
|
import getMiniTocItems from '@/frame/lib/get-mini-toc-items.js'
|
||||||
import statsd from '#src/observability/lib/statsd.js'
|
import { pathLanguagePrefixed } from '@/languages/lib/languages.js'
|
||||||
import { allVersions } from '#src/versions/lib/all-versions.js'
|
import statsd from '@/observability/lib/statsd.js'
|
||||||
|
import { allVersions } from '@/versions/lib/all-versions.js'
|
||||||
import { isConnectionDropped } from './halt-on-dropped-connection'
|
import { isConnectionDropped } from './halt-on-dropped-connection'
|
||||||
import { nextHandleRequest } from './next.js'
|
import { nextHandleRequest } from './next.js'
|
||||||
import { defaultCacheControl } from './cache-control.js'
|
import { defaultCacheControl } from './cache-control.js'
|
||||||
@@ -16,37 +19,41 @@ import { minimumNotFoundHtml } from '../lib/constants.js'
|
|||||||
const STATSD_KEY_RENDER = 'middleware.render_page'
|
const STATSD_KEY_RENDER = 'middleware.render_page'
|
||||||
const STATSD_KEY_404 = 'middleware.render_404'
|
const STATSD_KEY_404 = 'middleware.render_404'
|
||||||
|
|
||||||
async function buildRenderedPage(req) {
|
async function buildRenderedPage(req: ExtendedRequest): Promise<string> {
|
||||||
const { context } = req
|
const { context } = req
|
||||||
|
if (!context) throw new Error('request not contextualized')
|
||||||
const { page } = context
|
const { page } = context
|
||||||
|
if (!page) throw new Error('page not set in context')
|
||||||
const path = req.pagePath || req.path
|
const path = req.pagePath || req.path
|
||||||
|
|
||||||
const pageRenderTimed = statsd.asyncTimer(page.render, STATSD_KEY_RENDER, [`path:${path}`])
|
const pageRenderTimed = statsd.asyncTimer(page.render, STATSD_KEY_RENDER, [`path:${path}`])
|
||||||
|
|
||||||
return await pageRenderTimed(context)
|
return (await pageRenderTimed(context)) as string
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildMiniTocItems(req) {
|
async function buildMiniTocItems(req: ExtendedRequest): Promise<string | undefined> {
|
||||||
const { context } = req
|
const { context } = req
|
||||||
|
if (!context) throw new Error('request not contextualized')
|
||||||
const { page } = context
|
const { page } = context
|
||||||
|
|
||||||
// get mini TOC items on articles
|
// get mini TOC items on articles
|
||||||
if (!page.showMiniToc) {
|
if (!page || !page.showMiniToc) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return getMiniTocItems(context.renderedPage, '')
|
return getMiniTocItems(context.renderedPage, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function renderPage(req, res) {
|
export default async function renderPage(req: ExtendedRequest, res: Response) {
|
||||||
const { context } = req
|
const { context } = req
|
||||||
|
|
||||||
// This is a contextualizing the request so that when this `req` is
|
// This is a contextualizing the request so that when this `req` is
|
||||||
// ultimately passed into the `Error.getInitialProps` function,
|
// ultimately passed into the `Error.getInitialProps` function,
|
||||||
// which NextJS executes at runtime on errors, so that we can
|
// which NextJS executes at runtime on errors, so that we can
|
||||||
// from there send the error to Failbot.
|
// from there send the error to Failbot.
|
||||||
req.FailBot = FailBot
|
req.FailBot = FailBot as Failbot
|
||||||
|
|
||||||
|
if (!context) throw new Error('request not contextualized')
|
||||||
const { page } = context
|
const { page } = context
|
||||||
const path = req.pagePath || req.path
|
const path = req.pagePath || req.path
|
||||||
|
|
||||||
@@ -95,7 +102,7 @@ export default async function renderPage(req, res) {
|
|||||||
// src/pages/404.tsx) but control the status code (and the Cache-Control).
|
// src/pages/404.tsx) but control the status code (and the Cache-Control).
|
||||||
//
|
//
|
||||||
// Create a new request for a real one.
|
// Create a new request for a real one.
|
||||||
const tempReq = new http.IncomingMessage(req)
|
const tempReq = new http.IncomingMessage(req as any) as ExtendedRequest
|
||||||
tempReq.method = 'GET'
|
tempReq.method = 'GET'
|
||||||
// There is a `src/pages/_notfound.txt`. That's why this will render
|
// There is a `src/pages/_notfound.txt`. That's why this will render
|
||||||
// a working and valid React component.
|
// a working and valid React component.
|
||||||
@@ -129,6 +136,7 @@ export default async function renderPage(req, res) {
|
|||||||
// Stop processing if the connection was already dropped
|
// Stop processing if the connection was already dropped
|
||||||
if (isConnectionDropped(req, res)) return
|
if (isConnectionDropped(req, res)) return
|
||||||
|
|
||||||
|
if (!req.context) throw new Error('request not contextualized')
|
||||||
req.context.renderedPage = await buildRenderedPage(req)
|
req.context.renderedPage = await buildRenderedPage(req)
|
||||||
req.context.miniTocItems = await buildMiniTocItems(req)
|
req.context.miniTocItems = await buildMiniTocItems(req)
|
||||||
|
|
||||||
@@ -142,11 +150,11 @@ export default async function renderPage(req, res) {
|
|||||||
if (!patterns.homepagePath.test(path)) {
|
if (!patterns.homepagePath.test(path)) {
|
||||||
if (
|
if (
|
||||||
req.context.currentVersion === 'free-pro-team@latest' ||
|
req.context.currentVersion === 'free-pro-team@latest' ||
|
||||||
!allVersions[req.context.currentVersion]
|
!allVersions[req.context.currentVersion!]
|
||||||
) {
|
) {
|
||||||
page.fullTitle += ' - ' + context.site.data.ui.header.github_docs
|
page.fullTitle += ' - ' + context.site!.data.ui.header.github_docs
|
||||||
} else {
|
} else {
|
||||||
const { versionTitle } = allVersions[req.context.currentVersion]
|
const { versionTitle } = allVersions[req.context.currentVersion!]
|
||||||
page.fullTitle += ' - '
|
page.fullTitle += ' - '
|
||||||
// Some plans don't have the word "GitHub" in them.
|
// Some plans don't have the word "GitHub" in them.
|
||||||
// E.g. "Enterprise Server 3.5"
|
// E.g. "Enterprise Server 3.5"
|
||||||
@@ -163,9 +171,15 @@ export default async function renderPage(req, res) {
|
|||||||
|
|
||||||
// `?json` query param for debugging request context
|
// `?json` query param for debugging request context
|
||||||
if (isRequestingJsonForDebugging) {
|
if (isRequestingJsonForDebugging) {
|
||||||
if (req.query.json.length > 1) {
|
const json = req.query.json
|
||||||
|
if (Array.isArray(json)) {
|
||||||
|
// e.g. ?json=page.permalinks&json=currentPath
|
||||||
|
throw new Error("'json' query string can only be 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json) {
|
||||||
// deep reference: ?json=page.permalinks
|
// deep reference: ?json=page.permalinks
|
||||||
return res.json(get(context, req.query.json))
|
return res.json(get(context, req.query.json as string))
|
||||||
} else {
|
} else {
|
||||||
// dump all the keys: ?json
|
// dump all the keys: ?json
|
||||||
return res.json({
|
return res.json({
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test, vi } from 'vitest'
|
import { describe, expect, test, vi } from 'vitest'
|
||||||
|
|
||||||
import { get } from '#src/tests/helpers/e2etest.js'
|
import { get } from '@/tests/helpers/e2etest.js'
|
||||||
|
|
||||||
describe('bad requests', () => {
|
describe('bad requests', () => {
|
||||||
vi.setConfig({ testTimeout: 60 * 1000 })
|
vi.setConfig({ testTimeout: 60 * 1000 })
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Request } from 'express'
|
import type { Request } from 'express'
|
||||||
|
import type { Failbot } from '@github/failbot'
|
||||||
|
|
||||||
import type enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.d.ts'
|
import type enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.d.ts'
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ export type ExtendedRequest = Request & {
|
|||||||
context?: Context
|
context?: Context
|
||||||
language?: string
|
language?: string
|
||||||
userLanguage?: string
|
userLanguage?: string
|
||||||
// Add more properties here as needed
|
FailBot?: Failbot
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make this type from inference using AJV based on the schema.
|
// TODO: Make this type from inference using AJV based on the schema.
|
||||||
@@ -164,6 +165,8 @@ export type Context = Features & {
|
|||||||
productGroups?: ProductGroup[]
|
productGroups?: ProductGroup[]
|
||||||
featuredLinks?: FeaturedLinksExpanded
|
featuredLinks?: FeaturedLinksExpanded
|
||||||
currentLearningTrack?: LearningTrack | null
|
currentLearningTrack?: LearningTrack | null
|
||||||
|
renderedPage?: string
|
||||||
|
miniTocItems?: string | undefined
|
||||||
}
|
}
|
||||||
export type LearningTracks = {
|
export type LearningTracks = {
|
||||||
[group: string]: {
|
[group: string]: {
|
||||||
@@ -349,6 +352,10 @@ export type Page = {
|
|||||||
autogenerated?: string
|
autogenerated?: string
|
||||||
featuredLinks?: FeaturedLinksExpanded
|
featuredLinks?: FeaturedLinksExpanded
|
||||||
redirect_from?: string[]
|
redirect_from?: string[]
|
||||||
|
showMiniToc?: boolean
|
||||||
|
effectiveDate?: string
|
||||||
|
fullTitle?: string
|
||||||
|
render: (context: Context) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangeLog = {
|
type ChangeLog = {
|
||||||
|
|||||||
Reference in New Issue
Block a user