diff --git a/src/frame/middleware/context/product-groups.js b/src/frame/middleware/context/product-groups.ts similarity index 63% rename from src/frame/middleware/context/product-groups.js rename to src/frame/middleware/context/product-groups.ts index add02931f8..59f7e282fb 100644 --- a/src/frame/middleware/context/product-groups.js +++ b/src/frame/middleware/context/product-groups.ts @@ -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]) { @@ -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, @@ -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) } diff --git a/src/frame/middleware/index.ts b/src/frame/middleware/index.ts index 96c22c8c56..512e5ffdc9 100644 --- a/src/frame/middleware/index.ts +++ b/src/frame/middleware/index.ts @@ -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' diff --git a/src/products/lib/all-products.d.ts b/src/products/lib/all-products.d.ts index 755864c34f..923571e2a9 100644 --- a/src/products/lib/all-products.d.ts +++ b/src/products/lib/all-products.d.ts @@ -1,6 +1,6 @@ -import type { Product } from '@/types' +import type { PageFrontmatter, Product } from '@/types' -export const { data }: Record +export const data: PageFrontmatter export const productIds: string[] diff --git a/src/products/lib/get-product-groups.js b/src/products/lib/get-product-groups.ts similarity index 79% rename from src/products/lib/get-product-groups.js rename to src/products/lib/get-product-groups.ts index 242e90525a..916e6a5c23 100644 --- a/src/products/lib/get-product-groups.js +++ b/src/products/lib/get-product-groups.ts @@ -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 + +async function getPage( + id: string, + lang: string, + pageMap: PageMap, + context: Context, +): Promise { 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 @@ -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)) @@ -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/enterprise-server@3.1` 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] @@ -74,9 +86,13 @@ 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 { return await Promise.all( - data.childGroups.map(async (group) => { + data.childGroups!.map(async (group) => { return { name: group.name, icon: group.icon || null, @@ -84,7 +100,7 @@ export async function getProductGroups(pageMap, lang, context) { // 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[], } }), ) diff --git a/src/types.ts b/src/types.ts index 9132407ce6..0b5ba99841 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 @@ -23,6 +83,7 @@ export type Product = { wip?: boolean hidden?: boolean versions?: string[] + external?: boolean } type ProductMap = { @@ -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 = {