Skip to content

Commit 551da4e

Browse files
authored
Convert page.js to TypeScript (Phase 1) (#56424)
1 parent dcca53d commit 551da4e

File tree

1 file changed

+136
-33
lines changed

1 file changed

+136
-33
lines changed

src/frame/lib/page.js renamed to src/frame/lib/page.ts

Lines changed: 136 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,111 @@ import { renderContentWithFallback } from '@/languages/lib/render-with-fallback'
1919
import { deprecated, supported } from '@/versions/lib/enterprise-server-releases'
2020
import { allPlatforms } from '@/tools/lib/all-platforms'
2121

22+
import type { Context, FrontmatterVersions } from '@/types'
23+
2224
// We're going to check a lot of pages' "ID" (the first part of
2325
// the relativePath) against `productMap` to make sure it's valid.
2426
// To avoid having to do `Object.keys(productMap).includes(id)`
2527
// every single time, we turn it into a Set once.
2628
const productMapKeysAsSet = new Set(Object.keys(productMap))
2729

30+
type ReadFileContentsResult = {
31+
data?: any
32+
content?: string
33+
errors?: any[]
34+
}
35+
36+
type PageInitOptions = {
37+
languageCode: string
38+
relativePath: string
39+
basePath: string
40+
}
41+
42+
type PageReadResult = PageInitOptions & {
43+
fullPath: string
44+
markdown: string
45+
frontmatterErrors?: any[]
46+
} & any
47+
48+
type RenderOptions = {
49+
preferShort?: boolean
50+
unwrap?: boolean
51+
textOnly?: boolean
52+
throwIfEmpty?: boolean
53+
}
54+
55+
type CommunityRedirect = {
56+
name: string
57+
href: string
58+
}
59+
60+
type GuideWithType = {
61+
href: string
62+
title: string
63+
type?: string
64+
topics?: string[]
65+
}
66+
2867
export class FrontmatterErrorsError extends Error {
29-
constructor(message, frontmatterErrors) {
68+
public frontmatterErrors: string[]
69+
70+
constructor(message: string, frontmatterErrors: string[]) {
3071
super(message)
3172
this.frontmatterErrors = frontmatterErrors
3273
}
3374
}
3475

3576
class Page {
36-
static async init(opts) {
37-
opts = await Page.read(opts)
38-
if (!opts) return
39-
return new Page(opts)
77+
// Core properties from PageFrontmatter
78+
public title: string = ''
79+
public rawTitle: string = ''
80+
public shortTitle?: string
81+
public rawShortTitle?: string
82+
public intro: string = ''
83+
public rawIntro?: string
84+
public product?: string
85+
public rawProduct?: string
86+
public permissions?: string
87+
public rawPermissions?: string
88+
public versions: FrontmatterVersions = {}
89+
public showMiniToc?: boolean
90+
public hidden?: boolean
91+
public redirect_from?: string[]
92+
public learningTracks?: any[]
93+
public rawLearningTracks?: string[]
94+
public includeGuides?: GuideWithType[]
95+
public rawIncludeGuides?: string[]
96+
public introLinks?: Record<string, string>
97+
public rawIntroLinks?: Record<string, string>
98+
99+
// Derived properties
100+
public languageCode!: string
101+
public relativePath!: string
102+
public basePath!: string
103+
public fullPath!: string
104+
public markdown!: string
105+
public documentType: string
106+
public applicableVersions: string[]
107+
public permalinks: Permalink[]
108+
public tocItems?: any[]
109+
public communityRedirect?: CommunityRedirect
110+
public detectedPlatforms: string[] = []
111+
public includesPlatformSpecificContent: boolean = false
112+
public detectedTools: string[] = []
113+
public includesToolSpecificContent: boolean = false
114+
public allToolsParsed: typeof allTools = allTools
115+
public introPlainText?: string
116+
117+
// Bound method
118+
public render: (context: Context) => Promise<string>
119+
120+
static async init(opts: PageInitOptions): Promise<Page | undefined> {
121+
const readResult = await Page.read(opts)
122+
if (!readResult) return
123+
return new Page(readResult)
40124
}
41125

42-
static async read(opts) {
126+
static async read(opts: PageInitOptions): Promise<PageReadResult | false> {
43127
assert(opts.languageCode, 'languageCode is required')
44128
assert(opts.relativePath, 'relativePath is required')
45129
assert(opts.basePath, 'basePath is required')
@@ -50,7 +134,11 @@ class Page {
50134
// Per https://nodejs.org/api/fs.html#fs_fs_exists_path_callback
51135
// its better to read and handle errors than to check access/stats first
52136
try {
53-
const { data, content, errors: frontmatterErrors } = await readFileContents(fullPath)
137+
const {
138+
data,
139+
content,
140+
errors: frontmatterErrors,
141+
}: ReadFileContentsResult = await readFileContents(fullPath)
54142

55143
// The `|| ''` is for pages that are purely frontmatter.
56144
// So the `content` property will be `undefined`.
@@ -72,11 +160,11 @@ class Page {
72160
// where as notations like `__GHES_DEPRECATED__[3]`
73161
// or `__GHES_SUPPORTED__[0]` are static.
74162
if (opts.basePath.split(path.sep).includes('fixtures')) {
75-
supported.forEach((version, i, arr) => {
163+
supported.forEach((version: string, i: number, arr: string[]) => {
76164
markdown = markdown.replaceAll(`__GHES_SUPPORTED__[${i}]`, version)
77165
markdown = markdown.replaceAll(`__GHES_SUPPORTED__[-${arr.length - i}]`, version)
78166
})
79-
deprecated.forEach((version, i, arr) => {
167+
deprecated.forEach((version: string, i: number, arr: string[]) => {
80168
markdown = markdown.replaceAll(`__GHES_DEPRECATED__[${i}]`, version)
81169
markdown = markdown.replaceAll(`__GHES_DEPRECATED__[-${arr.length - i}]`, version)
82170
})
@@ -86,25 +174,29 @@ class Page {
86174
...opts,
87175
relativePath,
88176
fullPath,
89-
...data,
177+
...(data || {}),
90178
markdown,
91179
frontmatterErrors,
92-
}
93-
} catch (err) {
180+
} as PageReadResult
181+
} catch (err: any) {
94182
if (err.code === 'ENOENT') return false
95183
console.error(err)
184+
return false
96185
}
97186
}
98187

99-
constructor(opts) {
188+
constructor(opts: PageReadResult) {
100189
if (opts.frontmatterErrors && opts.frontmatterErrors.length) {
101190
throw new FrontmatterErrorsError(
102191
`${opts.frontmatterErrors.length} frontmatter errors trying to load ${opts.fullPath}`,
103192
opts.frontmatterErrors,
104193
)
105194
}
106-
delete opts.frontmatterErrors
107-
Object.assign(this, { ...opts })
195+
196+
// Remove frontmatter errors before assignment
197+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
198+
const { frontmatterErrors: _, ...cleanOpts } = opts
199+
Object.assign(this, cleanOpts)
108200

109201
// Store raw data so we can cache parsed versions
110202
this.rawIntro = this.intro
@@ -113,7 +205,7 @@ class Page {
113205
this.rawProduct = this.product
114206
this.rawPermissions = this.permissions
115207
this.rawLearningTracks = this.learningTracks
116-
this.rawIncludeGuides = this.includeGuides
208+
this.rawIncludeGuides = this.includeGuides as any
117209
this.rawIntroLinks = this.introLinks
118210

119211
// Is this the Homepage or a Product, Category, Topic, or Article?
@@ -130,7 +222,7 @@ class Page {
130222
const versionsParentProductIsNotAvailableIn = this.applicableVersions
131223
// only the homepage will not have this.parentProduct
132224
.filter(
133-
(availableVersion) =>
225+
(availableVersion: string) =>
134226
this.parentProduct && !this.parentProduct.versions.includes(availableVersion),
135227
)
136228

@@ -164,12 +256,15 @@ class Page {
164256
return this
165257
}
166258

167-
buildRedirects() {
168-
return generateRedirectsForPermalinks(this.permalinks, this.redirect_from || [])
259+
buildRedirects(): Record<string, string> {
260+
return generateRedirectsForPermalinks(this.permalinks, this.redirect_from || []) as Record<
261+
string,
262+
string
263+
>
169264
}
170265

171266
// Infer the parent product ID from the page's relative file path
172-
get parentProductId() {
267+
get parentProductId(): string | null {
173268
// Each page's top-level content directory matches its product ID
174269
const id = this.relativePath.split('/')[0]
175270

@@ -184,17 +279,21 @@ class Page {
184279
return id
185280
}
186281

187-
get parentProduct() {
188-
return productMap[this.parentProductId]
282+
get parentProduct(): any {
283+
const id = this.parentProductId
284+
return id ? productMap[id] : undefined
189285
}
190286

191-
async renderTitle(context, opts = { preferShort: true }) {
287+
async renderTitle(
288+
context: Context,
289+
opts: RenderOptions = { preferShort: true },
290+
): Promise<string> {
192291
return opts.preferShort && this.shortTitle
193292
? this.renderProp('shortTitle', context, opts)
194293
: this.renderProp('title', context, opts)
195294
}
196295

197-
async _render(context) {
296+
private async _render(context: Context): Promise<string> {
198297
// use English IDs/anchors for translated headings, so links don't break (see #8572)
199298
if (this.languageCode !== 'en') {
200299
const englishHeadings = getEnglishHeadings(this, context)
@@ -246,7 +345,7 @@ class Page {
246345

247346
// introLinks may contain Liquid and need to have versioning processed.
248347
if (this.rawIntroLinks) {
249-
const introLinks = {}
348+
const introLinks: Record<string, string> = {}
250349
for (const [rawKey, value] of Object.entries(this.rawIntroLinks)) {
251350
introLinks[rawKey] = await renderContent(value, context, {
252351
textOnly: true,
@@ -257,8 +356,8 @@ class Page {
257356
}
258357

259358
if (this.rawIncludeGuides) {
260-
this.includeGuides = await getLinkData(this.rawIncludeGuides, context)
261-
this.includeGuides.map((guide) => {
359+
this.includeGuides = (await getLinkData(this.rawIncludeGuides, context)) as GuideWithType[]
360+
this.includeGuides?.map((guide: any) => {
262361
const { page } = guide
263362
guide.type = page.type
264363
if (page.topics) {
@@ -272,7 +371,7 @@ class Page {
272371
// set a flag so layout knows whether to render a mac/windows/linux switcher element
273372
// Remember, the values of platform is matched in
274373
// the handleInvalidQuerystringValues shielding middleware.
275-
this.detectedPlatforms = allPlatforms.filter((platform) => {
374+
this.detectedPlatforms = allPlatforms.filter((platform: string) => {
276375
// This matches `ghd-tool mac` but not `ghd-tool macos`
277376
// Whereas `html.includes('ghd-tool mac')` would match both.
278377
const regex = new RegExp(`ghd-tool ${platform}\\b|platform-${platform}\\b`)
@@ -281,7 +380,7 @@ class Page {
281380
this.includesPlatformSpecificContent = this.detectedPlatforms.length > 0
282381

283382
// set flags for webui, cli, etc switcher element
284-
this.detectedTools = Object.keys(allTools).filter((tool) => {
383+
this.detectedTools = Object.keys(allTools).filter((tool: string) => {
285384
// This matches `ghd-tool jetbrain` but not `ghd-tool jetbrain_beta`
286385
// Whereas `html.includes('ghd-tool jetbrain')` would match both.
287386
const regex = new RegExp(`ghd-tool ${tool}\\b|tool-${tool}\\b`)
@@ -298,8 +397,12 @@ class Page {
298397

299398
// Allow other modules (like custom liquid tags) to make one-off requests
300399
// for a page's rendered properties like `title` and `intro`
301-
async renderProp(propName, context, opts = { unwrap: false }) {
302-
let prop
400+
async renderProp(
401+
propName: string,
402+
context: Context,
403+
opts: RenderOptions = { unwrap: false },
404+
): Promise<string> {
405+
let prop: string
303406
if (propName === 'title') {
304407
prop = 'rawTitle'
305408
} else if (propName === 'shortTitle') {
@@ -316,13 +419,13 @@ class Page {
316419

317420
// The unwrap option removes surrounding tags from a string, preserving any inner HTML
318421
const $ = cheerio.load(html, { xmlMode: true })
319-
return $.root().contents().html()
422+
return $.root().contents().html() || ''
320423
}
321424

322425
// infer current page's corresponding homepage
323426
// /en/articles/foo -> /en
324427
// /en/enterprise/2.14/user/articles/foo -> /en/enterprise/2.14/user
325-
static getHomepage(requestPath) {
428+
static getHomepage(requestPath: string): string {
326429
return requestPath.replace(/\/articles.*/, '')
327430
}
328431
}

0 commit comments

Comments
 (0)