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

Migrate 7 JavaScript files to TypeScript (#57816)

This commit is contained in:
Kevin Heis
2025-10-07 12:17:40 -07:00
committed by GitHub
parent 42240a66a4
commit 1f0df7449f
13 changed files with 207 additions and 100 deletions

View File

@@ -1,15 +1,26 @@
import { renderLiquid } from './liquid/index' import { renderLiquid } from './liquid/index'
import { renderMarkdown, renderUnified } from './unified/index' import { renderMarkdown, renderUnified } from './unified/index'
import { engine } from './liquid/engine' import { engine } from './liquid/engine'
import type { Context } from '@/types'
const globalCache = new Map() interface RenderOptions {
cache?: boolean | ((template: string, context: Context) => string)
filename?: string
textOnly?: boolean
}
const globalCache = new Map<string, string>()
// parse multiple times because some templates contain more templates. :] // parse multiple times because some templates contain more templates. :]
export async function renderContent(template = '', context = {}, options = {}) { export async function renderContent(
template = '',
context: Context = {} as Context,
options: RenderOptions = {},
): Promise<string> {
// If called with a falsy template, it can't ever become something // If called with a falsy template, it can't ever become something
// when rendered. We can exit early to save some pointless work. // when rendered. We can exit early to save some pointless work.
if (!template) return template if (!template) return template
let cacheKey = null let cacheKey: string | null = null
if (options && options.cache) { if (options && options.cache) {
if (!context) throw new Error("If setting 'cache' in options, the 'context' must be set too") if (!context) throw new Error("If setting 'cache' in options, the 'context' must be set too")
if (typeof options.cache === 'function') { if (typeof options.cache === 'function') {
@@ -21,13 +32,13 @@ export async function renderContent(template = '', context = {}, options = {}) {
throw new Error('cache option must return a string if truthy') throw new Error('cache option must return a string if truthy')
} }
if (globalCache.has(cacheKey)) { if (globalCache.has(cacheKey)) {
return globalCache.get(cacheKey) return globalCache.get(cacheKey) as string
} }
} }
try { try {
template = await renderLiquid(template, context) template = await renderLiquid(template, context)
if (context.markdownRequested) { if (context.markdownRequested) {
const md = await renderMarkdown(template, context, options) const md = await renderMarkdown(template, context)
return md return md
} }
@@ -45,7 +56,7 @@ export async function renderContent(template = '', context = {}, options = {}) {
} }
} }
function getDefaultCacheKey(template, context) { function getDefaultCacheKey(template: string, context: Context): string {
return `${template}:${context.currentVersion}:${context.currentLanguage}` return `${template}:${context.currentVersion}:${context.currentLanguage}`
} }

View File

@@ -1,54 +0,0 @@
import { Liquid } from 'liquidjs'
import GithubSlugger from 'github-slugger'
import IndentedDataReference from './indented-data-reference'
import Data from './data'
import Octicon from './octicon'
import Ifversion from './ifversion'
import { Tool, tags as toolTags } from './tool'
import { Spotlight, tags as spotlightTags } from './spotlight'
import { Prompt } from './prompt'
export const engine = new Liquid({
extname: '.html',
dynamicPartials: false,
})
engine.registerTag('indented_data_reference', IndentedDataReference)
engine.registerTag('data', Data)
engine.registerTag('octicon', Octicon)
engine.registerTag('ifversion', Ifversion)
for (const tag of toolTags) {
engine.registerTag(tag, Tool)
}
for (const tag in spotlightTags) {
engine.registerTag(tag, Spotlight)
}
engine.registerTag('prompt', Prompt)
/**
* Like the `size` filter, but specifically for
* getting the number of keys in an object
*/
engine.registerFilter('obj_size', (input) => {
if (!input) return 0
return Object.keys(input).length
})
/**
* Returns the version number of a GHES version string
* ex: enterprise-server@2.22 => 2.22
*/
engine.registerFilter('version_num', (input) => {
return input.split('@')[1]
})
/**
* Convert the input to a slug
*/
engine.registerFilter('slugify', (input) => {
const slugger = new GithubSlugger()
return slugger.slug(input)
})

View File

@@ -0,0 +1,66 @@
import { Liquid } from 'liquidjs'
import GithubSlugger from 'github-slugger'
// Copilot: When converting data.js to TypeScript, remove the 'as any' assertion for anyData below
import Data from './data'
import Octicon from './octicon'
// Copilot: When converting ifversion.js to TypeScript, remove the 'as any' assertion for anyIfversion below
import Ifversion from './ifversion'
// Copilot: When converting tool.js to TypeScript, remove the 'as any' assertion for anyTool below
import { Tool, tags as toolTags } from './tool'
import { Spotlight, tags as spotlightTags } from './spotlight'
import { Prompt } from './prompt'
import IndentedDataReference from './indented-data-reference'
// Type assertions for .js files without type definitions
// Copilot: Remove these assertions when the corresponding .js files are converted to TypeScript
const anyData = Data as any
const anyIfversion = Ifversion as any
const anyTool = Tool as any
const anySpotlight = Spotlight as any
const anyPrompt = Prompt as any
const anyIndentedDataReference = IndentedDataReference as any
export const engine = new Liquid({
extname: '.html',
dynamicPartials: false,
})
engine.registerTag('indented_data_reference', anyIndentedDataReference)
engine.registerTag('data', anyData)
engine.registerTag('octicon', Octicon)
engine.registerTag('ifversion', anyIfversion)
for (const tag of toolTags) {
engine.registerTag(tag, anyTool)
}
for (const tag in spotlightTags) {
engine.registerTag(tag, anySpotlight)
}
engine.registerTag('prompt', anyPrompt)
/**
* Like the `size` filter, but specifically for
* getting the number of keys in an object
*/
engine.registerFilter('obj_size', (input: Record<string, unknown> | null | undefined): number => {
if (!input) return 0
return Object.keys(input).length
})
/**
* Returns the version number of a GHES version string
* ex: enterprise-server@2.22 => 2.22
*/
engine.registerFilter('version_num', (input: string): string => {
return input.split('@')[1]
})
/**
* Convert the input to a slug
*/
engine.registerFilter('slugify', (input: string): string => {
const slugger = new GithubSlugger()
return slugger.slug(input)
})

View File

@@ -3,6 +3,21 @@ import assert from 'assert'
import { THROW_ON_EMPTY, IndentedDataReferenceError } from './error-handling' import { THROW_ON_EMPTY, IndentedDataReferenceError } from './error-handling'
import { getDataByLanguage } from '@/data-directory/lib/get-data' import { getDataByLanguage } from '@/data-directory/lib/get-data'
// Note: Using 'any' for liquidjs-related types because liquidjs doesn't provide comprehensive TypeScript definitions
interface LiquidTag {
markup: string
liquid: any
parse(tagToken: any): void
render(scope: any): Promise<string | undefined>
}
interface LiquidScope {
environments: {
currentLanguage: string
[key: string]: any
}
}
// This class supports a tag that expects two parameters, a data reference and `spaces=NUMBER`: // This class supports a tag that expects two parameters, a data reference and `spaces=NUMBER`:
// //
// {% indented_data_reference foo.bar spaces=NUMBER %} // {% indented_data_reference foo.bar spaces=NUMBER %}
@@ -13,12 +28,15 @@ import { getDataByLanguage } from '@/data-directory/lib/get-data'
// reference is used inside a block element (like a list or nested list) without // reference is used inside a block element (like a list or nested list) without
// affecting the formatting when the reference is used elsewhere via {{ site.data.foo.bar }}. // affecting the formatting when the reference is used elsewhere via {{ site.data.foo.bar }}.
export default { const IndentedDataReference: LiquidTag = {
parse(tagToken) { markup: '',
liquid: null as any,
parse(tagToken: any): void {
this.markup = tagToken.args.trim() this.markup = tagToken.args.trim()
}, },
async render(scope) { async render(scope: LiquidScope): Promise<string | undefined> {
// obfuscate first legit space, remove all other spaces, then restore legit space // obfuscate first legit space, remove all other spaces, then restore legit space
// this way we can support spaces=NUMBER as well as spaces = NUMBER // this way we can support spaces=NUMBER as well as spaces = NUMBER
const input = this.markup const input = this.markup
@@ -29,12 +47,15 @@ export default {
const [dataReference, spaces] = input.split(' ') const [dataReference, spaces] = input.split(' ')
// if no spaces are specified, default to 2 // if no spaces are specified, default to 2
const numSpaces = spaces ? spaces.replace(/spaces=/, '') : '2' const numSpaces: string = spaces ? spaces.replace(/spaces=/, '') : '2'
assert(parseInt(numSpaces) || numSpaces === '0', '"spaces=NUMBER" must include a number') assert(parseInt(numSpaces) || numSpaces === '0', '"spaces=NUMBER" must include a number')
// Get the referenced value from the context // Get the referenced value from the context
const text = getDataByLanguage(dataReference, scope.environments.currentLanguage) const text: string | undefined = getDataByLanguage(
dataReference,
scope.environments.currentLanguage,
)
if (text === undefined) { if (text === undefined) {
if (scope.environments.currentLanguage === 'en') { if (scope.environments.currentLanguage === 'en') {
const message = `Can't find the key 'indented_data_reference ${dataReference}' in the scope.` const message = `Can't find the key 'indented_data_reference ${dataReference}' in the scope.`
@@ -47,8 +68,10 @@ export default {
} }
// add spaces to each line // add spaces to each line
const renderedReferenceWithIndent = text.replace(/^/gm, ' '.repeat(numSpaces)) const renderedReferenceWithIndent: string = text.replace(/^/gm, ' '.repeat(parseInt(numSpaces)))
return this.liquid.parseAndRender(renderedReferenceWithIndent, scope.environments) return this.liquid.parseAndRender(renderedReferenceWithIndent, scope.environments)
}, },
} }
export default IndentedDataReference

View File

@@ -1,6 +1,22 @@
import { TokenizationError } from 'liquidjs' import { TokenizationError } from 'liquidjs'
// @ts-ignore - @primer/octicons doesn't provide TypeScript declarations
import octicons from '@primer/octicons' import octicons from '@primer/octicons'
// Note: Using 'any' for liquidjs-related types because liquidjs doesn't provide comprehensive TypeScript definitions
interface LiquidTag {
icon: string
options: Record<string, string>
parse(tagToken: any): void
render(): Promise<string>
}
interface OcticonsMatch {
groups: {
icon: string
options?: string
}
}
const OptionsSyntax = /([a-zA-Z-]+)="([\w\s-]+)"*/g const OptionsSyntax = /([a-zA-Z-]+)="([\w\s-]+)"*/g
const Syntax = new RegExp('"(?<icon>[a-zA-Z-]+)"(?<options>(?:\\s' + OptionsSyntax.source + ')*)') const Syntax = new RegExp('"(?<icon>[a-zA-Z-]+)"(?<options>(?:\\s' + OptionsSyntax.source + ')*)')
const SyntaxHelp = 'Syntax Error in tag \'octicon\' - Valid syntax: octicon "<name>" <key="value">' const SyntaxHelp = 'Syntax Error in tag \'octicon\' - Valid syntax: octicon "<name>" <key="value">'
@@ -12,9 +28,12 @@ const SyntaxHelp = 'Syntax Error in tag \'octicon\' - Valid syntax: octicon "<na
* {% octicon "check" %} * {% octicon "check" %}
* {% octicon "check" width="64" aria-label="Example label" %} * {% octicon "check" width="64" aria-label="Example label" %}
*/ */
export default { const Octicon: LiquidTag = {
parse(tagToken) { icon: '',
const match = tagToken.args.match(Syntax) options: {},
parse(tagToken: any): void {
const match: OcticonsMatch | null = tagToken.args.match(Syntax)
if (!match) { if (!match) {
throw new TokenizationError(SyntaxHelp, tagToken) throw new TokenizationError(SyntaxHelp, tagToken)
} }
@@ -32,7 +51,7 @@ export default {
// Memoize any options passed // Memoize any options passed
if (match.groups.options) { if (match.groups.options) {
let optionsMatch let optionsMatch: RegExpExecArray | null
// Loop through each option matching the OptionsSyntax regex // Loop through each option matching the OptionsSyntax regex
while ((optionsMatch = OptionsSyntax.exec(match.groups.options))) { while ((optionsMatch = OptionsSyntax.exec(match.groups.options))) {
@@ -46,13 +65,15 @@ export default {
} }
}, },
async render() { async render(): Promise<string> {
// Throw an error if the requested octicon does not exist. // Throw an error if the requested octicon does not exist.
if (!Object.prototype.hasOwnProperty.call(octicons, this.icon)) { if (!Object.prototype.hasOwnProperty.call(octicons, this.icon)) {
throw new Error(`Octicon ${this.icon} does not exist`) throw new Error(`Octicon ${this.icon} does not exist`)
} }
const result = octicons[this.icon].toSVG(this.options) const result: string = octicons[this.icon].toSVG(this.options)
return result return result
}, },
} }
export default Octicon

View File

@@ -5,18 +5,31 @@ import { readCompressedJsonFileFallback } from '@/frame/lib/read-json-file'
import { getOpenApiVersion } from '@/versions/lib/all-versions' import { getOpenApiVersion } from '@/versions/lib/all-versions'
import { categoriesWithoutSubcategories } from '../../rest/lib/index' import { categoriesWithoutSubcategories } from '../../rest/lib/index'
interface AppsConfig {
pages: Record<string, unknown>
}
// Note: Using 'any' for AppsData to maintain compatibility with existing consumers that expect different shapes
type AppsData = any
const ENABLED_APPS_DIR = 'src/github-apps/data' const ENABLED_APPS_DIR = 'src/github-apps/data'
const githubAppsData = new Map() const githubAppsData = new Map<string, Map<string, AppsData>>()
// Initialize the Map with the page type keys listed under `pages` // Initialize the Map with the page type keys listed under `pages`
// in the config.json file. // in the config.json file.
const appsDataConfig = JSON.parse(fs.readFileSync('src/github-apps/lib/config.json', 'utf8')) const appsDataConfig: AppsConfig = JSON.parse(
fs.readFileSync('src/github-apps/lib/config.json', 'utf8'),
)
for (const pageType of Object.keys(appsDataConfig.pages)) { for (const pageType of Object.keys(appsDataConfig.pages)) {
githubAppsData.set(pageType, new Map()) githubAppsData.set(pageType, new Map<string, AppsData>())
} }
export async function getAppsData(pageType, docsVersion, apiVersion) { export async function getAppsData(
const pageTypeMap = githubAppsData.get(pageType) pageType: string,
docsVersion: string,
apiVersion?: string,
): Promise<AppsData> {
const pageTypeMap = githubAppsData.get(pageType)!
const filename = `${pageType}.json` const filename = `${pageType}.json`
const openApiVersion = getOpenApiVersion(docsVersion) + (apiVersion ? `-${apiVersion}` : '') const openApiVersion = getOpenApiVersion(docsVersion) + (apiVersion ? `-${apiVersion}` : '')
if (!pageTypeMap.has(openApiVersion)) { if (!pageTypeMap.has(openApiVersion)) {
@@ -27,26 +40,34 @@ export async function getAppsData(pageType, docsVersion, apiVersion) {
pageTypeMap.set(openApiVersion, data) pageTypeMap.set(openApiVersion, data)
} }
return pageTypeMap.get(openApiVersion) return pageTypeMap.get(openApiVersion)!
} }
export async function getAppsServerSideProps(context, pageType, { useDisplayTitle = false }) { export async function getAppsServerSideProps(
context: any,
pageType: string,
{ useDisplayTitle = false }: { useDisplayTitle?: boolean },
): Promise<{
currentVersion: string
appsItems: AppsData
categoriesWithoutSubcategories: string[]
}> {
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items') const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items')
const { getAutomatedPageContextFromRequest } = await import( const { getAutomatedPageContextFromRequest } = await import(
'@/automated-pipelines/components/AutomatedPageContext' '@/automated-pipelines/components/AutomatedPageContext'
) )
const currentVersion = context.query.versionId const currentVersion: string = context.query.versionId
const allVersions = context.req.context.allVersions const allVersions = context.req.context.allVersions
const queryApiVersion = context.query.apiVersion const queryApiVersion: string = context.query.apiVersion
const apiVersion = allVersions[currentVersion].apiVersions.includes(queryApiVersion) const apiVersion: string = allVersions[currentVersion].apiVersions.includes(queryApiVersion)
? queryApiVersion ? queryApiVersion
: allVersions[currentVersion].latestApiVersion : allVersions[currentVersion].latestApiVersion
const appsItems = await getAppsData(pageType, currentVersion, apiVersion) const appsItems: AppsData = await getAppsData(pageType, currentVersion, apiVersion)
// Create minitoc // Create minitoc
const { miniTocItems } = getAutomatedPageContextFromRequest(context.req) const { miniTocItems } = getAutomatedPageContextFromRequest(context.req)
const titles = useDisplayTitle const titles: string[] = useDisplayTitle
? Object.values(appsItems).map((item) => item.displayTitle) ? Object.values(appsItems).map((item: any) => item.displayTitle!)
: Object.keys(appsItems) : Object.keys(appsItems)
const appMiniToc = await getAutomatedPageMiniTocItems(titles, context) const appMiniToc = await getAutomatedPageMiniTocItems(titles, context)
appMiniToc && miniTocItems.push(...appMiniToc) appMiniToc && miniTocItems.push(...appMiniToc)

View File

@@ -35,7 +35,7 @@ export default function FineGrainedTokenEndpoints({
} }
export const getServerSideProps: GetServerSideProps<Props> = async (context) => { export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { getAppsServerSideProps } = await import('@/github-apps/lib/index.js') const { getAppsServerSideProps } = await import('@/github-apps/lib/index')
const { currentVersion, appsItems, categoriesWithoutSubcategories } = const { currentVersion, appsItems, categoriesWithoutSubcategories } =
await getAppsServerSideProps(context, 'fine-grained-pat', { useDisplayTitle: false }) await getAppsServerSideProps(context, 'fine-grained-pat', { useDisplayTitle: false })

View File

@@ -35,7 +35,7 @@ export default function GitHubAppEndpoints({
} }
export const getServerSideProps: GetServerSideProps<Props> = async (context) => { export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { getAppsServerSideProps } = await import('@/github-apps/lib/index.js') const { getAppsServerSideProps } = await import('@/github-apps/lib/index')
const { currentVersion, appsItems, categoriesWithoutSubcategories } = const { currentVersion, appsItems, categoriesWithoutSubcategories } =
await getAppsServerSideProps(context, 'server-to-server-rest', { useDisplayTitle: false }) await getAppsServerSideProps(context, 'server-to-server-rest', { useDisplayTitle: false })

View File

@@ -35,7 +35,7 @@ export default function UserGitHubAppEndpoints({
} }
export const getServerSideProps: GetServerSideProps<Props> = async (context) => { export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { getAppsServerSideProps } = await import('@/github-apps/lib/index.js') const { getAppsServerSideProps } = await import('@/github-apps/lib/index')
const { currentVersion, appsItems, categoriesWithoutSubcategories } = const { currentVersion, appsItems, categoriesWithoutSubcategories } =
await getAppsServerSideProps(context, 'user-to-server-rest', { useDisplayTitle: false }) await getAppsServerSideProps(context, 'user-to-server-rest', { useDisplayTitle: false })

View File

@@ -36,7 +36,7 @@ export default function FineGrainedPatPermissions({
} }
export const getServerSideProps: GetServerSideProps<Props> = async (context) => { export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { getAppsServerSideProps } = await import('@/github-apps/lib/index.js') const { getAppsServerSideProps } = await import('@/github-apps/lib/index')
const { currentVersion, appsItems, categoriesWithoutSubcategories } = const { currentVersion, appsItems, categoriesWithoutSubcategories } =
await getAppsServerSideProps(context, 'fine-grained-pat-permissions', { useDisplayTitle: true }) await getAppsServerSideProps(context, 'fine-grained-pat-permissions', { useDisplayTitle: true })

View File

@@ -36,7 +36,7 @@ export default function GitHubAppPermissions({
} }
export const getServerSideProps: GetServerSideProps<Props> = async (context) => { export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { getAppsServerSideProps } = await import('@/github-apps/lib/index.js') const { getAppsServerSideProps } = await import('@/github-apps/lib/index')
const { currentVersion, appsItems, categoriesWithoutSubcategories } = const { currentVersion, appsItems, categoriesWithoutSubcategories } =
await getAppsServerSideProps(context, 'server-to-server-permissions', { useDisplayTitle: true }) await getAppsServerSideProps(context, 'server-to-server-permissions', { useDisplayTitle: true })

View File

@@ -2,7 +2,20 @@
// src/github-apps/data/fine-grained-pat-permissions.json // src/github-apps/data/fine-grained-pat-permissions.json
// and src/github-apps/data/server-to-server-permissions.json // and src/github-apps/data/server-to-server-permissions.json
const permissionObjects = { interface SchemaProperty {
type: string
enum?: string[]
description?: string
items?: object
}
interface Schema {
type: string
required?: string[]
properties: Record<string, SchemaProperty>
}
const permissionObjects: Schema = {
type: 'object', type: 'object',
required: ['access', 'category', 'subcategory', 'slug', 'verb', 'requestPath'], required: ['access', 'category', 'subcategory', 'slug', 'verb', 'requestPath'],
properties: { properties: {
@@ -32,7 +45,7 @@ const permissionObjects = {
}, },
} }
export default { const schema: Schema = {
type: 'object', type: 'object',
required: ['title', 'displayTitle', 'permissions'], required: ['title', 'displayTitle', 'permissions'],
properties: { properties: {
@@ -52,3 +65,5 @@ export default {
}, },
}, },
} }
export default schema

View File

@@ -10,6 +10,10 @@ import { globSync } from 'glob'
import { program } from 'commander' import { program } from 'commander'
import { createOperations, processOperations } from './utils/get-operations' import { createOperations, processOperations } from './utils/get-operations'
interface ProgramOptions {
files: string[]
}
program program
.description('Generate dereferenced OpenAPI and decorated schema files.') .description('Generate dereferenced OpenAPI and decorated schema files.')
.requiredOption( .requiredOption(
@@ -18,9 +22,9 @@ program
) )
.parse(process.argv) .parse(process.argv)
const filenames = program.opts().files const filenames: string[] = (program.opts() as ProgramOptions).files
const filesToCheck = filenames.flatMap((filename) => globSync(filename)) const filesToCheck: string[] = filenames.flatMap((filename: string) => globSync(filename))
if (filesToCheck.length) { if (filesToCheck.length) {
check(filesToCheck) check(filesToCheck)
@@ -29,22 +33,22 @@ if (filesToCheck.length) {
process.exit(1) process.exit(1)
} }
async function check(files) { async function check(files: string[]): Promise<void> {
console.log('Verifying OpenAPI files are valid with decorator') console.log('Verifying OpenAPI files are valid with decorator')
const documents = files.map((filename) => [ const documents: [string, any][] = files.map((filename: string) => [
filename, filename,
JSON.parse(fs.readFileSync(path.join(filename))), JSON.parse(fs.readFileSync(path.join(filename), 'utf8')),
]) ])
for (const [filename, schema] of documents) { for (const [filename, schema] of documents as [string, any][]) {
try { try {
// munge OpenAPI definitions object in an array of operations objects // munge OpenAPI definitions object in an array of operations objects
const operations = await createOperations(schema) const operations = await createOperations(schema)
// process each operation, asynchronously rendering markdown and stuff // process each operation, asynchronously rendering markdown and stuff
await processOperations(operations) await processOperations(operations, {})
console.log(`Successfully could decorate OpenAPI operations for document ${filename}`) console.log(`Successfully could decorate OpenAPI operations for document ${filename}`)
} catch (error) { } catch (error: unknown) {
console.error(error) console.error(error)
console.log( console.log(
`🐛 Whoops! It looks like the decorator script wasn't able to parse the dereferenced schema in file ${filename}. A recent change may not yet be supported by the decorator. Please reach out in the #docs-engineering slack channel for help.`, `🐛 Whoops! It looks like the decorator script wasn't able to parse the dereferenced schema in file ${filename}. A recent change may not yet be supported by the decorator. Please reach out in the #docs-engineering slack channel for help.`,