From 8f5f64a443dd242eff84bfa379f6bd3bc82fe117 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Mon, 22 Sep 2025 16:09:50 -0700 Subject: [PATCH] Convert 9 JavaScript files to TypeScript (#57621) --- src/content-linter/lib/init-test.js | 22 ----------- src/content-linter/lib/init-test.ts | 34 +++++++++++++++++ ...ode-annotations.js => code-annotations.ts} | 19 +++++++--- ...kebab-case.js => image-file-kebab-case.ts} | 11 +++--- .../{image-no-gif.js => image-no-gif.ts} | 14 ++++--- ...variable.js => hardcoded-data-variable.ts} | 14 ++++--- src/content-linter/types.ts | 37 ++++++++++++++++++- src/content-render/unified/heading-links.js | 23 ------------ src/content-render/unified/heading-links.ts | 34 +++++++++++++++++ src/content-render/unified/index.js | 22 ----------- src/content-render/unified/index.ts | 26 +++++++++++++ src/frame/lib/find-page.js | 23 ------------ src/frame/lib/find-page.ts | 31 ++++++++++++++++ .../{get-operations.js => get-operations.ts} | 26 +++++++++++-- src/types.ts | 1 + 15 files changed, 221 insertions(+), 116 deletions(-) delete mode 100644 src/content-linter/lib/init-test.js create mode 100644 src/content-linter/lib/init-test.ts rename src/content-linter/lib/linting-rules/{code-annotations.js => code-annotations.ts} (52%) rename src/content-linter/lib/linting-rules/{image-file-kebab-case.js => image-file-kebab-case.ts} (62%) rename src/content-linter/lib/linting-rules/{image-no-gif.js => image-no-gif.ts} (51%) rename src/content-linter/tests/unit/{hardcoded-data-variable.js => hardcoded-data-variable.ts} (62%) delete mode 100644 src/content-render/unified/heading-links.js create mode 100644 src/content-render/unified/heading-links.ts delete mode 100644 src/content-render/unified/index.js create mode 100644 src/content-render/unified/index.ts delete mode 100644 src/frame/lib/find-page.js create mode 100644 src/frame/lib/find-page.ts rename src/rest/scripts/utils/{get-operations.js => get-operations.ts} (54%) diff --git a/src/content-linter/lib/init-test.js b/src/content-linter/lib/init-test.js deleted file mode 100644 index 9e7aaf970021..000000000000 --- a/src/content-linter/lib/init-test.js +++ /dev/null @@ -1,22 +0,0 @@ -import markdownlint from 'markdownlint' - -import { defaultConfig } from './default-markdownlint-options' - -export async function runRule(module, { strings, files, ruleConfig, markdownlintOptions = {} }) { - if ((!strings && !files) || (strings && files)) - throw new Error('Must provide either Markdown strings or files to run a rule') - - const testConfig = { - [module.names[0]]: ruleConfig || true, - } - - const testOptions = { - customRules: [module], - config: { ...defaultConfig, ...testConfig }, - } - if (strings) testOptions.strings = strings - if (files) testOptions.files = files - - const options = { ...markdownlintOptions, ...testOptions } - return await markdownlint.promises.markdownlint(options) -} diff --git a/src/content-linter/lib/init-test.ts b/src/content-linter/lib/init-test.ts new file mode 100644 index 000000000000..f5eb91e05d45 --- /dev/null +++ b/src/content-linter/lib/init-test.ts @@ -0,0 +1,34 @@ +import markdownlint from 'markdownlint' +import type { Configuration, Options } from 'markdownlint' + +import { defaultConfig } from '@/content-linter/lib/default-markdownlint-options' +import type { Rule } from '@/content-linter/types' + +interface RunRuleOptions { + strings?: { [key: string]: string } + files?: string[] + ruleConfig?: boolean | object + markdownlintOptions?: Partial +} + +export async function runRule( + module: Rule, + { strings, files, ruleConfig, markdownlintOptions = {} }: RunRuleOptions, +) { + if ((!strings && !files) || (strings && files)) + throw new Error('Must provide either Markdown strings or files to run a rule') + + const testConfig: Configuration = { + [module.names[0]]: ruleConfig || true, + } + + const testOptions: Partial = { + customRules: [module as any], + config: { ...defaultConfig, ...testConfig }, + } + if (strings) testOptions.strings = strings + if (files) testOptions.files = files + + const options: Options = { ...markdownlintOptions, ...testOptions } + return await markdownlint.promises.markdownlint(options) +} diff --git a/src/content-linter/lib/linting-rules/code-annotations.js b/src/content-linter/lib/linting-rules/code-annotations.ts similarity index 52% rename from src/content-linter/lib/linting-rules/code-annotations.js rename to src/content-linter/lib/linting-rules/code-annotations.ts index b06a0703ddd6..226e93a4d2ab 100644 --- a/src/content-linter/lib/linting-rules/code-annotations.js +++ b/src/content-linter/lib/linting-rules/code-annotations.ts @@ -1,17 +1,24 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations import { addError, filterTokens } from 'markdownlint-rule-helpers' -import { getFrontmatter } from '../helpers/utils' +import { getFrontmatter } from '@/content-linter/lib/helpers/utils' +import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types' + +interface Frontmatter { + layout?: string + [key: string]: any +} export const codeAnnotations = { names: ['GHD007', 'code-annotations'], description: 'Code annotations defined in Markdown must contain a specific layout frontmatter property', tags: ['code', 'feature', 'annotate', 'frontmatter'], - parser: 'markdownit', - function: (params, onError) => { - filterTokens(params, 'fence', (token) => { - if (!token.info.includes('annotate')) return - const fm = getFrontmatter(params.frontMatterLines) + parser: 'markdownit' as const, + function: (params: RuleParams, onError: RuleErrorCallback) => { + filterTokens(params, 'fence', (token: MarkdownToken) => { + if (!token.info?.includes('annotate')) return + const fm: Frontmatter | null = getFrontmatter(params.frontMatterLines) if (!fm || (fm.layout && fm.layout === 'inline')) return addError( diff --git a/src/content-linter/lib/linting-rules/image-file-kebab-case.js b/src/content-linter/lib/linting-rules/image-file-kebab-case.ts similarity index 62% rename from src/content-linter/lib/linting-rules/image-file-kebab-case.js rename to src/content-linter/lib/linting-rules/image-file-kebab-case.ts index f7ce14a63af7..96535ee93dd5 100644 --- a/src/content-linter/lib/linting-rules/image-file-kebab-case.js +++ b/src/content-linter/lib/linting-rules/image-file-kebab-case.ts @@ -1,13 +1,14 @@ -import { addFixErrorDetail, forEachInlineChild } from '../helpers/utils' +import { addFixErrorDetail, forEachInlineChild } from '@/content-linter/lib/helpers/utils' +import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types' export const imageFileKebabCase = { names: ['GHD004', 'image-file-kebab-case'], description: 'Image file names must use kebab-case', tags: ['images'], - parser: 'markdownit', - function: (params, onError) => { - forEachInlineChild(params, 'image', async function forToken(token) { - const imageFileName = token.attrs[0][1].split('/').pop().split('.')[0] + parser: 'markdownit' as const, + function: (params: RuleParams, onError: RuleErrorCallback) => { + forEachInlineChild(params, 'image', async function forToken(token: MarkdownToken) { + const imageFileName = token.attrs?.[0]?.[1]?.split('/').pop()?.split('.')[0] || '' const nonKebabRegex = /([A-Z]|_)/ const suggestedFileName = imageFileName.toLowerCase().replace(/_/g, '-') if (imageFileName.match(nonKebabRegex)) { diff --git a/src/content-linter/lib/linting-rules/image-no-gif.js b/src/content-linter/lib/linting-rules/image-no-gif.ts similarity index 51% rename from src/content-linter/lib/linting-rules/image-no-gif.js rename to src/content-linter/lib/linting-rules/image-no-gif.ts index bdd0a8a7fbdb..1e59a473050e 100644 --- a/src/content-linter/lib/linting-rules/image-no-gif.js +++ b/src/content-linter/lib/linting-rules/image-no-gif.ts @@ -1,17 +1,19 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations import { addError } from 'markdownlint-rule-helpers' -import { forEachInlineChild } from '../helpers/utils' +import { forEachInlineChild } from '@/content-linter/lib/helpers/utils' +import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types' export const imageNoGif = { names: ['GHD036', 'image-no-gif'], description: 'Image must not be a gif, styleguide reference: contributing/style-guide-and-content-model/style-guide.md#images', tags: ['images'], - parser: 'markdownit', - function: (params, onError) => { - forEachInlineChild(params, 'image', function forToken(token) { - const imageFileName = token.attrs[0][1] - if (imageFileName.endsWith('.gif')) { + parser: 'markdownit' as const, + function: (params: RuleParams, onError: RuleErrorCallback) => { + forEachInlineChild(params, 'image', function forToken(token: MarkdownToken) { + const imageFileName = token.attrs?.[0]?.[1] + if (imageFileName && imageFileName.endsWith('.gif')) { addError( onError, token.lineNumber, diff --git a/src/content-linter/tests/unit/hardcoded-data-variable.js b/src/content-linter/tests/unit/hardcoded-data-variable.ts similarity index 62% rename from src/content-linter/tests/unit/hardcoded-data-variable.js rename to src/content-linter/tests/unit/hardcoded-data-variable.ts index 9ecce111241f..109f82b9c809 100644 --- a/src/content-linter/tests/unit/hardcoded-data-variable.js +++ b/src/content-linter/tests/unit/hardcoded-data-variable.ts @@ -1,18 +1,22 @@ import { describe, expect, test } from 'vitest' -import { runRule } from '../../lib/init-test' -import { hardcodedDataVariable } from '../../lib/linting-rules/hardcoded-data-variable' +import { runRule } from '@/content-linter/lib/init-test' +import { hardcodedDataVariable } from '@/content-linter/lib/linting-rules/hardcoded-data-variable' describe(hardcodedDataVariable.names.join(' - '), () => { - test('Using hardcoded personal access token string causes error', async () => { - const markdown = [ + test('Using hardcoded personal access token string causes error', async (): Promise => { + const markdown: string = [ 'Hello personal access token for apps.', 'A Personal access token for apps.', 'Lots of PERSONAL ACCESS TOKENS for apps.', 'access tokens for apps.', 'personal access token and a Personal Access Tokens', ].join('\n') - const result = await runRule(hardcodedDataVariable, { strings: { markdown } }) + const result = await runRule(hardcodedDataVariable, { + strings: { markdown }, + files: undefined, + ruleConfig: true, + }) const errors = result.markdown expect(errors.length).toBe(5) expect(errors[errors.length - 2].lineNumber).toBe(5) diff --git a/src/content-linter/types.ts b/src/content-linter/types.ts index 5deac218a82b..c4feed37b124 100644 --- a/src/content-linter/types.ts +++ b/src/content-linter/types.ts @@ -1,9 +1,44 @@ +// Interfaces for content linter rule parameters and callbacks +export interface MarkdownToken { + type: string + tag?: string + attrs?: [string, string][] + content?: string + info?: string + lineNumber: number + line: string + children?: MarkdownToken[] +} + +export interface RuleParams { + name: string // file path + lines: string[] // array of lines from the file + frontMatterLines: string[] // array of frontmatter lines + tokens?: MarkdownToken[] // markdown tokens (when using markdownit parser) + config?: { + [key: string]: any // rule-specific configuration + } +} + +export interface RuleErrorCallback { + ( + lineNumber: number, + detail?: string, + context?: string, + range?: [number, number], + fixInfo?: any, + ): void +} + export type Rule = { names: string[] description: string tags: string[] information?: URL - function: Function[] + parser?: 'markdownit' | 'micromark' | 'none' + asynchronous?: boolean + severity?: string + function: (params: RuleParams, onError: RuleErrorCallback) => void | Promise } type RuleDetail = Rule & { diff --git a/src/content-render/unified/heading-links.js b/src/content-render/unified/heading-links.js deleted file mode 100644 index 366eeecc65e5..000000000000 --- a/src/content-render/unified/heading-links.js +++ /dev/null @@ -1,23 +0,0 @@ -import { visit } from 'unist-util-visit' -import { h } from 'hastscript' - -const matcher = (node) => - node.type === 'element' && - ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && - node.properties?.id - -export default function headingLinks() { - return (tree) => { - visit(tree, matcher, (node) => { - const { id } = node.properties - const text = node.children - node.properties.tabIndex = -1 - node.children = [ - h('a', { class: 'heading-link', href: `#${id}` }, [ - ...text, - h('span', { class: 'heading-link-symbol', ariaHidden: 'true' }), - ]), - ] - }) - } -} diff --git a/src/content-render/unified/heading-links.ts b/src/content-render/unified/heading-links.ts new file mode 100644 index 000000000000..e4a500441ea0 --- /dev/null +++ b/src/content-render/unified/heading-links.ts @@ -0,0 +1,34 @@ +import { visit } from 'unist-util-visit' +import { h } from 'hastscript' + +interface HeadingNode { + type: 'element' + tagName: string + properties: { + id?: string + tabIndex?: number + [key: string]: any + } + children: any[] +} + +const matcher = (node: any): node is HeadingNode => + node.type === 'element' && + ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && + node.properties?.id + +export default function headingLinks() { + return (tree: any) => { + visit(tree, matcher, (node: HeadingNode) => { + const { id } = node.properties + const text = node.children + node.properties.tabIndex = -1 + node.children = [ + h('a', { className: 'heading-link', href: `#${id}` }, [ + ...text, + h('span', { className: 'heading-link-symbol', ariaHidden: 'true' }), + ]), + ] + }) + } +} diff --git a/src/content-render/unified/index.js b/src/content-render/unified/index.js deleted file mode 100644 index d7244485daa9..000000000000 --- a/src/content-render/unified/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import { fastTextOnly } from './text-only' -import { createProcessor, createMarkdownOnlyProcessor } from './processor' - -export async function renderUnified(template, context, options) { - const processor = createProcessor(context) - const vFile = await processor.process(template) - let html = vFile.toString() - - if (options.textOnly) { - html = fastTextOnly(html) - } - - return html.trim() -} - -export async function renderMarkdown(template, context, options) { - const processor = createMarkdownOnlyProcessor(context) - const vFile = await processor.process(template) - let markdown = vFile.toString() - - return markdown.trim() -} diff --git a/src/content-render/unified/index.ts b/src/content-render/unified/index.ts new file mode 100644 index 000000000000..83055d249b06 --- /dev/null +++ b/src/content-render/unified/index.ts @@ -0,0 +1,26 @@ +import { fastTextOnly } from '@/content-render/unified/text-only' +import { createProcessor, createMarkdownOnlyProcessor } from '@/content-render/unified/processor' + +interface RenderOptions { + textOnly?: boolean +} + +export async function renderUnified(template: string, context: any, options: RenderOptions = {}) { + const processor = createProcessor(context) + const vFile = await processor.process(template) + let html = vFile.toString() + + if (options.textOnly) { + html = fastTextOnly(html) + } + + return html.trim() +} + +export async function renderMarkdown(template: string, context: any) { + const processor = createMarkdownOnlyProcessor(context) + const vFile = await processor.process(template) + const markdown = vFile.toString() + + return markdown.trim() +} diff --git a/src/frame/lib/find-page.js b/src/frame/lib/find-page.js deleted file mode 100644 index 2657f26db383..000000000000 --- a/src/frame/lib/find-page.js +++ /dev/null @@ -1,23 +0,0 @@ -import { getLanguageCode } from './patterns' -import getRedirect from '@/redirects/lib/get-redirect' - -export default function findPage(href, pages, redirects) { - if (Array.isArray(pages)) throw new Error("'pages' is not supposed to be an array") - if (pages === undefined) throw new Error("'pages' cannot be undefined") - - // remove any fragments - href = new URL(href, 'http://example.com').pathname - - const redirectsContext = { redirects, pages } - - // find the page - const page = pages[href] || pages[getRedirect(href, redirectsContext)] - if (page) return page - - // get the current language - const currentLang = getLanguageCode.test(href) ? href.match(getLanguageCode)[1] : 'en' - - // try to fall back to English if the translated page can't be found - const englishHref = href.replace(`/${currentLang}/`, '/en/') - return pages[englishHref] || pages[getRedirect(englishHref, redirectsContext)] -} diff --git a/src/frame/lib/find-page.ts b/src/frame/lib/find-page.ts new file mode 100644 index 000000000000..1514f7604209 --- /dev/null +++ b/src/frame/lib/find-page.ts @@ -0,0 +1,31 @@ +import { getLanguageCode } from '@/frame/lib/patterns' +import getRedirect from '@/redirects/lib/get-redirect' +import type { Context, Page } from '@/types' + +export default function findPage( + href: string, + pages: Record | undefined, + redirects: Record | undefined, +): Page | undefined { + if (Array.isArray(pages)) throw new Error("'pages' is not supposed to be an array") + if (pages === undefined) return undefined + + // remove any fragments + href = new URL(href, 'http://example.com').pathname + + const redirectsContext: Context = { redirects: redirects || {}, pages } + + // find the page + const redirectedHref = getRedirect(href, redirectsContext) + const page = pages[href] || (redirectedHref ? pages[redirectedHref] : undefined) + if (page) return page + + // get the current language + const languageMatch = href.match(getLanguageCode) + const currentLang = getLanguageCode.test(href) && languageMatch ? languageMatch[1] : 'en' + + // try to fall back to English if the translated page can't be found + const englishHref = href.replace(`/${currentLang}/`, '/en/') + const redirectedEnglishHref = getRedirect(englishHref, redirectsContext) + return pages[englishHref] || (redirectedEnglishHref ? pages[redirectedEnglishHref] : undefined) +} diff --git a/src/rest/scripts/utils/get-operations.js b/src/rest/scripts/utils/get-operations.ts similarity index 54% rename from src/rest/scripts/utils/get-operations.js rename to src/rest/scripts/utils/get-operations.ts index 916a9450fab6..705e07832ece 100644 --- a/src/rest/scripts/utils/get-operations.js +++ b/src/rest/scripts/utils/get-operations.ts @@ -1,9 +1,26 @@ -import Operation from './operation' +import Operation from '@/rest/scripts/utils/operation' + +interface ProgAccessData { + [key: string]: any +} + +interface SchemaInput { + paths?: { + [requestPath: string]: { + [verb: string]: any + } + } + servers?: any[] + [key: string]: any +} // The module accepts a JSON schema object as input // and returns an array of its operation objects with their // HTTP verb and requestPath attached as properties -export async function processOperations(operations, progAccessData) { +export async function processOperations( + operations: any[], + progAccessData: ProgAccessData, +): Promise { await Promise.all( operations.map(async (operation) => { await operation.process(progAccessData) @@ -12,7 +29,10 @@ export async function processOperations(operations, progAccessData) { return operations } -export async function createOperations(schema) { +export async function createOperations(schema: SchemaInput): Promise { + if (!schema.paths) { + return [] + } return Object.entries(schema.paths) .map(([requestPath, operationsAtPath]) => Object.entries(operationsAtPath).map( diff --git a/src/types.ts b/src/types.ts index db399ccb59dc..d5108fc5aad3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -358,6 +358,7 @@ export type Page = { languageCode: string documentType: string renderProp: (prop: string, context: any, opts?: any) => Promise + renderTitle: (context: Context, opts?: any) => Promise markdown: string versions: FrontmatterVersions applicableVersions: string[]