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

Convert 10 JavaScript files to TypeScript (#57642)

This commit is contained in:
Kevin Heis
2025-09-25 07:55:33 -07:00
committed by GitHub
parent 8aa5f5324c
commit e1713a7d28
13 changed files with 203 additions and 117 deletions

View File

@@ -5,14 +5,14 @@ import fs from 'fs'
// is unpredictable in GitHub Actions because of how it does `git clone`. // is unpredictable in GitHub Actions because of how it does `git clone`.
// So we rely on environment variables instead. // So we rely on environment variables instead.
export function getDiffFiles() { export function getDiffFiles(): string[] {
// Instead of testing every single file possible, if there's // Instead of testing every single file possible, if there's
// an environment variable called `DIFF_FILES` or one called // an environment variable called `DIFF_FILES` or one called
// `DIFF_FILE` then use that. // `DIFF_FILE` then use that.
// If `DIFF_FILES` is set, it's expected to be a space separated // If `DIFF_FILES` is set, it's expected to be a space separated
// string. If `DIFF_FILE` is set, it's expected to be a text file // string. If `DIFF_FILE` is set, it's expected to be a text file
// which contains a space separated string. // which contains a space separated string.
const diffFiles = [] const diffFiles: string[] = []
// Setting an environment variable called `DIFF_FILES` is optional. // Setting an environment variable called `DIFF_FILES` is optional.
// But if and only if it's set, we will respect it. // But if and only if it's set, we will respect it.
// And if it set, turn it into a cleaned up Set so it's made available // And if it set, turn it into a cleaned up Set so it's made available

View File

@@ -1,20 +1,24 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError, filterTokens, newLineRe } from 'markdownlint-rule-helpers' import { addError, filterTokens, newLineRe } from 'markdownlint-rule-helpers'
export const codeFenceLineLength = { import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '@/content-linter/types'
export const codeFenceLineLength: Rule = {
names: ['GHD030', 'code-fence-line-length'], names: ['GHD030', 'code-fence-line-length'],
description: 'Code fence lines should not exceed a maximum length', description: 'Code fence lines should not exceed a maximum length',
tags: ['code', 'accessibility'], tags: ['code', 'accessibility'],
parser: 'markdownit', parser: 'markdownit',
function: (params, onError) => { function: (params: RuleParams, onError: RuleErrorCallback) => {
const MAX_LINE_LENGTH = String(params.config.maxLength || 60) const MAX_LINE_LENGTH: number = params.config?.maxLength || 60
filterTokens(params, 'fence', (token) => { filterTokens(params, 'fence', (token: MarkdownToken) => {
const lines = token.content.split(newLineRe) if (!token.content) return
lines.forEach((line, index) => { const lines: string[] = token.content.split(newLineRe)
lines.forEach((line: string, index: number) => {
if (line.length > MAX_LINE_LENGTH) { if (line.length > MAX_LINE_LENGTH) {
// The token line number is the line number of the first line of the // The token line number is the line number of the first line of the
// code fence. We want to report the line number of the content within // code fence. We want to report the line number of the content within
// the code fence so we need to add 1 + the index. // the code fence so we need to add 1 + the index.
const lineNumber = token.lineNumber + index + 1 const lineNumber: number = token.lineNumber + index + 1
addError( addError(
onError, onError,
lineNumber, lineNumber,

View File

@@ -1,16 +1,16 @@
// used below to remove extra newlines in TOC lists // used below to remove extra newlines in TOC lists
const endLine = '</a>\r?\n' const endLine: string = '</a>\r?\n'
const blankLine = '\\s*?[\r\n]*' const blankLine: string = '\\s*?[\r\n]*'
const startNextLine = '[^\\S\r\n]*?[-\\*] <a' const startNextLine: string = '[^\\S\r\n]*?[-\\*] <a'
const blankLineInList = new RegExp(`(${endLine})${blankLine}(${startNextLine})`, 'mg') const blankLineInList: RegExp = new RegExp(`(${endLine})${blankLine}(${startNextLine})`, 'mg')
export function processLiquidPost(template) { export function processLiquidPost(template: string): string {
template = cleanUpListEmptyLines(template) template = cleanUpListEmptyLines(template)
template = cleanUpExtraEmptyLines(template) template = cleanUpExtraEmptyLines(template)
return template return template
} }
function cleanUpListEmptyLines(template) { function cleanUpListEmptyLines(template: string): string {
// clean up empty lines in TOC lists left by unrendered list items (due to productVersions) // clean up empty lines in TOC lists left by unrendered list items (due to productVersions)
// for example, remove the blank line here: // for example, remove the blank line here:
// - <a>foo</a> // - <a>foo</a>
@@ -22,7 +22,7 @@ function cleanUpListEmptyLines(template) {
return template return template
} }
function cleanUpExtraEmptyLines(template) { function cleanUpExtraEmptyLines(template: string): string {
// this removes any extra newlines left by (now resolved) liquid // this removes any extra newlines left by (now resolved) liquid
// statements so that extra space doesn't mess with list numbering // statements so that extra space doesn't mess with list numbering
template = template.replace(/(\r?\n){3}/g, '\n\n') template = template.replace(/(\r?\n){3}/g, '\n\n')

View File

@@ -1,17 +1,26 @@
// src/content-render/liquid/prompt.js // src/content-render/liquid/prompt.ts
// Defines {% prompt %}…{% endprompt %} to wrap its content in <code> and append the Copilot icon. // Defines {% prompt %}…{% endprompt %} to wrap its content in <code> and append the Copilot icon.
// @ts-ignore - @primer/octicons doesn't provide TypeScript declarations
import octicons from '@primer/octicons' import octicons from '@primer/octicons'
export const Prompt = { interface LiquidTag {
type: 'block'
templates?: any[] // Note: Using 'any' because liquidjs doesn't provide proper types for template objects
// Note: Using 'any' for liquid-related parameters because liquidjs doesn't provide comprehensive TypeScript definitions
parse(tagToken: any, remainTokens: any): void
render(scope: any): Generator<any, string, unknown>
}
export const Prompt: LiquidTag = {
type: 'block', type: 'block',
// Collect everything until {% endprompt %} // Collect everything until {% endprompt %}
parse(tagToken, remainTokens) { parse(tagToken: any, remainTokens: any): void {
this.templates = [] this.templates = []
const stream = this.liquid.parser.parseStream(remainTokens) const stream = this.liquid.parser.parseStream(remainTokens)
stream stream
.on('template', (tpl) => this.templates.push(tpl)) .on('template', (tpl: any) => this.templates.push(tpl))
.on('tag:endprompt', () => stream.stop()) .on('tag:endprompt', () => stream.stop())
.on('end', () => { .on('end', () => {
throw new Error(`{% prompt %} tag not closed`) throw new Error(`{% prompt %} tag not closed`)
@@ -20,12 +29,12 @@ export const Prompt = {
}, },
// Render the inner Markdown, wrap in <code>, then append the SVG // Render the inner Markdown, wrap in <code>, then append the SVG
render: function* (scope) { render: function* (scope: any): Generator<any, string, unknown> {
const content = yield this.liquid.renderer.renderTemplates(this.templates, scope) const content = yield this.liquid.renderer.renderTemplates(this.templates, scope)
// build a URL with the prompt text encoded as query parameter // build a URL with the prompt text encoded as query parameter
const promptParam = encodeURIComponent(content) const promptParam: string = encodeURIComponent(content as string)
const href = `https://github.com/copilot?prompt=${promptParam}` const href: string = `https://github.com/copilot?prompt=${promptParam}`
return `<code>${content}</code><a href="${href}" target="_blank" class="tooltipped tooltipped-nw ml-1" aria-label="Run this prompt in Copilot Chat" style="text-decoration:none;">${octicons.copilot.toSVG()}</a>` return `<code>${content}</code><a href="${href}" target="_blank" class="tooltipped tooltipped-nw ml-1" aria-label="Run this prompt in Copilot Chat" style="text-decoration:none;">${octicons.copilot.toSVG()}</a>`
}, },
} }

View File

@@ -3,8 +3,8 @@ import { renderContent } from '@/content-render/index'
describe('prompt tag', () => { describe('prompt tag', () => {
test('wraps content in <code> and appends svg', async () => { test('wraps content in <code> and appends svg', async () => {
const input = 'Here is your prompt: {% prompt %}example prompt text{% endprompt %}.' const input: string = 'Here is your prompt: {% prompt %}example prompt text{% endprompt %}.'
const output = await renderContent(input) const output: string = await renderContent(input)
expect(output).toContain('<code>example prompt text</code><a') expect(output).toContain('<code>example prompt text</code><a')
expect(output).toContain('<svg') expect(output).toContain('<svg')
}) })

View File

@@ -1,32 +0,0 @@
// Based on https://spec.commonmark.org/0.30/#info-string
// Parse out info strings on fenced code blocks, example:
// ```javascript lineNumbers:left copy:all annotate
// becomes...
// node.lang = javascript
// node.meta = { lineNumbers: 'left', copy: 'all', annotate: true }
// Also parse equals signs, where id=some-id becomes { id: 'some-id' }
import { visit } from 'unist-util-visit'
const matcher = (node) => node.type === 'code' && node.lang
export default function parseInfoString() {
return (tree) => {
visit(tree, matcher, (node) => {
node.meta = strToObj(node.meta)
// Temporary, remove {:copy} to avoid highlight parse error in translations.
node.lang = node.lang.replace('{:copy}', '')
})
}
}
function strToObj(str) {
if (!str) return {}
return Object.fromEntries(
str
.split(/\s+/g)
.map((k) => k.split(/[:=]/)) // split by colon or equals sign
.map(([k, ...v]) => [k, v.length ? v.join(':') : true]),
)
}

View File

@@ -0,0 +1,45 @@
// Based on https://spec.commonmark.org/0.30/#info-string
// Parse out info strings on fenced code blocks, example:
// ```javascript lineNumbers:left copy:all annotate
// becomes...
// node.lang = javascript
// node.meta = { lineNumbers: 'left', copy: 'all', annotate: true }
// Also parse equals signs, where id=some-id becomes { id: 'some-id' }
import { visit } from 'unist-util-visit'
interface CodeNode {
type: 'code'
lang?: string
meta?: string | Record<string, string | boolean>
value: string
}
// Note: Using 'any' for node because unist-util-visit's type constraints
// don't easily allow for proper code node typing without complex generics
const matcher = (node: any): node is CodeNode => node.type === 'code' && node.lang
export default function parseInfoString() {
// Note: Using 'any' for tree because unified's AST types are complex and
// this function works with different tree types depending on the processor
return (tree: any) => {
visit(tree, matcher, (node: CodeNode) => {
node.meta = strToObj(node.meta as string)
// Temporary, remove {:copy} to avoid highlight parse error in translations.
if (node.lang) {
node.lang = node.lang.replace('{:copy}', '')
}
})
}
}
function strToObj(str?: string): Record<string, string | boolean> {
if (!str) return {}
return Object.fromEntries(
str
.split(/\s+/g)
.map((k: string) => k.split(/[:=]/)) // split by colon or equals sign
.map(([k, ...v]: string[]) => [k, v.length ? v.join(':') : true]),
)
}

View File

@@ -1,29 +0,0 @@
import GithubSlugger from 'github-slugger'
import { encode } from 'html-entities'
import { toString } from 'hast-util-to-string'
import { visit } from 'unist-util-visit'
const slugger = new GithubSlugger()
const matcher = (node) => node.type === 'element' && ['h2', 'h3', 'h4'].includes(node.tagName)
// replace translated IDs and links in headings with English
export default function useEnglishHeadings({ englishHeadings }) {
if (!englishHeadings) return
return (tree) => {
visit(tree, matcher, (node) => {
slugger.reset()
// Get the plain text content of the heading node
const text = toString(node)
// find English heading in the collection
const englishHeading = englishHeadings[encode(text)]
// get English slug
const englishSlug = slugger.slug(englishHeading)
// use English slug for heading ID and link
if (englishSlug) {
// only use English slug if there is one, otherwise we'll end up with
// empty IDs
node.properties.id = englishSlug
}
})
}
}

View File

@@ -0,0 +1,41 @@
import GithubSlugger from 'github-slugger'
import { encode } from 'html-entities'
import { toString } from 'hast-util-to-string'
import { visit } from 'unist-util-visit'
const slugger = new GithubSlugger()
// Note: Using 'any' for node because the unist/hast type system is complex and
// the visit function's type constraints don't easily allow for proper element typing
// without extensive type gymnastics. The runtime check ensures type safety.
const matcher = (node: any) => node.type === 'element' && ['h2', 'h3', 'h4'].includes(node.tagName)
interface UseEnglishHeadingsOptions {
englishHeadings?: Record<string, string>
}
// replace translated IDs and links in headings with English
export default function useEnglishHeadings({ englishHeadings }: UseEnglishHeadingsOptions) {
if (!englishHeadings) return
// Note: Using 'any' for tree because unified's AST types are complex and
// this function works with different tree types depending on the processor
return (tree: any) => {
// Note: Using 'any' for node because visit() callback typing is restrictive
// and doesn't easily allow for proper element typing without complex generics
visit(tree, matcher, (node: any) => {
slugger.reset()
// Get the plain text content of the heading node
const text: string = toString(node)
// find English heading in the collection
const englishHeading: string = englishHeadings[encode(text)]
// get English slug
const englishSlug: string = slugger.slug(englishHeading)
// use English slug for heading ID and link
if (englishSlug) {
// only use English slug if there is one, otherwise we'll end up with
// empty IDs
node.properties.id = englishSlug
}
})
}
}

View File

@@ -3,7 +3,23 @@
// src/github-apps/data/user-to-server-rest.json // src/github-apps/data/user-to-server-rest.json
// and src/github-apps/data/fine-grained-pat.json // and src/github-apps/data/fine-grained-pat.json
export default { interface SchemaProperty {
description: string
type: string
}
interface EnabledListSchema {
type: string
required: string[]
properties: {
slug: SchemaProperty
subcategory: SchemaProperty
verb: SchemaProperty
requestPath: SchemaProperty
}
}
const schema: EnabledListSchema = {
type: 'object', type: 'object',
required: ['slug', 'subcategory', 'verb', 'requestPath'], required: ['slug', 'subcategory', 'verb', 'requestPath'],
properties: { properties: {
@@ -25,3 +41,5 @@ export default {
}, },
}, },
} }
export default schema

View File

@@ -7,12 +7,14 @@ import { REST_DATA_DIR } from '../lib/index'
const clientSideRestAPIRedirects = readCompressedJsonFileFallbackLazily( const clientSideRestAPIRedirects = readCompressedJsonFileFallbackLazily(
path.join(REST_DATA_DIR, 'client-side-rest-api-redirects.json'), path.join(REST_DATA_DIR, 'client-side-rest-api-redirects.json'),
) ) as () => Record<string, string>
const router = express.Router() const router = express.Router()
// Returns a client side redirect if one exists for the given path. // Returns a client side redirect if one exists for the given path.
router.get('/', function redirects(req, res) { // Note: Using 'any' for req/res because Express types are complex and the
// function signature is constrained by the router.get() overloads
router.get('/', function redirects(req: any, res: any) {
if (!req.query.path) { if (!req.query.path) {
return res.status(400).send("Missing 'path' query string") return res.status(400).send("Missing 'path' query string")
} }
@@ -22,7 +24,7 @@ router.get('/', function redirects(req, res) {
defaultCacheControl(res) defaultCacheControl(res)
const redirectFrom = `${req.query.path}#${req.query.hash}` const redirectFrom: string = `${req.query.path}#${req.query.hash}`
res.status(200).send({ to: clientSideRestAPIRedirects()[redirectFrom] }) res.status(200).send({ to: clientSideRestAPIRedirects()[redirectFrom] })
}) })

View File

@@ -1,27 +0,0 @@
const childPage = {
type: 'object',
required: ['href', 'page'],
properties: {
href: {
type: 'string',
},
page: {
type: 'object',
required: ['title', 'relativePath', 'permalinks'],
properties: {
title: {
type: 'string',
},
relativePath: {
type: 'string',
},
permalinks: {
type: 'array',
minItems: 1,
},
},
},
},
}
export default { childPage }

View File

@@ -0,0 +1,55 @@
interface SchemaProperty {
type: string
minItems?: number
}
interface PageProperties {
title: SchemaProperty
relativePath: SchemaProperty
permalinks: SchemaProperty
}
interface PageSchema {
type: string
required: string[]
properties: PageProperties
}
interface ChildPageProperties {
href: SchemaProperty
page: PageSchema
}
interface ChildPageSchema {
type: string
required: string[]
properties: ChildPageProperties
}
const childPage: ChildPageSchema = {
type: 'object',
required: ['href', 'page'],
properties: {
href: {
type: 'string',
},
page: {
type: 'object',
required: ['title', 'relativePath', 'permalinks'],
properties: {
title: {
type: 'string',
},
relativePath: {
type: 'string',
},
permalinks: {
type: 'array',
minItems: 1,
},
},
},
},
}
export default { childPage }