1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Migrate 6 JavaScript files to TypeScript (#57969)

This commit is contained in:
Kevin Heis
2025-10-15 09:43:53 -07:00
committed by GitHub
parent 5268d1a084
commit a415892096
12 changed files with 152 additions and 106 deletions

View File

@@ -2,9 +2,15 @@ import { describe, expect, test } from 'vitest'
import { shouldIncludeResult } from '../../lib/helpers/should-include-result'
import { reportingConfig } from '../../style/github-docs'
interface LintFlaw {
severity: string
ruleNames: string[]
errorDetail?: string
}
describe('lint report exclusions', () => {
// Helper function to simulate the reporting logic from lint-report.ts
function shouldIncludeInReport(flaw, filePath) {
function shouldIncludeInReport(flaw: LintFlaw, filePath: string): boolean {
if (!flaw.ruleNames || !Array.isArray(flaw.ruleNames)) {
return false
}

View File

@@ -1,18 +1,34 @@
import { describe, expect, test } from 'vitest'
import { octiconAriaLabels } from '../../lib/linting-rules/octicon-aria-labels'
interface ErrorInfo {
lineNumber: number
detail?: string
context?: string
range?: [number, number]
fixInfo?: any // Matches RuleErrorCallback signature - fixInfo structure varies by rule
}
describe('octicon-aria-labels', () => {
const rule = octiconAriaLabels
test('detects octicon without aria-label', () => {
const errors = []
const onError = (errorInfo) => {
// Helper to create onError callback that captures errors
function createErrorCollector() {
const errors: ErrorInfo[] = []
// Using any because the actual rule implementation calls onError with an object,
// not individual parameters as defined in RuleErrorCallback
const onError = (errorInfo: any) => {
errors.push(errorInfo)
}
return { errors, onError }
}
test('detects octicon without aria-label', () => {
const { errors, onError } = createErrorCollector()
const content = ['This is a test with an octicon:', '{% octicon "alert" %}', 'Some more text.']
rule.function({ lines: content }, onError)
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
@@ -21,10 +37,7 @@ describe('octicon-aria-labels', () => {
})
test('ignores octicons with aria-label', () => {
const errors = []
const onError = (errorInfo) => {
errors.push(errorInfo)
}
const { errors, onError } = createErrorCollector()
const content = [
'This is a test with a proper octicon:',
@@ -32,16 +45,13 @@ describe('octicon-aria-labels', () => {
'Some more text.',
]
rule.function({ lines: content }, onError)
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
expect(errors.length).toBe(0)
})
test('detects multiple octicons without aria-label', () => {
const errors = []
const onError = (errorInfo) => {
errors.push(errorInfo)
}
const { errors, onError } = createErrorCollector()
const content = [
'This is a test with multiple octicons:',
@@ -51,7 +61,7 @@ describe('octicon-aria-labels', () => {
'More text.',
]
rule.function({ lines: content }, onError)
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
expect(errors.length).toBe(2)
expect(errors[0].lineNumber).toBe(2)
@@ -61,10 +71,7 @@ describe('octicon-aria-labels', () => {
})
test('ignores non-octicon liquid tags', () => {
const errors = []
const onError = (errorInfo) => {
errors.push(errorInfo)
}
const { errors, onError } = createErrorCollector()
const content = [
'This is a test with non-octicon tags:',
@@ -74,16 +81,13 @@ describe('octicon-aria-labels', () => {
'{% endif %}',
]
rule.function({ lines: content }, onError)
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
expect(errors.length).toBe(0)
})
test('suggests correct fix for octicon with other attributes', () => {
const errors = []
const onError = (errorInfo) => {
errors.push(errorInfo)
}
const { errors, onError } = createErrorCollector()
const content = [
'This is a test with an octicon with other attributes:',
@@ -91,7 +95,7 @@ describe('octicon-aria-labels', () => {
'Some more text.',
]
rule.function({ lines: content }, onError)
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
@@ -101,10 +105,7 @@ describe('octicon-aria-labels', () => {
})
test('handles octicons with unusual spacing', () => {
const errors = []
const onError = (errorInfo) => {
errors.push(errorInfo)
}
const { errors, onError } = createErrorCollector()
const content = [
'This is a test with unusual spacing:',
@@ -112,7 +113,7 @@ describe('octicon-aria-labels', () => {
'Some more text.',
]
rule.function({ lines: content }, onError)
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
@@ -120,10 +121,7 @@ describe('octicon-aria-labels', () => {
})
test('handles octicons split across multiple lines', () => {
const errors = []
const onError = (errorInfo) => {
errors.push(errorInfo)
}
const { errors, onError } = createErrorCollector()
const content = [
'This is a test with a multi-line octicon:',
@@ -133,17 +131,14 @@ describe('octicon-aria-labels', () => {
'Some more text.',
]
rule.function({ lines: content }, onError)
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
expect(errors.length).toBe(1)
expect(errors[0].detail).toContain('aria-label=chevron-down')
})
test('falls back to "icon" when octicon name cannot be determined', () => {
const errors = []
const onError = (errorInfo) => {
errors.push(errorInfo)
}
const { errors, onError } = createErrorCollector()
const content = [
'This is a test with a malformed octicon:',
@@ -151,7 +146,7 @@ describe('octicon-aria-labels', () => {
'Some more text.',
]
rule.function({ lines: content }, onError)
rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError)
expect(errors.length).toBe(1)
expect(errors[0].detail).toContain('aria-label=icon')

View File

@@ -1,12 +1,13 @@
import { describe, expect, test, beforeEach, afterEach } from 'vitest'
import { renderContent } from '@/content-render/index'
import { TitleFromAutotitleError } from '@/content-render/unified/rewrite-local-links'
import type { Context } from '@/types'
describe('link error line numbers', () => {
let fs
let originalReadFileSync
let originalExistsSync
let mockContext
let fs: any // Dynamic import of fs module for mocking in tests
let originalReadFileSync: any // Storing original fs.readFileSync for restoration after test
let originalExistsSync: any // Storing original fs.existsSync for restoration after test
let mockContext: Context
beforeEach(async () => {
// Set up file system mocking
@@ -20,11 +21,11 @@ describe('link error line numbers', () => {
mockContext = {
currentLanguage: 'en',
currentVersion: 'free-pro-team@latest',
pages: new Map(),
redirects: new Map(),
pages: {} as any,
redirects: {} as any,
page: {
fullPath: '/fake/test-file.md',
},
} as any,
}
})
@@ -60,14 +61,16 @@ More content here.`
// The broken link is on line 10 in the original file
// (3 lines of frontmatter + 1 blank line + 1 title + 1 blank + 1 content + 1 blank + 1 link line)
// The error message should reference the correct line number
expect(error.message).toContain('/nonexistent/page')
expect(error.message).toContain('could not be resolved')
expect(error.message).toContain('(Line: 10)')
expect((error as TitleFromAutotitleError).message).toContain('/nonexistent/page')
expect((error as TitleFromAutotitleError).message).toContain('could not be resolved')
expect((error as TitleFromAutotitleError).message).toContain('(Line: 10)')
}
})
test('reports correct line numbers with different frontmatter sizes', async () => {
mockContext.page.fullPath = '/fake/test-file-2.md'
mockContext.page = {
fullPath: '/fake/test-file-2.md',
} as any
// Test with more extensive frontmatter
const template = `---
@@ -96,13 +99,15 @@ Content with a [AUTOTITLE](/another/nonexistent/page) link.`
expect.fail('Expected TitleFromAutotitleError to be thrown')
} catch (error) {
expect(error).toBeInstanceOf(TitleFromAutotitleError)
expect(error.message).toContain('/another/nonexistent/page')
expect(error.message).toContain('could not be resolved')
expect((error as TitleFromAutotitleError).message).toContain('/another/nonexistent/page')
expect((error as TitleFromAutotitleError).message).toContain('could not be resolved')
}
})
test('handles files without frontmatter correctly', async () => {
mockContext.page.fullPath = '/fake/no-frontmatter.md'
mockContext.page = {
fullPath: '/fake/no-frontmatter.md',
} as any
// Test content without frontmatter
const template = `# Simple Title
@@ -118,13 +123,15 @@ Here is a broken link: [AUTOTITLE](/missing/page).`
expect.fail('Expected TitleFromAutotitleError to be thrown')
} catch (error) {
expect(error).toBeInstanceOf(TitleFromAutotitleError)
expect(error.message).toContain('/missing/page')
expect(error.message).toContain('could not be resolved')
expect((error as TitleFromAutotitleError).message).toContain('/missing/page')
expect((error as TitleFromAutotitleError).message).toContain('could not be resolved')
}
})
test('error message format is improved', async () => {
mockContext.page.fullPath = '/fake/message-test.md'
mockContext.page = {
fullPath: '/fake/message-test.md',
} as any
const template = `---
title: Message Test
@@ -142,13 +149,17 @@ title: Message Test
expect(error).toBeInstanceOf(TitleFromAutotitleError)
// Check that the new error message format is used
expect(error.message).toContain('could not be resolved in one or more versions')
expect(error.message).toContain('Make sure that this link can be reached from all versions')
expect(error.message).toContain('/test/broken/link')
expect((error as TitleFromAutotitleError).message).toContain(
'could not be resolved in one or more versions',
)
expect((error as TitleFromAutotitleError).message).toContain(
'Make sure that this link can be reached from all versions',
)
expect((error as TitleFromAutotitleError).message).toContain('/test/broken/link')
// Check that the old error message format is NOT used
expect(error.message).not.toContain('Unable to find Page by')
expect(error.message).not.toContain('To fix it, look at')
expect((error as TitleFromAutotitleError).message).not.toContain('Unable to find Page by')
expect((error as TitleFromAutotitleError).message).not.toContain('To fix it, look at')
}
})
})

View File

@@ -2,9 +2,33 @@ import cheerio from 'cheerio'
import { range } from 'lodash-es'
import { renderContent } from '@/content-render/index'
import type { Context } from '@/types'
interface MiniTocContents {
href: string
title: string
}
export interface MiniTocItem {
contents: MiniTocContents
items?: MiniTocItem[]
platform?: string
}
interface FlatTocItem {
contents: MiniTocContents
headingLevel: number
platform: string
indentationLevel: number
items?: FlatTocItem[]
}
// Keep maxHeadingLevel=2 for accessibility reasons, see docs-engineering#2701 for more info
export default function getMiniTocItems(html, maxHeadingLevel = 2, headingScope = '') {
export default function getMiniTocItems(
html: string,
maxHeadingLevel = 2,
headingScope = '',
): MiniTocItem[] {
const $ = cheerio.load(html, { xmlMode: true })
// eg `h2, h3` or `h2, h3, h4` depending on maxHeadingLevel
@@ -20,7 +44,7 @@ export default function getMiniTocItems(html, maxHeadingLevel = 2, headingScope
// - `platform` to show or hide platform-specific headings via client JS
// H1 = highest importance, H6 = lowest importance
let mostImportantHeadingLevel
let mostImportantHeadingLevel: number | undefined
const flatToc = headings
.get()
.filter((item) => {
@@ -48,13 +72,14 @@ export default function getMiniTocItems(html, maxHeadingLevel = 2, headingScope
// remove any <strong> tags but leave content
$('strong', item).map((i, el) => $(el).replaceWith($(el).contents()))
const contents = { href, title: $(item).text().trim() }
const headingLevel = parseInt($(item)[0].name.match(/\d+/)[0], 10) || 0 // the `2` from `h2`
const contents: MiniTocContents = { href, title: $(item).text().trim() }
const element = $(item)[0] as cheerio.TagElement
const headingLevel = parseInt(element.name.match(/\d+/)![0], 10) || 0 // the `2` from `h2`
const platform = $(item).parent('.ghd-tool').attr('class') || ''
// track the most important heading level while we're looping through the items
if (headingLevel < mostImportantHeadingLevel || mostImportantHeadingLevel === undefined) {
if (headingLevel < mostImportantHeadingLevel! || mostImportantHeadingLevel === undefined) {
mostImportantHeadingLevel = headingLevel
}
@@ -65,8 +90,8 @@ export default function getMiniTocItems(html, maxHeadingLevel = 2, headingScope
// set the indentation level for each item based on the most important
// heading level in the current article
return {
...item,
indentationLevel: item.headingLevel - mostImportantHeadingLevel,
...item!,
indentationLevel: item!.headingLevel - mostImportantHeadingLevel!,
}
})
@@ -77,18 +102,18 @@ export default function getMiniTocItems(html, maxHeadingLevel = 2, headingScope
}
// Recursively build a tree from the list of allItems
function buildNestedToc(allItems, startIndex = 0) {
function buildNestedToc(allItems: FlatTocItem[], startIndex = 0): FlatTocItem[] {
const startItem = allItems[startIndex]
if (!startItem) {
return []
}
let curLevelIndentation = startItem.indentationLevel
const currentLevel = []
const currentLevel: FlatTocItem[] = []
for (let cursor = startIndex; cursor < allItems.length; cursor++) {
const cursorItem = allItems[cursor]
const nextItem = allItems[cursor + 1]
const nextItemIsNested = nextItem && nextItem.indentationLevel > cursorItem.indentationLevel
const nextItemIsNested = nextItem && nextItem.indentationLevel! > cursorItem.indentationLevel!
// if it's the current indentation level, push it on and keep going
if (curLevelIndentation === cursorItem.indentationLevel) {
@@ -125,10 +150,10 @@ function buildNestedToc(allItems, startIndex = 0) {
// Strip the bits and pieces from each object in the array that are
// not needed in the React component rendering.
function minimalMiniToc(toc) {
function minimalMiniToc(toc: FlatTocItem[]): MiniTocItem[] {
return toc.map(({ platform, contents, items }) => {
const minimal = { contents }
const subItems = minimalMiniToc(items)
const minimal: MiniTocItem = { contents }
const subItems = minimalMiniToc(items || [])
if (subItems.length) minimal.items = subItems
if (platform) minimal.platform = platform
return minimal
@@ -136,11 +161,11 @@ function minimalMiniToc(toc) {
}
export async function getAutomatedPageMiniTocItems(
items,
context,
items: string[],
context: Context,
depth = 2,
markdownHeading = '',
) {
): Promise<MiniTocItem[]> {
const titles =
markdownHeading +
items

View File

@@ -28,7 +28,7 @@ async function buildRenderedPage(req: ExtendedRequest): Promise<string> {
return (await pageRenderTimed(context)) as string
}
async function buildMiniTocItems(req: ExtendedRequest): Promise<string | undefined> {
function buildMiniTocItems(req: ExtendedRequest) {
const { context } = req
if (!context) throw new Error('request not contextualized')
const { page } = context
@@ -38,7 +38,7 @@ async function buildMiniTocItems(req: ExtendedRequest): Promise<string | undefin
return
}
return getMiniTocItems(context.renderedPage, 0)
return getMiniTocItems(context.renderedPage || '', 0)
}
export default async function renderPage(req: ExtendedRequest, res: Response) {
@@ -92,7 +92,7 @@ export default async function renderPage(req: ExtendedRequest, res: Response) {
if (!req.context) throw new Error('request not contextualized')
req.context.renderedPage = await buildRenderedPage(req)
req.context.miniTocItems = await buildMiniTocItems(req)
req.context.miniTocItems = buildMiniTocItems(req)
// Stop processing if the connection was already dropped
if (isConnectionDropped(req, res)) return

View File

@@ -30,7 +30,7 @@ describe('mini toc items', () => {
].join('\n')
const tocItems = getMiniTocItems(html, 3)
expect(tocItems.length).toBe(2)
expect(tocItems[0].items.length).toBe(3)
expect(tocItems[0].items?.length).toBe(3)
})
/**
@@ -56,10 +56,11 @@ describe('mini toc items', () => {
].join('\n')
const tocItems = getMiniTocItems(html, 3)
expect(tocItems.length).toBe(4)
expect(tocItems[3].items.length).toBe(1)
expect(tocItems[3].items?.length).toBe(1)
})
// Mock scenario from: /en/organizations/managing-membership-in-your-organization/inviting-users-to-join-your-organization
// Mock scenario from:
// /en/organizations/managing-membership-in-your-organization/inviting-users-to-join-your-organization
test('creates empty toc', async () => {
const html = h1('test')
const tocItems = getMiniTocItems(html, 3)
@@ -86,6 +87,6 @@ describe('mini toc items', () => {
].join('\n')
const tocItems = getMiniTocItems(html, 5)
expect(tocItems.length).toBe(3)
expect(tocItems[1].items[0].items[0].items.length).toBe(1)
expect(tocItems[1].items?.[0].items?.[0].items?.length).toBe(1)
})
})

View File

@@ -38,7 +38,7 @@ export default function GraphqlBreakingChanges({
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { getGraphqlBreakingChanges } = await import('@/graphql/lib/index')
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items.js')
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items')
const req = context.req as any
const res = context.res as any

View File

@@ -29,7 +29,7 @@ export default function GraphqlChangelog({ mainContext, schema, automatedPageCon
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { getGraphqlChangelog } = await import('@/graphql/lib/index')
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items.js')
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items')
const req = context.req as any
const res = context.res as any

View File

@@ -34,7 +34,7 @@ export default function GraphqlPreviews({ mainContext, schema, automatedPageCont
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { getPreviews } = await import('@/graphql/lib/index')
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items.js')
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items')
const req = context.req as any
const res = context.res as any

View File

@@ -15,7 +15,13 @@ import { nonAutomatedRestPaths } from '../lib/config'
import { deprecated } from '@/versions/lib/enterprise-server-releases'
import walkFiles from '@/workflows/walk-files'
export async function getDiffOpenAPIContentRest() {
type CheckObject = Record<string, Record<string, string[]>>
type DifferenceResult = Record<string, string[]>
type ErrorMessages = Record<string, Record<string, { contentDir: string[]; openAPI: string[] }>>
export async function getDiffOpenAPIContentRest(): Promise<ErrorMessages> {
const contentFiles = getAutomatedMarkdownFiles('content/rest')
// Creating the categories/subcategories based on the current content directory
const checkContentDir = await createCheckContentDirectory(contentFiles)
@@ -25,15 +31,13 @@ export async function getDiffOpenAPIContentRest() {
// Get Differences between categories/subcategories from dereferenced schemas and the content/rest directory frontmatter versions
const differences = getDifferences(openAPISchemaCheck, checkContentDir)
const errorMessages = {}
const errorMessages: ErrorMessages = {}
if (Object.keys(differences).length > 0) {
for (const schemaName in differences) {
errorMessages[schemaName] = {}
differences[schemaName].forEach((category) => {
if (!errorMessages[schemaName]) errorMessages[schemaName] = category
errorMessages[schemaName][category] = {
contentDir: checkContentDir[schemaName][category],
openAPI: openAPISchemaCheck[schemaName][category],
@@ -45,7 +49,7 @@ export async function getDiffOpenAPIContentRest() {
return errorMessages
}
async function createOpenAPISchemasCheck() {
async function createOpenAPISchemasCheck(): Promise<CheckObject> {
const openAPICheck = createCheckObj()
const restDirectory = fs
.readdirSync(REST_DATA_DIR)
@@ -55,12 +59,12 @@ async function createOpenAPISchemasCheck() {
restDirectory.forEach((dir) => {
const filename = path.join(REST_DATA_DIR, dir, REST_SCHEMA_FILENAME)
const fileSchema = JSON.parse(fs.readFileSync(filename))
const fileSchema = JSON.parse(fs.readFileSync(filename, 'utf8'))
const categories = Object.keys(fileSchema).sort()
const version = getDocsVersion(dir)
categories.forEach((category) => {
const subcategories = Object.keys(fileSchema[category])
const subcategories = Object.keys(fileSchema[category]) as string[]
if (isApiVersioned(version)) {
getOnlyApiVersions(version).forEach(
(apiVersion) => (openAPICheck[apiVersion][category] = subcategories.sort()),
@@ -74,12 +78,12 @@ async function createOpenAPISchemasCheck() {
return openAPICheck
}
async function createCheckContentDirectory(contentFiles) {
async function createCheckContentDirectory(contentFiles: string[]): Promise<CheckObject> {
const checkContent = createCheckObj()
for (const filename of contentFiles) {
const { data } = frontmatter(await fs.promises.readFile(filename, 'utf8'))
const applicableVersions = getApplicableVersions(data.versions, filename)
const applicableVersions = getApplicableVersions(data?.versions, filename)
const splitPath = filename.split('/')
const subCategory = splitPath[splitPath.length - 1].replace('.md', '')
const category =
@@ -104,18 +108,18 @@ async function createCheckContentDirectory(contentFiles) {
return checkContent
}
function isApiVersioned(version) {
function isApiVersioned(version: string): boolean {
return allVersions[version] && allVersions[version].apiVersions.length > 0
}
function getOnlyApiVersions(version) {
function getOnlyApiVersions(version: string): string[] {
return allVersions[version].apiVersions.map(
(apiVersion) => `${allVersions[version].version}.${apiVersion}`,
)
}
function createCheckObj() {
const versions = {}
function createCheckObj(): CheckObject {
const versions: CheckObject = {}
Object.keys(allVersions).forEach((version) => {
isApiVersioned(version)
? getOnlyApiVersions(version).forEach((apiVersion) => (versions[apiVersion] = {}))
@@ -125,8 +129,11 @@ function createCheckObj() {
return versions
}
function getDifferences(openAPISchemaCheck, contentCheck) {
const differences = {}
function getDifferences(
openAPISchemaCheck: CheckObject,
contentCheck: CheckObject,
): DifferenceResult {
const differences: DifferenceResult = {}
for (const version in openAPISchemaCheck) {
const diffOpenApiContent = difference(openAPISchemaCheck[version], contentCheck[version])
if (Object.keys(diffOpenApiContent).length > 0) differences[version] = diffOpenApiContent
@@ -135,7 +142,7 @@ function getDifferences(openAPISchemaCheck, contentCheck) {
return differences
}
function difference(obj1, obj2) {
function difference(obj1: Record<string, string[]>, obj2: Record<string, string[]>): string[] {
const diff = Object.keys(obj1).reduce((result, key) => {
if (!Object.prototype.hasOwnProperty.call(obj2, key)) {
result.push(key)
@@ -149,7 +156,7 @@ function difference(obj1, obj2) {
return diff
}
export function getAutomatedMarkdownFiles(rootDir) {
export function getAutomatedMarkdownFiles(rootDir: string): string[] {
return walkFiles(rootDir, '.md')
.filter((file) => !file.includes('index.md'))
.filter((file) => !nonAutomatedRestPaths.some((path) => file.includes(path)))

View File

@@ -3,6 +3,7 @@ import type { Failbot } from '@github/failbot'
import type enterpriseServerReleases from '@/versions/lib/enterprise-server-releases.d'
import type { ValidOcticon } from '@/landings/types'
import type { MiniTocItem } from '@/frame/lib/get-mini-toc-items'
// Shared type for resolved article information used across landing pages and carousels
export interface ResolvedArticle {
@@ -180,7 +181,7 @@ export type Context = {
featuredLinks?: FeaturedLinksExpanded
currentLearningTrack?: LearningTrack | null
renderedPage?: string
miniTocItems?: string | undefined
miniTocItems?: MiniTocItem[]
markdownRequested?: boolean
}
export type LearningTracks = {