1
0
mirror of synced 2025-12-19 09:57:42 -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 { renderMarkdown, renderUnified } from './unified/index'
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. :]
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
// when rendered. We can exit early to save some pointless work.
if (!template) return template
let cacheKey = null
let cacheKey: string | null = null
if (options && options.cache) {
if (!context) throw new Error("If setting 'cache' in options, the 'context' must be set too")
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')
}
if (globalCache.has(cacheKey)) {
return globalCache.get(cacheKey)
return globalCache.get(cacheKey) as string
}
}
try {
template = await renderLiquid(template, context)
if (context.markdownRequested) {
const md = await renderMarkdown(template, context, options)
const md = await renderMarkdown(template, context)
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}`
}

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 { 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`:
//
// {% 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
// affecting the formatting when the reference is used elsewhere via {{ site.data.foo.bar }}.
export default {
parse(tagToken) {
const IndentedDataReference: LiquidTag = {
markup: '',
liquid: null as any,
parse(tagToken: any): void {
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
// this way we can support spaces=NUMBER as well as spaces = NUMBER
const input = this.markup
@@ -29,12 +47,15 @@ export default {
const [dataReference, spaces] = input.split(' ')
// 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')
// 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 (scope.environments.currentLanguage === 'en') {
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
const renderedReferenceWithIndent = text.replace(/^/gm, ' '.repeat(numSpaces))
const renderedReferenceWithIndent: string = text.replace(/^/gm, ' '.repeat(parseInt(numSpaces)))
return this.liquid.parseAndRender(renderedReferenceWithIndent, scope.environments)
},
}
export default IndentedDataReference

View File

@@ -1,6 +1,22 @@
import { TokenizationError } from 'liquidjs'
// @ts-ignore - @primer/octicons doesn't provide TypeScript declarations
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 Syntax = new RegExp('"(?<icon>[a-zA-Z-]+)"(?<options>(?:\\s' + OptionsSyntax.source + ')*)')
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" width="64" aria-label="Example label" %}
*/
export default {
parse(tagToken) {
const match = tagToken.args.match(Syntax)
const Octicon: LiquidTag = {
icon: '',
options: {},
parse(tagToken: any): void {
const match: OcticonsMatch | null = tagToken.args.match(Syntax)
if (!match) {
throw new TokenizationError(SyntaxHelp, tagToken)
}
@@ -32,7 +51,7 @@ export default {
// Memoize any options passed
if (match.groups.options) {
let optionsMatch
let optionsMatch: RegExpExecArray | null
// Loop through each option matching the OptionsSyntax regex
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.
if (!Object.prototype.hasOwnProperty.call(octicons, this.icon)) {
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
},
}
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 { 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 githubAppsData = new Map()
const githubAppsData = new Map<string, Map<string, AppsData>>()
// Initialize the Map with the page type keys listed under `pages`
// 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)) {
githubAppsData.set(pageType, new Map())
githubAppsData.set(pageType, new Map<string, AppsData>())
}
export async function getAppsData(pageType, docsVersion, apiVersion) {
const pageTypeMap = githubAppsData.get(pageType)
export async function getAppsData(
pageType: string,
docsVersion: string,
apiVersion?: string,
): Promise<AppsData> {
const pageTypeMap = githubAppsData.get(pageType)!
const filename = `${pageType}.json`
const openApiVersion = getOpenApiVersion(docsVersion) + (apiVersion ? `-${apiVersion}` : '')
if (!pageTypeMap.has(openApiVersion)) {
@@ -27,26 +40,34 @@ export async function getAppsData(pageType, docsVersion, apiVersion) {
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 { getAutomatedPageContextFromRequest } = await import(
'@/automated-pipelines/components/AutomatedPageContext'
)
const currentVersion = context.query.versionId
const currentVersion: string = context.query.versionId
const allVersions = context.req.context.allVersions
const queryApiVersion = context.query.apiVersion
const apiVersion = allVersions[currentVersion].apiVersions.includes(queryApiVersion)
const queryApiVersion: string = context.query.apiVersion
const apiVersion: string = allVersions[currentVersion].apiVersions.includes(queryApiVersion)
? queryApiVersion
: allVersions[currentVersion].latestApiVersion
const appsItems = await getAppsData(pageType, currentVersion, apiVersion)
const appsItems: AppsData = await getAppsData(pageType, currentVersion, apiVersion)
// Create minitoc
const { miniTocItems } = getAutomatedPageContextFromRequest(context.req)
const titles = useDisplayTitle
? Object.values(appsItems).map((item) => item.displayTitle)
const titles: string[] = useDisplayTitle
? Object.values(appsItems).map((item: any) => item.displayTitle!)
: Object.keys(appsItems)
const appMiniToc = await getAutomatedPageMiniTocItems(titles, context)
appMiniToc && miniTocItems.push(...appMiniToc)

View File

@@ -35,7 +35,7 @@ export default function FineGrainedTokenEndpoints({
}
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 } =
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) => {
const { getAppsServerSideProps } = await import('@/github-apps/lib/index.js')
const { getAppsServerSideProps } = await import('@/github-apps/lib/index')
const { currentVersion, appsItems, categoriesWithoutSubcategories } =
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) => {
const { getAppsServerSideProps } = await import('@/github-apps/lib/index.js')
const { getAppsServerSideProps } = await import('@/github-apps/lib/index')
const { currentVersion, appsItems, categoriesWithoutSubcategories } =
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) => {
const { getAppsServerSideProps } = await import('@/github-apps/lib/index.js')
const { getAppsServerSideProps } = await import('@/github-apps/lib/index')
const { currentVersion, appsItems, categoriesWithoutSubcategories } =
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) => {
const { getAppsServerSideProps } = await import('@/github-apps/lib/index.js')
const { getAppsServerSideProps } = await import('@/github-apps/lib/index')
const { currentVersion, appsItems, categoriesWithoutSubcategories } =
await getAppsServerSideProps(context, 'server-to-server-permissions', { useDisplayTitle: true })

View File

@@ -2,7 +2,20 @@
// src/github-apps/data/fine-grained-pat-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',
required: ['access', 'category', 'subcategory', 'slug', 'verb', 'requestPath'],
properties: {
@@ -32,7 +45,7 @@ const permissionObjects = {
},
}
export default {
const schema: Schema = {
type: 'object',
required: ['title', 'displayTitle', 'permissions'],
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 { createOperations, processOperations } from './utils/get-operations'
interface ProgramOptions {
files: string[]
}
program
.description('Generate dereferenced OpenAPI and decorated schema files.')
.requiredOption(
@@ -18,9 +22,9 @@ program
)
.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) {
check(filesToCheck)
@@ -29,22 +33,22 @@ if (filesToCheck.length) {
process.exit(1)
}
async function check(files) {
async function check(files: string[]): Promise<void> {
console.log('Verifying OpenAPI files are valid with decorator')
const documents = files.map((filename) => [
const documents: [string, any][] = files.map((filename: string) => [
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 {
// munge OpenAPI definitions object in an array of operations objects
const operations = await createOperations(schema)
// process each operation, asynchronously rendering markdown and stuff
await processOperations(operations)
await processOperations(operations, {})
console.log(`Successfully could decorate OpenAPI operations for document ${filename}`)
} catch (error) {
} catch (error: unknown) {
console.error(error)
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.`,