From 32a8d91b162375020e2c10b1896af87309ad2b1d Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Tue, 20 May 2025 11:05:51 -0700 Subject: [PATCH] Convert learning-track JS files to TypeScript (#55606) --- src/frame/lib/page.js | 4 +- src/landings/middleware/featured-links.ts | 18 ++- .../{get-link-data.js => get-link-data.ts} | 57 ++++---- ...g-tracks.js => process-learning-tracks.ts} | 32 +++-- src/learning-track/lib/types.ts | 122 ++++++++++++++++++ .../middleware/learning-track.ts | 28 ++-- src/types.ts | 4 +- 7 files changed, 202 insertions(+), 63 deletions(-) rename src/learning-track/lib/{get-link-data.js => get-link-data.ts} (52%) rename src/learning-track/lib/{process-learning-tracks.js => process-learning-tracks.ts} (78%) create mode 100644 src/learning-track/lib/types.ts diff --git a/src/frame/lib/page.js b/src/frame/lib/page.js index 06ef08ef71..e890cef11d 100644 --- a/src/frame/lib/page.js +++ b/src/frame/lib/page.js @@ -8,11 +8,11 @@ import { getAlertTitles } from '#src/languages/lib/get-alert-titles.ts' import getTocItems from './get-toc-items.js' import Permalink from './permalink.js' import { renderContent } from '#src/content-render/index.js' -import processLearningTracks from '#src/learning-track/lib/process-learning-tracks.js' +import processLearningTracks from '#src/learning-track/lib/process-learning-tracks' import { productMap } from '#src/products/lib/all-products.ts' import slash from 'slash' import readFileContents from './read-file-contents.js' -import getLinkData from '#src/learning-track/lib/get-link-data.js' +import getLinkData from '#src/learning-track/lib/get-link-data' import getDocumentType from '#src/events/lib/get-document-type.ts' import { allTools } from '#src/tools/lib/all-tools.ts' import { renderContentWithFallback } from '#src/languages/lib/render-with-fallback.js' diff --git a/src/landings/middleware/featured-links.ts b/src/landings/middleware/featured-links.ts index 70edb636ba..eed8891868 100644 --- a/src/landings/middleware/featured-links.ts +++ b/src/landings/middleware/featured-links.ts @@ -1,8 +1,8 @@ import type { Response, NextFunction } from 'express' import type { ExtendedRequest, FeaturedLinkExpanded } from '@/types' -import getLinkData from '@/learning-track/lib/get-link-data.js' -import { renderContent } from '@/content-render/index.js' +import getLinkData from '@/learning-track/lib/get-link-data' +import { renderContent } from '@/content-render/index' /** * This is the max. number of featured links, by any category, that we @@ -73,12 +73,20 @@ export default async function featuredLinks( if (!(key in req.context.page.featuredLinks)) throw new Error('featureLinks key not found in Page') const pageFeaturedLink = req.context.page.featuredLinks[key] - req.context.featuredLinks[key] = (await getLinkData( - pageFeaturedLink, + // Handle different types of featuredLinks by converting to string array + const stringLinks = Array.isArray(pageFeaturedLink) + ? pageFeaturedLink.map((item) => (typeof item === 'string' ? item : item.href)) + : [] + + const linkData = await getLinkData( + stringLinks, req.context, { title: true, intro: true, fullTitle: true }, MAX_FEATURED_LINKS, - )) as FeaturedLinkExpanded[] // Remove ones `getLinkData` is TS + ) + // We need to use a type assertion here because the Page interfaces are incompatible + // between our local types and the global types, but the actual runtime objects are compatible + req.context.featuredLinks[key] = (linkData || []) as unknown as FeaturedLinkExpanded[] } } diff --git a/src/learning-track/lib/get-link-data.js b/src/learning-track/lib/get-link-data.ts similarity index 52% rename from src/learning-track/lib/get-link-data.js rename to src/learning-track/lib/get-link-data.ts index 5705f94b65..66856641bc 100644 --- a/src/learning-track/lib/get-link-data.js +++ b/src/learning-track/lib/get-link-data.ts @@ -1,32 +1,34 @@ import path from 'path' -import findPage from '#src/frame/lib/find-page.js' -import nonEnterpriseDefaultVersion from '#src/versions/lib/non-enterprise-default-version.js' -import removeFPTFromPath from '#src/versions/lib/remove-fpt-from-path.js' -import { renderContent } from '#src/content-render/index.js' -import { executeWithFallback } from '#src/languages/lib/render-with-fallback.js' +import findPage from '@/frame/lib/find-page' +import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version' +import removeFPTFromPath from '@/versions/lib/remove-fpt-from-path' +import { renderContent } from '@/content-render/index' +import { executeWithFallback } from '@/languages/lib/render-with-fallback' +import { Context, LinkOptions, ProcessedLink } from './types' // rawLinks is an array of paths: [ '/foo' ] // we need to convert it to an array of localized objects: [ { href: '/en/foo', title: 'Foo', intro: 'Description here' } ] -export default async ( - rawLinks, - context, - option = { title: true, intro: true, fullTitle: false }, +export default async function getLinkData( + rawLinks: string[] | string | undefined, + context: Context, + options: LinkOptions = { title: true, intro: true, fullTitle: false }, maxLinks = Infinity, -) => { - if (!rawLinks) return +): Promise { + if (!rawLinks) return undefined if (typeof rawLinks === 'string') { - return await processLink(rawLinks, context, option) + const processedLink = await processLink(rawLinks, context, options) + return processedLink ? [processedLink] : undefined } - const links = [] + const links: ProcessedLink[] = [] // Using a for loop here because the async work is not network or // disk bound. It's CPU bound. // And if we use a for-loop we can potentially bail early if // the `maxLinks` is reached. That's instead of computing them all, // and then slicing the array. So it avoids wasted processing. for (const link of rawLinks) { - const processedLink = await processLink(link, context, option) + const processedLink = await processLink(link, context, options) if (processedLink) { links.push(processedLink) if (links.length >= maxLinks) { @@ -38,9 +40,13 @@ export default async ( return links } -async function processLink(link, context, option) { - const opts = { textOnly: true } - const linkHref = link.href || link +async function processLink( + link: string | { href: string }, + context: Context, + options: LinkOptions, +): Promise { + const opts: { textOnly: boolean; preferShort?: boolean } = { textOnly: true } + const linkHref = typeof link === 'string' ? link : link.href // Parse the link in case it includes Liquid conditionals const linkPath = linkHref.includes('{') ? await executeWithFallback( @@ -55,10 +61,13 @@ async function processLink(link, context, option) { if (!linkPath) return null const version = - context.currentVersion === 'homepage' ? nonEnterpriseDefaultVersion : context.currentVersion - const href = removeFPTFromPath(path.join('/', context.currentLanguage, version, linkPath)) + (context.currentVersion === 'homepage' + ? nonEnterpriseDefaultVersion + : context.currentVersion) || 'free-pro-team@latest' + const currentLanguage = context.currentLanguage || 'en' + const href = removeFPTFromPath(path.join('/', currentLanguage, version, linkPath)) - const linkedPage = findPage(href, context.pages, context.redirects) + const linkedPage = findPage(href, context.pages || {}, context.redirects || {}) if (!linkedPage) { // This can happen when the link depends on Liquid conditionals, // like... @@ -66,18 +75,18 @@ async function processLink(link, context, option) { return null } - const result = { href, page: linkedPage } + const result: ProcessedLink = { href, page: linkedPage } - if (option.title) { + if (options.title) { result.title = await linkedPage.renderTitle(context, opts) } - if (option.fullTitle) { + if (options.fullTitle) { opts.preferShort = false result.fullTitle = await linkedPage.renderTitle(context, opts) } - if (option.intro) { + if (options.intro) { result.intro = await linkedPage.renderProp('intro', context, opts) } return result diff --git a/src/learning-track/lib/process-learning-tracks.js b/src/learning-track/lib/process-learning-tracks.ts similarity index 78% rename from src/learning-track/lib/process-learning-tracks.js rename to src/learning-track/lib/process-learning-tracks.ts index fa38c529ee..97a5fee582 100644 --- a/src/learning-track/lib/process-learning-tracks.js +++ b/src/learning-track/lib/process-learning-tracks.ts @@ -1,15 +1,19 @@ -import getLinkData from './get-link-data.js' -import getApplicableVersions from '#src/versions/lib/get-applicable-versions.js' -import { getDataByLanguage } from '#src/data-directory/lib/get-data.js' -import { renderContent } from '#src/content-render/index.js' -import { executeWithFallback } from '#src/languages/lib/render-with-fallback.js' +import getLinkData from './get-link-data' +import getApplicableVersions from '@/versions/lib/get-applicable-versions' +import { getDataByLanguage } from '@/data-directory/lib/get-data' +import { renderContent } from '@/content-render/index' +import { executeWithFallback } from '@/languages/lib/render-with-fallback' +import { Context, TrackGuide, LearningTrack, ProcessedLearningTracks } from './types' const renderOpts = { textOnly: true } // This module returns an object that contains a single featured learning track // and an array of all the other learning tracks for the current version. -export default async function processLearningTracks(rawLearningTracks, context) { - const learningTracks = [] +export default async function processLearningTracks( + rawLearningTracks: string[], + context: Context, +): Promise { + const learningTracks: LearningTrack[] = [] if (!context.currentProduct) { throw new Error(`Missing context.currentProduct value.`) @@ -59,7 +63,7 @@ export default async function processLearningTracks(rawLearningTracks, context) // we need to have the English `title` and `description` to // fall back to. // - let enTrack + let enTrack: any if (context.currentLanguage !== 'en') { enTrack = getDataByLanguage( `learning-tracks.${context.currentProduct}.${renderedTrackName}`, @@ -86,26 +90,28 @@ export default async function processLearningTracks(rawLearningTracks, context) const title = await executeWithFallback( context, () => renderContent(track.title, context, renderOpts), - (enContext) => renderContent(enTrack.title, enContext, renderOpts), + (enContext: any) => renderContent(enTrack.title, enContext, renderOpts), ) const description = await executeWithFallback( context, () => renderContent(track.description, context, renderOpts), - (enContext) => renderContent(enTrack.description, enContext, renderOpts), + (enContext: any) => renderContent(enTrack.description, enContext, renderOpts), ) - const learningTrack = { + const guides = (await getLinkData(track.guides, context)) || [] + + const learningTrack: LearningTrack = { trackName: renderedTrackName, trackProduct: context.currentProduct || null, title, description, // getLinkData respects versioning and only returns guides available in the current version; // if no guides are available, the learningTrack.guides property will be an empty array. - guides: await getLinkData(track.guides, context), + guides: guides as TrackGuide[], } // Only add the track to the array of tracks if there are guides in this version and it's not the featured track. - if (learningTrack.guides.length) { + if (Array.isArray(learningTrack.guides) && learningTrack.guides.length > 0) { learningTracks.push(learningTrack) } } diff --git a/src/learning-track/lib/types.ts b/src/learning-track/lib/types.ts new file mode 100644 index 0000000000..2547d6db81 --- /dev/null +++ b/src/learning-track/lib/types.ts @@ -0,0 +1,122 @@ +/** + * Common types used across learning track components + */ + +/** + * Basic context interface for rendering operations + */ +export interface Context { + currentProduct?: string + currentLanguage?: string + currentVersion?: string + pages?: any + redirects?: any + // Additional properties that may be needed for rendering + [key: string]: any +} + +/** + * Options for retrieving link data + */ +export interface LinkOptions { + title?: boolean + intro?: boolean + fullTitle?: boolean +} + +/** + * Result of processing a link + */ +export interface ProcessedLink { + href: string + page: Page + title?: string + fullTitle?: string + intro?: string +} + +/** + * Definitions for featured links data + */ +export interface FeaturedLink { + title: string + href: string +} + +export interface PageFeaturedLinks { + [key: string]: string[] | FeaturedLink[] +} + +/** + * Page interface for basic page properties + */ +export interface Page { + renderTitle: (context: Context, opts: any) => Promise + renderProp: (prop: string, context: Context, opts: any) => Promise +} + +/** + * Guide in a learning track + */ +export interface TrackGuide { + href: string + page: Page + title: string + intro?: string +} + +/** + * A processed learning track + */ +export interface LearningTrack { + trackName: string + trackProduct: string | null + title: string + description: string + guides: TrackGuide[] +} + +/** + * Learning track metadata with guides + */ +export interface LearningTrackMetadata { + title: string + description: string + guides: string[] + versions?: any +} + +/** + * Collection of learning tracks by product and track name + */ +export interface LearningTracks { + [productId: string]: { + [trackName: string]: LearningTrackMetadata + } +} + +/** + * Return type for processLearningTracks function + */ +export interface ProcessedLearningTracks { + learningTracks: LearningTrack[] +} + +/** + * Learning track data for the current guide + */ +export interface CurrentLearningTrack { + trackName: string + trackProduct: string + trackTitle: string + numberOfGuides?: number + currentGuideIndex?: number + nextGuide?: { + href: string + title: string | undefined + } + prevGuide?: { + href: string + title: string | undefined + } +} diff --git a/src/learning-track/middleware/learning-track.ts b/src/learning-track/middleware/learning-track.ts index 35585f2189..6eb260427f 100644 --- a/src/learning-track/middleware/learning-track.ts +++ b/src/learning-track/middleware/learning-track.ts @@ -1,28 +1,22 @@ import type { Response, NextFunction } from 'express' -import type { - Context, - ExtendedRequest, - LearningTrack, - LearningTracks, - TrackGuide, - Page, -} from '@/types' +import type { ExtendedRequest, LearningTracks } from '@/types' +import type { Context, CurrentLearningTrack, TrackGuide } from '../lib/types' import { getPathWithoutLanguage, getPathWithoutVersion } from '@/frame/lib/path-utils.js' -import getLinkData from '../lib/get-link-data.js' +import getLinkData from '../lib/get-link-data' import { renderContent } from '@/content-render/index.js' import { executeWithFallback } from '@/languages/lib/render-with-fallback.js' import { getDeepDataByLanguage } from '@/data-directory/lib/get-data.js' export default async function learningTrack( - req: ExtendedRequest, + req: ExtendedRequest & { context: Context }, res: Response, next: NextFunction, ) { if (!req.context) throw new Error('request is not contextualized') const noTrack = () => { - req.context!.currentLearningTrack = null + req.context.currentLearningTrack = null return next() } @@ -94,12 +88,12 @@ export default async function learningTrack( () => '', // todo use english track.title ) - const currentLearningTrack: LearningTrack = { trackName, trackProduct, trackTitle } + const currentLearningTrack: CurrentLearningTrack = { trackName, trackProduct, trackTitle } const guidePath = getPathWithoutLanguage(getPathWithoutVersion(req.pagePath)) // The raw track.guides will return all guide paths, need to use getLinkData // so we only get guides available in the current version - const trackGuides = (await getLinkData(track.guides, req.context)) as TrackGuide[] + const trackGuides = ((await getLinkData(track.guides, req.context)) || []) as TrackGuide[] const trackGuidePaths = trackGuides.map((guide) => { return getPathWithoutLanguage(getPathWithoutVersion(guide.href)) @@ -137,8 +131,8 @@ export default async function learningTrack( intro: false, fullTitle: false, }) - if (!resultData) return noTrack() - const result = resultData as { href: string; page: Page; title: string } + if (!resultData || !resultData.length) return noTrack() + const result = resultData[0] const href = result.href const title = result.title @@ -152,8 +146,8 @@ export default async function learningTrack( intro: false, fullTitle: false, }) - if (!resultData) return noTrack() - const result = resultData as { href: string; page: Page; title: string } + if (!resultData || !resultData.length) return noTrack() + const result = resultData[0] const href = result.href const title = result.title diff --git a/src/types.ts b/src/types.ts index 033371af77..2c7b79cb14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -185,11 +185,11 @@ export type LearningTrack = { currentGuideIndex?: number nextGuide?: { href: string - title: string + title: string | undefined } prevGuide?: { href: string - title: string + title: string | undefined } }