From a79730e26839a676ee40b51c08d216bf82907559 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Tue, 14 Oct 2025 13:50:09 -0700 Subject: [PATCH] Migrate 10 JavaScript files to TypeScript (#57971) --- ...alidation.js => frontmatter-validation.ts} | 76 ++++++++++++++----- ...int-results.js => pretty-print-results.ts} | 40 +++++++--- .../tests/{annotate.js => annotate.ts} | 6 +- ...haned-features.js => orphaned-features.ts} | 0 .../lib/{create-tree.js => create-tree.ts} | 70 ++++++++++------- src/frame/tests/{pages.js => pages.ts} | 35 +++++---- ...ead-frontmatter.js => read-frontmatter.ts} | 23 +++--- .../utils/{operation.js => operation.ts} | 69 ++++++++++++----- .../{cli-examples.js => cli-examples.ts} | 29 ++++--- src/rest/tests/{rendering.js => rendering.ts} | 2 +- 10 files changed, 238 insertions(+), 112 deletions(-) rename src/content-linter/lib/linting-rules/{frontmatter-validation.js => frontmatter-validation.ts} (67%) rename src/content-linter/scripts/{pretty-print-results.js => pretty-print-results.ts} (81%) rename src/content-render/tests/{annotate.js => annotate.ts} (95%) rename src/data-directory/tests/{orphaned-features.js => orphaned-features.ts} (100%) rename src/frame/lib/{create-tree.js => create-tree.ts} (70%) rename src/frame/tests/{pages.js => pages.ts} (79%) rename src/frame/tests/{read-frontmatter.js => read-frontmatter.ts} (88%) rename src/rest/scripts/utils/{operation.js => operation.ts} (73%) rename src/rest/tests/{cli-examples.js => cli-examples.ts} (77%) rename src/rest/tests/{rendering.js => rendering.ts} (99%) diff --git a/src/content-linter/lib/linting-rules/frontmatter-validation.js b/src/content-linter/lib/linting-rules/frontmatter-validation.ts similarity index 67% rename from src/content-linter/lib/linting-rules/frontmatter-validation.js rename to src/content-linter/lib/linting-rules/frontmatter-validation.ts index 60fa08efb9..ff91c4b246 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-validation.js +++ b/src/content-linter/lib/linting-rules/frontmatter-validation.ts @@ -1,9 +1,27 @@ +// @ts-ignore - no types available for markdownlint-rule-helpers import { addError } from 'markdownlint-rule-helpers' import { getFrontmatter } from '@/content-linter/lib/helpers/utils' +import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' + +interface PropertyLimits { + max: number + recommended: number + required?: boolean +} + +interface ContentRules { + title: PropertyLimits + shortTitle: PropertyLimits + intro: PropertyLimits + requiredProperties: string[] +} + +type ContentType = 'category' | 'mapTopic' | 'article' | null + // Strip liquid tags from text for character counting purposes -function stripLiquidTags(text) { - if (typeof text !== 'string') return text +function stripLiquidTags(text: unknown): string { + if (typeof text !== 'string') return text as string // Remove both {% %} and {{ }} liquid tags return text.replace(/\{%.*?%\}/g, '').replace(/\{\{.*?\}\}/g, '') } @@ -13,15 +31,15 @@ export const frontmatterValidation = { description: 'Frontmatter properties must meet character limits and required property requirements', tags: ['frontmatter', 'character-limits', 'required-properties'], - function: (params, onError) => { - const fm = getFrontmatter(params.lines) + function: (params: RuleParams, onError: RuleErrorCallback) => { + const fm = getFrontmatter(params.lines as string[]) if (!fm) return // Detect content type based on frontmatter properties and file path const contentType = detectContentType(fm, params.name) // Define character limits and requirements for different content types - const contentRules = { + const contentRules: Record = { category: { title: { max: 70, recommended: 67 }, shortTitle: { max: 30, recommended: 27 }, @@ -42,7 +60,7 @@ export const frontmatterValidation = { }, } - const rules = contentRules[contentType] + const rules = contentType ? contentRules[contentType] : null if (!rules) return // Check required properties @@ -61,14 +79,21 @@ export const frontmatterValidation = { // Check title length if (fm.title) { - validatePropertyLength(onError, params.lines, 'title', fm.title, rules.title, 'Title') + validatePropertyLength( + onError, + params.lines as string[], + 'title', + fm.title, + rules.title, + 'Title', + ) } // Check shortTitle length if (fm.shortTitle) { validatePropertyLength( onError, - params.lines, + params.lines as string[], 'shortTitle', fm.shortTitle, rules.shortTitle, @@ -78,17 +103,24 @@ export const frontmatterValidation = { // Check intro length if it exists if (fm.intro && rules.intro) { - validatePropertyLength(onError, params.lines, 'intro', fm.intro, rules.intro, 'Intro') + validatePropertyLength( + onError, + params.lines as string[], + 'intro', + fm.intro, + rules.intro, + 'Intro', + ) } // Cross-property validation: if title is longer than shortTitle limit, shortTitle must exist const strippedTitle = stripLiquidTags(fm.title) - if (fm.title && strippedTitle.length > rules.shortTitle.max && !fm.shortTitle) { - const titleLine = findPropertyLine(params.lines, 'title') + if (fm.title && (strippedTitle as string).length > rules.shortTitle.max && !fm.shortTitle) { + const titleLine = findPropertyLine(params.lines as string[], 'title') addError( onError, titleLine, - `Title is ${strippedTitle.length} characters, which exceeds the shortTitle limit of ${rules.shortTitle.max} characters. A shortTitle must be provided.`, + `Title is ${(strippedTitle as string).length} characters, which exceeds the shortTitle limit of ${rules.shortTitle.max} characters. A shortTitle must be provided.`, fm.title, null, null, @@ -98,10 +130,10 @@ export const frontmatterValidation = { // Special validation for articles: should have at least one topic if (contentType === 'article' && fm.topics) { if (!Array.isArray(fm.topics)) { - const topicsLine = findPropertyLine(params.lines, 'topics') + const topicsLine = findPropertyLine(params.lines as string[], 'topics') addError(onError, topicsLine, 'Topics must be an array', String(fm.topics), null, null) } else if (fm.topics.length === 0) { - const topicsLine = findPropertyLine(params.lines, 'topics') + const topicsLine = findPropertyLine(params.lines as string[], 'topics') addError( onError, topicsLine, @@ -115,9 +147,16 @@ export const frontmatterValidation = { }, } -function validatePropertyLength(onError, lines, propertyName, propertyValue, limits, displayName) { +function validatePropertyLength( + onError: RuleErrorCallback, + lines: string[], + propertyName: string, + propertyValue: string, + limits: PropertyLimits, + displayName: string, +): void { const strippedValue = stripLiquidTags(propertyValue) - const propertyLength = strippedValue.length + const propertyLength = (strippedValue as string).length const propertyLine = findPropertyLine(lines, propertyName) // Only report the most severe error - maximum takes precedence over recommended @@ -142,7 +181,8 @@ function validatePropertyLength(onError, lines, propertyName, propertyValue, lim } } -function detectContentType(frontmatter, filePath) { +// frontmatter object structure varies based on YAML content, using any for flexibility +function detectContentType(frontmatter: any, filePath: string): ContentType { // Only apply validation to markdown files if (!filePath || !filePath.endsWith('.md')) { return null @@ -168,7 +208,7 @@ function detectContentType(frontmatter, filePath) { return 'article' } -function findPropertyLine(lines, property) { +function findPropertyLine(lines: string[], property: string): number { const line = lines.find((line) => line.trim().startsWith(`${property}:`)) return line ? lines.indexOf(line) + 1 : 1 } diff --git a/src/content-linter/scripts/pretty-print-results.js b/src/content-linter/scripts/pretty-print-results.ts similarity index 81% rename from src/content-linter/scripts/pretty-print-results.js rename to src/content-linter/scripts/pretty-print-results.ts index fb938cfcea..615fb812ef 100644 --- a/src/content-linter/scripts/pretty-print-results.js +++ b/src/content-linter/scripts/pretty-print-results.ts @@ -1,19 +1,36 @@ import chalk from 'chalk' -function isNumber(value) { +interface LintResult { + ruleDescription: string + ruleNames: string[] + lineNumber: number + columnNumber?: number + severity: string + errorDetail?: string + errorContext?: string + context?: string + fixable?: boolean +} + +type LintResults = Record + +function isNumber(value: unknown): value is number { return typeof value === 'number' && !isNaN(value) } -function shorten(text, length = 70) { +function shorten(text: string, length = 70): string { if (text.length <= length) return text return `${text.slice(0, length - 3)}…` } -export function prettyPrintResults(results, { fixed = false } = {}) { +export function prettyPrintResults( + results: LintResults, + { fixed = false }: { fixed?: boolean } = {}, +): void { const PREFIX_PADDING = ' '.repeat(4) const columnPadding = 'Description'.length // The longest column header word - function label(text, padding = columnPadding) { + function label(text: string, padding = columnPadding): string { if (padding < text.length) throw new Error('Padding must be greater than text length') return `${PREFIX_PADDING}${chalk.dim(text.padEnd(padding))}` } @@ -114,7 +131,8 @@ export function prettyPrintResults(results, { fixed = false } = {}) { } } -function chalkFunColors(text) { +function chalkFunColors(text: string): string { + // Valid chalk color method names for terminal output const colors = [ 'red', 'yellow', @@ -126,19 +144,21 @@ function chalkFunColors(text) { 'greenBright', 'magentaBright', 'cyanBright', - ].sort(() => Math.random() - 0.5) + ] as const + const shuffledColors = [...colors].sort(() => Math.random() - 0.5) let colorIndex = 0 return text .split('') .map((char) => { - const color = colors[colorIndex] - colorIndex = (colorIndex + 1) % colors.length - return chalk[color](char) + const color = shuffledColors[colorIndex] + colorIndex = (colorIndex + 1) % shuffledColors.length + // Chalk's TypeScript types don't support dynamic property access, but these are valid color methods + return (chalk as any)[color](char) }) .join('') } -function indentWrappedString(str, startingIndent) { +function indentWrappedString(str: string, startingIndent: number): string { const NEW_LINE_PADDING = ' '.repeat(16) const width = process.stdout.columns || 80 // Use terminal width, default to 80 if not available let indentedString = '' diff --git a/src/content-render/tests/annotate.js b/src/content-render/tests/annotate.ts similarity index 95% rename from src/content-render/tests/annotate.js rename to src/content-render/tests/annotate.ts index b1fd79020e..bd1b3be982 100644 --- a/src/content-render/tests/annotate.js +++ b/src/content-render/tests/annotate.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest' import cheerio from 'cheerio' import { renderContent } from '@/content-render/index' +import type { Context } from '@/types' const example = ` \`\`\`yaml annotate @@ -131,7 +132,7 @@ on: [push] ` // Create a mock context with pages for AUTOTITLE resolution - const mockPages = { + const mockPages: Record = { '/get-started/start-your-journey/hello-world': { href: '/get-started/start-your-journey/hello-world', rawTitle: 'Hello World', @@ -147,7 +148,8 @@ on: [push] currentVersion: 'free-pro-team@latest', pages: mockPages, redirects: {}, - } + // Mock test object doesn't need all Context properties, using 'as unknown as' to bypass strict type checking + } as unknown as Context const res = await renderContent(example, mockContext) const $ = cheerio.load(res) diff --git a/src/data-directory/tests/orphaned-features.js b/src/data-directory/tests/orphaned-features.ts similarity index 100% rename from src/data-directory/tests/orphaned-features.js rename to src/data-directory/tests/orphaned-features.ts diff --git a/src/frame/lib/create-tree.js b/src/frame/lib/create-tree.ts similarity index 70% rename from src/frame/lib/create-tree.js rename to src/frame/lib/create-tree.ts index f90c778e03..8f47312a9d 100644 --- a/src/frame/lib/create-tree.js +++ b/src/frame/lib/create-tree.ts @@ -1,16 +1,21 @@ import path from 'path' import fs from 'fs/promises' -import Page from './page' +import PageClass from './page' +import type { UnversionedTree, Page } from '@/types' -export default async function createTree(originalPath, rootPath, previousTree) { +export default async function createTree( + originalPath: string, + rootPath?: string, + previousTree?: UnversionedTree, +): Promise { const basePath = rootPath || originalPath // On recursive runs, this is processing page.children items in `/` format. // If the path exists as is, assume this is a directory with a child index.md. // Otherwise, assume it's a child .md file and add `.md` to the path. - let filepath - let mtime + let filepath: string + let mtime: number // This kills two birds with one stone. We (attempt to) read it as a file, // to find out if it's a directory or a file and whence we know that // we also collect it's modification time. @@ -18,7 +23,7 @@ export default async function createTree(originalPath, rootPath, previousTree) { filepath = `${originalPath}.md` mtime = await getMtime(filepath) } catch (error) { - if (error.code !== 'ENOENT') { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error } filepath = `${originalPath}/index.md` @@ -30,7 +35,7 @@ export default async function createTree(originalPath, rootPath, previousTree) { try { mtime = await getMtime(filepath) } catch (error) { - if (error.code !== 'ENOENT') { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error } // Throw an error if we can't find a content file associated with the children: entry. @@ -51,7 +56,7 @@ export default async function createTree(originalPath, rootPath, previousTree) { // Reading in a file from disk is slow and best avoided if we can be // certain it isn't necessary. If the previous tree is known and that // tree's page node's `mtime` hasn't changed, we can use that instead. - let page + let page: Page if (previousTree && previousTree.page.mtime === mtime) { // A save! We can use the same exact Page instance from the previous // tree because the assumption is that since the `.md` file it was @@ -61,20 +66,22 @@ export default async function createTree(originalPath, rootPath, previousTree) { } else { // Either the previous tree doesn't exist yet or the modification time // of the file on disk has changed. - page = await Page.init({ + const newPage = await PageClass.init({ basePath, relativePath, languageCode: 'en', mtime, - }) - } - - if (!page) { - throw Error(`Cannot initialize page for ${filepath}`) + // PageInitOptions doesn't include mtime in its type definition, but PageReadResult uses `& any` + // which allows additional properties to be passed through to the Page constructor + } as any) + if (!newPage) { + throw Error(`Cannot initialize page for ${filepath}`) + } + page = newPage as unknown as Page } // Create the root tree object on the first run, and create children recursively. - const item = { + const item: UnversionedTree = { page, // This is only here for the sake of reloading the tree later which // only happens in development mode. @@ -86,18 +93,23 @@ export default async function createTree(originalPath, rootPath, previousTree) { // this value now will be different from what it was before. // It's not enough to rely on *length* of the array before and after // because the change could have been to remove one and add another. - children: page.children, + // Page class has dynamic frontmatter properties like 'children' that aren't in the type definition + children: (page as any).children || [], + childPages: [], } // Process frontmatter children recursively. - if (item.page.children) { - assertUniqueChildren(item.page) + // Page class has dynamic frontmatter properties like 'children' that aren't in the type definition + if ((page as any).children) { + assertUniqueChildren(page as any) item.childPages = ( await Promise.all( - item.page.children.map(async (child, i) => { - let childPreviousTree + // Page class has dynamic frontmatter properties like 'children' that aren't in the type definition + ((page as any).children as string[]).map(async (child: string, i: number) => { + let childPreviousTree: UnversionedTree | undefined if (previousTree && previousTree.childPages) { - if (equalArray(item.page.children, previousTree.children)) { + // Page class has dynamic frontmatter properties like 'children' that aren't in the type definition + if (equalArray((page as any).children, previousTree.children)) { // We can only safely rely on picking the same "n'th" item // from the array if we're confident the names are the same // as they were before. @@ -119,22 +131,25 @@ export default async function createTree(originalPath, rootPath, previousTree) { // (early exit instead of returning a tree). So let's // mutate the `page.children` so we can benefit from the // ability to reload the site tree on consecutive requests. - item.page.children = item.page.children.filter((c) => c !== child) + // Page class has dynamic frontmatter properties like 'children' that aren't in the type definition + ;(page as any).children = ((page as any).children as string[]).filter( + (c: string) => c !== child, + ) } return subTree }), ) - ).filter(Boolean) + ).filter((tree): tree is UnversionedTree => tree !== undefined) } return item } -function equalArray(arr1, arr2) { +function equalArray(arr1: string[], arr2: string[]): boolean { return arr1.length === arr2.length && arr1.every((value, i) => value === arr2[i]) } -async function getMtime(filePath) { +async function getMtime(filePath: string): Promise { // Use mtimeMs, which is a regular floating point number, instead of the // mtime which is a Date based on that same number. // Otherwise, if we use the Date instances, we have to compare @@ -150,10 +165,11 @@ async function getMtime(filePath) { return Math.round(mtimeMs) } -function assertUniqueChildren(page) { +// Page class has dynamic frontmatter properties that aren't in the type definition +function assertUniqueChildren(page: any): void { if (page.children.length !== new Set(page.children).size) { - const count = {} - page.children.forEach((entry) => (count[entry] = 1 + (count[entry] || 0))) + const count: Record = {} + page.children.forEach((entry: string) => (count[entry] = 1 + (count[entry] || 0))) let msg = `${page.relativePath} has duplicates in the 'children' key.` for (const [entry, times] of Object.entries(count)) { if (times > 1) msg += ` '${entry}' is repeated ${times} times. ` diff --git a/src/frame/tests/pages.js b/src/frame/tests/pages.ts similarity index 79% rename from src/frame/tests/pages.js rename to src/frame/tests/pages.ts index 1f3eee1cf5..76ed444e4b 100644 --- a/src/frame/tests/pages.js +++ b/src/frame/tests/pages.ts @@ -10,6 +10,7 @@ import libLanguages from '@/languages/lib/languages' import { liquid } from '@/content-render/index' import patterns from '@/frame/lib/patterns' import removeFPTFromPath from '@/versions/lib/remove-fpt-from-path' +import type { Page } from '@/types' const languageCodes = Object.keys(libLanguages) const slugger = new GithubSlugger() @@ -17,7 +18,7 @@ const slugger = new GithubSlugger() describe('pages module', () => { vi.setConfig({ testTimeout: 60 * 1000 }) - let pages + let pages: Page[] beforeAll(async () => { pages = await loadPages() @@ -50,29 +51,30 @@ describe('pages module', () => { const englishPages = chain(pages) .filter(['languageCode', 'en']) .filter('redirect_from') - .map((pages) => pick(pages, ['redirect_from', 'applicableVersions', 'fullPath'])) + .map((page) => pick(page, ['redirect_from', 'applicableVersions', 'fullPath'])) .value() // Map from redirect path to Set of file paths - const redirectToFiles = new Map() - const versionedRedirects = [] + const redirectToFiles = new Map>() + const versionedRedirects: Array<{ path: string; file: string }> = [] - englishPages.forEach((page) => { - page.redirect_from.forEach((redirect) => { - page.applicableVersions.forEach((version) => { + // Page objects have dynamic properties from chain/lodash that aren't fully typed + englishPages.forEach((page: any) => { + page.redirect_from.forEach((redirect: string) => { + page.applicableVersions.forEach((version: string) => { const versioned = removeFPTFromPath(path.posix.join('/', version, redirect)) versionedRedirects.push({ path: versioned, file: page.fullPath }) if (!redirectToFiles.has(versioned)) { - redirectToFiles.set(versioned, new Set()) + redirectToFiles.set(versioned, new Set()) } - redirectToFiles.get(versioned).add(page.fullPath) + redirectToFiles.get(versioned)!.add(page.fullPath) }) }) }) // Only consider as duplicate if more than one unique file defines the same redirect const duplicates = Array.from(redirectToFiles.entries()) - .filter(([_, files]) => files.size > 1) + .filter(([, files]) => files.size > 1) .map(([path]) => path) // Build a detailed message with sources for each duplicate @@ -96,7 +98,8 @@ describe('pages module', () => { return ( page.languageCode === 'en' && // only check English !page.relativePath.includes('index.md') && // ignore TOCs - !page.allowTitleToDifferFromFilename && // ignore docs with override + // Page class has dynamic frontmatter properties like 'allowTitleToDifferFromFilename' not in type definition + !(page as any).allowTitleToDifferFromFilename && // ignore docs with override slugger.slug(decode(page.title)) !== path.basename(page.relativePath, '.md') && slugger.slug(decode(page.shortTitle || '')) !== path.basename(page.relativePath, '.md') ) @@ -127,7 +130,8 @@ describe('pages module', () => { test('every page has valid frontmatter', async () => { const frontmatterErrors = chain(pages) // .filter(page => page.languageCode === 'en') - .map((page) => page.frontmatterErrors) + // Page class has dynamic error properties like 'frontmatterErrors' not in type definition + .map((page) => (page as any).frontmatterErrors) .filter(Boolean) .flatten() .value() @@ -141,17 +145,18 @@ describe('pages module', () => { }) test('every page has valid Liquid templating', async () => { - const liquidErrors = [] + const liquidErrors: Array<{ filename: string; error: string }> = [] for (const page of pages) { - const markdown = page.raw + // Page class has dynamic properties like 'raw' markdown not in type definition + const markdown = (page as any).raw if (!patterns.hasLiquid.test(markdown)) continue try { await liquid.parse(markdown) } catch (error) { liquidErrors.push({ filename: page.fullPath, - error: error.message, + error: (error as Error).message, }) } } diff --git a/src/frame/tests/read-frontmatter.js b/src/frame/tests/read-frontmatter.ts similarity index 88% rename from src/frame/tests/read-frontmatter.js rename to src/frame/tests/read-frontmatter.ts index da0d0adb95..feda8abe96 100644 --- a/src/frame/tests/read-frontmatter.js +++ b/src/frame/tests/read-frontmatter.ts @@ -22,9 +22,9 @@ versions: describe('frontmatter', () => { test('parses frontmatter and content in a given string (no options required)', () => { const { data, content, errors } = parse(fixture1) - expect(data.title).toBe('Hello, World') - expect(data.meaning_of_life).toBe(42) - expect(content.trim()).toBe('I am content.') + expect(data!.title).toBe('Hello, World') + expect(data!.meaning_of_life).toBe(42) + expect(content!.trim()).toBe('I am content.') expect(errors.length).toBe(0) }) @@ -85,9 +85,9 @@ I am content. } const { data, content, errors } = parse(fixture1, { schema }) - expect(data.title).toBe('Hello, World') - expect(data.meaning_of_life).toBe(42) - expect(content.trim()).toBe('I am content.') + expect(data!.title).toBe('Hello, World') + expect(data!.meaning_of_life).toBe(42) + expect(content!.trim()).toBe('I am content.') expect(errors.length).toBe(0) }) @@ -102,9 +102,9 @@ I am content. } const { data, content, errors } = parse(fixture1, { schema }) - expect(data.title).toBe('Hello, World') - expect(data.meaning_of_life).toBe(42) - expect(content.trim()).toBe('I am content.') + expect(data!.title).toBe('Hello, World') + expect(data!.meaning_of_life).toBe(42) + expect(content!.trim()).toBe('I am content.') expect(errors.length).toBe(1) const expectedError = { instancePath: '/meaning_of_life', @@ -121,7 +121,10 @@ I am content. test('creates errors if versions frontmatter does not match semver format', () => { const schema = { type: 'object', required: ['versions'], properties: {} } - schema.properties.versions = Object.assign({}, frontmatterSchema.properties.versions) + ;(schema.properties as any).versions = Object.assign( + {}, + (frontmatterSchema.properties as any).versions, + ) const { errors } = parse(fixture2, { schema }) const expectedError = { diff --git a/src/rest/scripts/utils/operation.js b/src/rest/scripts/utils/operation.ts similarity index 73% rename from src/rest/scripts/utils/operation.js rename to src/rest/scripts/utils/operation.ts index 1f493b5001..2dcc78b1aa 100644 --- a/src/rest/scripts/utils/operation.js +++ b/src/rest/scripts/utils/operation.ts @@ -1,6 +1,8 @@ +// @ts-ignore - no types available import httpStatusCodes from 'http-status-code' import { get, isPlainObject } from 'lodash-es' import { parseTemplate } from 'url-template' +// @ts-ignore - no types available import mergeAllOf from 'json-schema-merge-allof' import { renderContent } from './render-content' @@ -10,19 +12,41 @@ import { validateJson } from '@/tests/lib/validate-json-schema' import { getBodyParams } from './get-body-params' export default class Operation { - #operation - constructor(verb, requestPath, operation, globalServers) { + // OpenAPI operation object - schema is dynamic and varies by endpoint + #operation: any + serverUrl: string + verb: string + requestPath: string + title: string + category: string + subcategory: string + // OpenAPI parameters vary by endpoint, no fixed schema available + parameters: any[] + // Body parameters are dynamically generated from OpenAPI schema + bodyParameters: any[] + descriptionHTML?: string + // Code examples structure varies by language and endpoint + codeExamples?: any[] + // Status codes are dynamically generated from OpenAPI responses + statusCodes?: any[] + previews?: any[] + // Programmatic access data structure varies by operation + progAccess?: any + + // OpenAPI operation and globalServers objects have dynamic schema + constructor(verb: string, requestPath: string, operation: any, globalServers?: any[]) { this.#operation = operation // The global server object sets metadata including the base url for // all operations in a version. Individual operations can override // the global server url at the operation level. - this.serverUrl = operation.servers ? operation.servers[0].url : globalServers[0].url + this.serverUrl = operation.servers ? operation.servers[0].url : globalServers?.[0]?.url const serverVariables = operation.servers ? operation.servers[0].variables - : globalServers[0].variables + : globalServers?.[0]?.variables if (serverVariables) { - const templateVariables = {} + // Template variables structure comes from OpenAPI server variables + const templateVariables: Record = {} Object.keys(serverVariables).forEach( (key) => (templateVariables[key] = serverVariables[key].default), ) @@ -47,9 +71,10 @@ export default class Operation { return this } - async process(progAccessData) { + // Programmatic access data structure varies by operation and is not strongly typed + async process(progAccessData: any): Promise { await Promise.all([ - this.codeExamples(), + this.renderCodeExamples(), this.renderDescription(), this.renderStatusCodes(), this.renderParameterDescriptions(), @@ -65,7 +90,7 @@ export default class Operation { } } - async renderDescription() { + async renderDescription(): Promise { try { this.descriptionHTML = await renderContent(this.#operation.description) return this @@ -75,25 +100,27 @@ export default class Operation { } } - async codeExamples() { - this.codeExamples = await getCodeSamples(this.#operation) + async renderCodeExamples(): Promise { + const codeExamples = await getCodeSamples(this.#operation) try { - return await Promise.all( - this.codeExamples.map(async (codeExample) => { + this.codeExamples = await Promise.all( + // Code example structure varies by endpoint and language + codeExamples.map(async (codeExample: any) => { codeExample.response.description = await renderContent(codeExample.response.description) return codeExample }), ) + return this.codeExamples } catch (error) { console.error(error) throw new Error(`Error generating code examples for ${this.verb} ${this.requestPath}`) } } - async renderStatusCodes() { + async renderStatusCodes(): Promise { const responses = this.#operation.responses const responseKeys = Object.keys(responses) - if (responseKeys.length === 0) return [] + if (responseKeys.length === 0) return try { this.statusCodes = await Promise.all( @@ -121,7 +148,7 @@ export default class Operation { } } - async renderParameterDescriptions() { + async renderParameterDescriptions(): Promise { try { return Promise.all( this.parameters.map(async (param) => { @@ -135,8 +162,8 @@ export default class Operation { } } - async renderBodyParameterDescriptions() { - if (!this.#operation.requestBody) return [] + async renderBodyParameterDescriptions(): Promise { + if (!this.#operation.requestBody) return // There is currently only one operation with more than one content type // and the request body parameter types are the same for both. @@ -161,11 +188,12 @@ export default class Operation { } } - async renderPreviewNotes() { + async renderPreviewNotes(): Promise { const previews = get(this.#operation, 'x-github.previews', []) try { this.previews = await Promise.all( - previews.map(async (preview) => { + // Preview note structure from OpenAPI x-github extension is dynamic + previews.map(async (preview: any) => { const note = preview.note // remove extra leading and trailing newlines .replace(/```\n\n\n/gm, '```\n') @@ -186,7 +214,8 @@ export default class Operation { } } - programmaticAccess(progAccessData) { + // Programmatic access data structure varies by operation and is not strongly typed + programmaticAccess(progAccessData: any): void { this.progAccess = progAccessData[this.#operation.operationId] } } diff --git a/src/rest/tests/cli-examples.js b/src/rest/tests/cli-examples.ts similarity index 77% rename from src/rest/tests/cli-examples.js rename to src/rest/tests/cli-examples.ts index 94336f564a..2f3b99f844 100644 --- a/src/rest/tests/cli-examples.js +++ b/src/rest/tests/cli-examples.ts @@ -1,6 +1,8 @@ import { describe, expect, test } from 'vitest' import { getGHExample, getShellExample } from '../components/get-rest-code-samples' +import type { CodeSample, Operation } from '@/rest/components/types' +import type { VersionItem } from '@/frame/components/context/MainContext' describe('CLI examples generation', () => { const mockOperation = { @@ -9,14 +11,16 @@ describe('CLI examples generation', () => { serverUrl: 'https://api.github.com', subcategory: 'code-scanning', parameters: [], - } + // Partial mock object for testing - 'as unknown as' bypasses strict type checking for missing properties + } as unknown as Operation const mockVersions = { 'free-pro-team@latest': { apiVersions: ['2022-11-28'], latestApiVersion: '2022-11-28', }, - } + // Partial mock object for testing - 'as unknown as' bypasses strict type checking for missing properties + } as unknown as Record test('GitHub CLI example properly escapes contractions in string values', () => { const codeSample = { @@ -33,7 +37,8 @@ describe('CLI examples generation', () => { "This alert is not actually correct, because there's a sanitizer included in the library.", }, }, - } + // Partial mock object for testing - 'as unknown as' bypasses strict type checking for missing properties + } as unknown as CodeSample const result = getGHExample(mockOperation, codeSample, 'free-pro-team@latest', mockVersions) @@ -61,7 +66,8 @@ describe('CLI examples generation', () => { }, contentType: 'application/json', }, - } + // Partial mock object for testing - 'as unknown as' bypasses strict type checking for missing properties + } as unknown as CodeSample const result = getShellExample(mockOperation, codeSample, 'free-pro-team@latest', mockVersions) @@ -83,7 +89,8 @@ describe('CLI examples generation', () => { body: "It's not working because there's an issue and we can't fix it", }, }, - } + // Partial mock object for testing - 'as unknown as' bypasses strict type checking for missing properties + } as unknown as CodeSample const mockSimpleOperation = { verb: 'post', @@ -91,7 +98,8 @@ describe('CLI examples generation', () => { serverUrl: 'https://api.github.com', subcategory: 'issues', parameters: [], - } + // Partial mock object for testing - 'as unknown as' bypasses strict type checking for missing properties + } as unknown as Operation const result = getGHExample( mockSimpleOperation, @@ -121,7 +129,8 @@ describe('CLI examples generation', () => { 'This alert is not actually correct because there is a sanitizer included in the library.', }, }, - } + // Partial mock object for testing - 'as unknown as' bypasses strict type checking for missing properties + } as unknown as CodeSample const result = getGHExample(mockOperation, codeSample, 'free-pro-team@latest', mockVersions) @@ -143,7 +152,8 @@ describe('CLI examples generation', () => { }, contentType: 'application/x-www-form-urlencoded', }, - } + // Partial mock object for testing - 'as unknown as' bypasses strict type checking for missing properties + } as unknown as CodeSample const mockSimpleOperation = { verb: 'post', @@ -151,7 +161,8 @@ describe('CLI examples generation', () => { serverUrl: 'https://api.github.com', subcategory: 'pulls', parameters: [], - } + // Partial mock object for testing - 'as unknown as' bypasses strict type checking for missing properties + } as unknown as Operation const result = getShellExample( mockSimpleOperation, diff --git a/src/rest/tests/rendering.js b/src/rest/tests/rendering.ts similarity index 99% rename from src/rest/tests/rendering.js rename to src/rest/tests/rendering.ts index 6c078d6d60..56c9e85006 100644 --- a/src/rest/tests/rendering.js +++ b/src/rest/tests/rendering.ts @@ -145,7 +145,7 @@ describe('REST references docs', () => { }) }) -function formatErrors(differences) { +function formatErrors(differences: Record): string { let errorMessage = 'There are differences in Categories/Subcategories in:\n' for (const schema in differences) { errorMessage += 'Version: ' + schema + '\n'