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