Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Port product-groups.js to TypeScript (#51364)
  • Loading branch information
Peter Bengtsson authored Jun 25, 2024
commit 48c71d9c49ad6ce46012eaba45cab12ed8fe15de
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { getProductGroups } from '#src/products/lib/get-product-groups.js'
import warmServer from '#src/frame/lib/warm-server.js'
import { languageKeys } from '#src/languages/lib/languages.js'
import { allVersionKeys } from '#src/versions/lib/all-versions.js'
import type { Response, NextFunction } from 'express'

const isHomepage = (path) => {
import type { ExtendedRequest } from '@/types'
import { getProductGroups } from '@/products/lib/get-product-groups'
import warmServer from '@/frame/lib/warm-server.js'
import { languageKeys } from '@/languages/lib/languages.js'
import { allVersionKeys } from '@/versions/lib/all-versions.js'

const isHomepage = (path: string) => {
const split = path.split('/')
// E.g. `/foo` but not `foo/bar` or `foo/`
if (split.length === 2 && split[1] && !split[0]) {
Expand All @@ -17,7 +20,14 @@ const isHomepage = (path) => {
return false
}

export default async function productGroups(req, res, next) {
export default async function productGroups(
req: ExtendedRequest,
res: Response,
next: NextFunction,
) {
if (!req.context) throw new Error('request is not contextualized')
if (!req.pagePath) throw new Error('pagePath is not set on request')
if (!req.language) throw new Error('language is not set on request')
// It's important to use `req.pathPage` instead of `req.path` because
// the request could be the client-side routing from Next where the URL
// might be something like `/_next/data/foo/bar.json` which is translated,
Expand All @@ -31,7 +41,7 @@ export default async function productGroups(req, res, next) {
// known versions. Because if it's not valid, any possible
// use of `{% ifversion ... %}` in Liquid, will throw an error.
if (isHomepage(req.pagePath) && req.context.currentVersionObj) {
const { pages } = await warmServer()
const { pages } = await warmServer([])
req.context.productGroups = await getProductGroups(pages, req.language, req.context)
}

Expand Down
2 changes: 1 addition & 1 deletion src/frame/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import glossaries from './context/glossaries'
import renderProductName from './context/render-product-name'
import features from '@/versions/middleware/features.js'
import productExamples from './context/product-examples'
import productGroups from './context/product-groups.js'
import productGroups from './context/product-groups'
import featuredLinks from '@/landings/middleware/featured-links.js'
import learningTrack from '@/learning-track/middleware/learning-track.js'
import next from './next.js'
Expand Down
4 changes: 2 additions & 2 deletions src/products/lib/all-products.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Product } from '@/types'
import type { PageFrontmatter, Product } from '@/types'

export const { data }: Record<string, any>
export const data: PageFrontmatter

export const productIds: string[]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import path from 'path'

import type { Page, ProductGroup, ProductGroupChild, Context } from '@/types'
import { productMap, data } from './all-products.js'
import { renderContentWithFallback } from '#src/languages/lib/render-with-fallback.js'
import removeFPTFromPath from '#src/versions/lib/remove-fpt-from-path.js'
import { renderContentWithFallback } from '@/languages/lib/render-with-fallback.js'
import removeFPTFromPath from '@/versions/lib/remove-fpt-from-path.js'

async function getPage(id, lang, pageMap, context) {
type PageMap = Record<string, Page>

async function getPage(
id: string,
lang: string,
pageMap: PageMap,
context: Context,
): Promise<ProductGroupChild | undefined> {
const productId = id.split('/')[0]
const product = productMap[productId]

const external = product.external || false // undefined becomes false

// The href depends. Initially all we have is an `id` which might be
Expand All @@ -26,6 +35,8 @@ async function getPage(id, lang, pageMap, context) {

let name = product.name

if (!context.currentVersion) throw new Error('context.currentVersion is not set')

if (!external) {
// First we have to find it as a page object based on its ID.
href = removeFPTFromPath(path.posix.join('/', lang, context.currentVersion, id))
Expand All @@ -34,6 +45,7 @@ async function getPage(id, lang, pageMap, context) {
// fall back it its default version, which is `product.versions[0]`.
// For example, you're on `/en/[email protected]` and you're
// but a `/foo/bar` is only available in `enterprise-cloud@latest`.
if (!product.versions) throw new Error(`Product ${productId} has no versions`)
href = removeFPTFromPath(path.posix.join('/', lang, product.versions[0], id))
}
const page = pageMap[href]
Expand Down Expand Up @@ -74,17 +86,21 @@ async function getPage(id, lang, pageMap, context) {
}
}

export async function getProductGroups(pageMap, lang, context) {
export async function getProductGroups(
pageMap: PageMap,
lang: string,
context: Context,
): Promise<ProductGroup[]> {
return await Promise.all(
data.childGroups.map(async (group) => {
data.childGroups!.map(async (group) => {
return {
name: group.name,
icon: group.icon || null,
octicon: group.octicon || null,
// Typically the children are product IDs, but we support deeper page paths too
children: (
await Promise.all(group.children.map((id) => getPage(id, lang, pageMap, context)))
).filter(Boolean),
).filter(Boolean) as ProductGroupChild[],
}
}),
)
Expand Down
76 changes: 76 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,66 @@ export type ExtendedRequest = Request & {
// Add more properties here as needed
}

// TODO: Make this type from inference using AJV based on the schema.
// For now, it's based on `schema` in frame/lib/frontmatter.js
export type PageFrontmatter = {
title: string
versions: FrontmatterVersions
shortTitle?: string
intro?: string
product?: string
permissions?: string
showMiniToc?: boolean
miniTocMaxHeadingLevel?: number
mapTopic?: boolean
hidden?: boolean
noEarlyAccessBanner?: boolean
earlyAccessToc?: string
layout?: string | boolean
redirect_from?: string[]
allowTitleToDifferFromFilename?: boolean
introLinks?: object
authors?: string[]
examples_source?: string
effectiveDate?: string

featuredLinks?: {
gettingStarted?: string[]
startHere?: string[]
guideCards?: string[]
popular?: string[]
popularHeading?: string
videos?: {
title: string
href: string
}[]
videoHeadings?: string
}[]
changelog?: ChangeLog
type?: string
topics?: string[]
includeGuides?: string[]
learningTracks?: string[]
beta_product?: boolean
product_video?: boolean
product_video_transcript?: string
interactive?: boolean
communityRedirect?: {
name: string
href: string
}
defaultPlatform?: 'mac' | 'windows' | 'linux'
defaultTool?: string
childGroups?: ChildGroup[]
}

export type ChildGroup = {
name: string
octicon: string
children: string[]
icon?: string
}

export type Product = {
id: string
name: string
Expand All @@ -23,6 +83,7 @@ export type Product = {
wip?: boolean
hidden?: boolean
versions?: string[]
external?: boolean
}

type ProductMap = {
Expand Down Expand Up @@ -95,6 +156,21 @@ export type Context = {
currentProductName?: string
productCommunityExamples?: ProductExample[]
productUserExamples?: ProductExample[]
productGroups?: ProductGroup[]
}

export type ProductGroup = {
name: string
icon: string | null
octicon: string | null
children: ProductGroupChild[]
}

export type ProductGroupChild = {
id: string
name: string
href: string
external: boolean
}

export type Glossary = {
Expand Down