From 647e0dca2a67641e0e10885561972948b2c98881 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:42:31 -0700 Subject: [PATCH 1/2] Bump github/gh-base-image/gh-base-noble from 20250924-191915-gc04d4a50b to 20250929-093120-g65a62eb8c in the baseimages group (#57742) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d172f5620ab9..04d6509f67f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ # --------------------------------------------------------------- # To update the sha: # https://github.com/github/gh-base-image/pkgs/container/gh-base-image%2Fgh-base-noble -FROM ghcr.io/github/gh-base-image/gh-base-noble:20250924-191915-gc04d4a50b AS base +FROM ghcr.io/github/gh-base-image/gh-base-noble:20250929-093120-g65a62eb8c AS base # Install curl for Node install and determining the early access branch # Install git for cloning docs-early-access & translations repos From 501e2512d763df01ddb630c4e7749934e425acf7 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Tue, 30 Sep 2025 13:44:12 -0700 Subject: [PATCH 2/2] Convert 27 JavaScript files to TypeScript (#57693) --- next.config.js | 24 +++++- .../lib/helpers/schema-utils.js | 41 ---------- .../lib/helpers/schema-utils.ts | 79 +++++++++++++++++++ ...den-docs.js => frontmatter-hidden-docs.ts} | 7 +- ...s.js => github-owned-action-references.ts} | 6 +- ...variable.js => hardcoded-data-variable.ts} | 6 +- ...n.js => image-alt-text-end-punctuation.ts} | 5 +- ...ext-length.js => image-alt-text-length.ts} | 13 ++- ...ink-punctuation.js => link-punctuation.ts} | 6 +- ...ack-liquid.js => learning-track-liquid.ts} | 9 ++- ...ode-annotations.js => code-annotations.ts} | 0 ...den-docs.js => frontmatter-hidden-docs.ts} | 0 ...ext-length.js => image-alt-text-length.ts} | 7 +- ...kebab-case.js => image-file-kebab-case.ts} | 0 .../unit/{image-no-gif.js => image-no-gif.ts} | 0 ...s-no-lang.js => internal-links-no-lang.ts} | 5 +- ...rsion.js => internal-links-old-version.ts} | 5 +- .../{link-quotation.js => link-quotation.ts} | 9 ++- src/content-render/liquid/spotlight.js | 36 --------- src/content-render/liquid/spotlight.ts | 64 +++++++++++++++ .../unified/rewrite-thead-th-scope.js | 35 -------- .../unified/rewrite-thead-th-scope.ts | 42 ++++++++++ ...{learning-tracks.js => learning-tracks.ts} | 2 +- src/frame/lib/{constants.js => constants.ts} | 0 .../{get-toc-items.js => get-toc-items.ts} | 42 +++++++--- src/frame/tests/{content.js => content.ts} | 21 +++-- .../tests/{find-page.js => find-page.ts} | 14 +++- ...rocess-previews.js => process-previews.ts} | 24 +++++- .../lib/{log-levels.js => log-levels.ts} | 18 ++++- .../{get-redirects.js => get-redirects.ts} | 30 +++++-- ...ise-versions.js => enterprise-versions.ts} | 0 31 files changed, 374 insertions(+), 176 deletions(-) delete mode 100644 src/content-linter/lib/helpers/schema-utils.js create mode 100644 src/content-linter/lib/helpers/schema-utils.ts rename src/content-linter/lib/linting-rules/{frontmatter-hidden-docs.js => frontmatter-hidden-docs.ts} (79%) rename src/content-linter/lib/linting-rules/{github-owned-action-references.js => github-owned-action-references.ts} (83%) rename src/content-linter/lib/linting-rules/{hardcoded-data-variable.js => hardcoded-data-variable.ts} (83%) rename src/content-linter/lib/linting-rules/{image-alt-text-end-punctuation.js => image-alt-text-end-punctuation.ts} (84%) rename src/content-linter/lib/linting-rules/{image-alt-text-length.js => image-alt-text-length.ts} (81%) rename src/content-linter/lib/linting-rules/{link-punctuation.js => link-punctuation.ts} (81%) rename src/content-linter/tests/{learning-track-liquid.js => learning-track-liquid.ts} (74%) rename src/content-linter/tests/unit/{code-annotations.js => code-annotations.ts} (100%) rename src/content-linter/tests/unit/{frontmatter-hidden-docs.js => frontmatter-hidden-docs.ts} (100%) rename src/content-linter/tests/unit/{image-alt-text-length.js => image-alt-text-length.ts} (81%) rename src/content-linter/tests/unit/{image-file-kebab-case.js => image-file-kebab-case.ts} (100%) rename src/content-linter/tests/unit/{image-no-gif.js => image-no-gif.ts} (100%) rename src/content-linter/tests/unit/{internal-links-no-lang.js => internal-links-no-lang.ts} (87%) rename src/content-linter/tests/unit/{internal-links-old-version.js => internal-links-old-version.ts} (85%) rename src/content-linter/tests/unit/{link-quotation.js => link-quotation.ts} (79%) delete mode 100644 src/content-render/liquid/spotlight.js create mode 100644 src/content-render/liquid/spotlight.ts delete mode 100644 src/content-render/unified/rewrite-thead-th-scope.js create mode 100644 src/content-render/unified/rewrite-thead-th-scope.ts rename src/data-directory/lib/data-schemas/{learning-tracks.js => learning-tracks.ts} (93%) rename src/frame/lib/{constants.js => constants.ts} (100%) rename src/frame/lib/{get-toc-items.js => get-toc-items.ts} (51%) rename src/frame/tests/{content.js => content.ts} (66%) rename src/frame/tests/{find-page.js => find-page.ts} (61%) rename src/graphql/scripts/utils/{process-previews.js => process-previews.ts} (64%) rename src/observability/logger/lib/{log-levels.js => log-levels.ts} (67%) rename src/rest/scripts/utils/{get-redirects.js => get-redirects.ts} (61%) rename src/versions/tests/{enterprise-versions.js => enterprise-versions.ts} (100%) diff --git a/next.config.js b/next.config.js index 3a5a2073570a..3e0720539bb1 100644 --- a/next.config.js +++ b/next.config.js @@ -2,7 +2,29 @@ import fs from 'fs' import path from 'path' import frontmatter from '@gr2m/gray-matter' -import { getLogLevelNumber } from './src/observability/logger/lib/log-levels.js' +// Hardcoded log level function since next.config.js cannot import from TypeScript files +// Matches ./src/observability/logger/lib/log-levels +function getLogLevelNumber() { + const LOG_LEVELS = { + error: 0, + warn: 1, + info: 2, + debug: 3, + } + + let defaultLogLevel = 'info' + if ( + !process.env.LOG_LEVEL && + (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') + ) { + defaultLogLevel = 'debug' + } + + const envLogLevel = process.env.LOG_LEVEL?.toLowerCase() || defaultLogLevel + const logLevel = LOG_LEVELS[envLogLevel] !== undefined ? envLogLevel : defaultLogLevel + + return LOG_LEVELS[logLevel] +} // Replace imports with hardcoded values const ROOT = process.env.ROOT || '.' diff --git a/src/content-linter/lib/helpers/schema-utils.js b/src/content-linter/lib/helpers/schema-utils.js deleted file mode 100644 index 95b302bf3eec..000000000000 --- a/src/content-linter/lib/helpers/schema-utils.js +++ /dev/null @@ -1,41 +0,0 @@ -// This function takes an array of AJV errors and formats them -// in a way that is more compatible with Markdownlint errors. -export function formatAjvErrors(errors = []) { - return errors.map((errorObj) => { - const error = {} - - // An instancePath is either blank or starts with a slash - // and separates object properties with slashes. A more - // common way to read object nesting is using dot notation. - error.instancePath = - errorObj.instancePath === '' - ? errorObj.instancePath - : errorObj.instancePath.slice(1).replace('/', '.') - - if (errorObj.keyword === 'additionalProperties') { - error.detail = 'The frontmatter includes an unsupported property.' - const pathContext = error.instancePath ? ` from \`${error.instancePath}\`` : '' - error.context = `Remove the property \`${errorObj.params.additionalProperty}\`${pathContext}.` - error.errorProperty = errorObj.params.additionalProperty - error.searchProperty = error.errorProperty - return error - } - - if (errorObj.keyword === 'required') { - error.detail = 'The frontmatter has a missing required property' - const pathContext = error.instancePath ? ` from \`${error.instancePath}\`` : '' - error.context = `Add the missing property \`${errorObj.params.missingProperty}\`${pathContext}` - error.errorProperty = errorObj.params.missingProperty - error.searchProperty = error.instancePath.split('.').pop() - return error - } - - // The two most common errors are required and additionalProperties. - // This catches any other with a generic detail that uses the AJV wording. - error.detail = `Frontmatter ${errorObj.message}.` - error.context = Object.values(errorObj.params).join('') - error.errorProperty = error.context - error.searchProperty = error.errorProperty - return error - }) -} diff --git a/src/content-linter/lib/helpers/schema-utils.ts b/src/content-linter/lib/helpers/schema-utils.ts new file mode 100644 index 000000000000..86cdc3c287db --- /dev/null +++ b/src/content-linter/lib/helpers/schema-utils.ts @@ -0,0 +1,79 @@ +import { getFrontmatter } from './utils' + +// AJV validation error object structure +interface AjvValidationError { + instancePath: string + keyword: string + message: string + params: { + additionalProperty?: string + missingProperty?: string + [key: string]: unknown + } +} + +// Processed error object for markdown linting +interface ProcessedValidationError { + instancePath: string + detail: string + context: string + errorProperty: string + searchProperty: string +} + +export function formatAjvErrors(errors: AjvValidationError[] = []): ProcessedValidationError[] { + const processedErrors: ProcessedValidationError[] = [] + + errors.forEach((errorObj: AjvValidationError) => { + const error: Partial = {} + + error.instancePath = + errorObj.instancePath === '' + ? errorObj.instancePath + : errorObj.instancePath.slice(1).replace('/', '.') + + if (errorObj.keyword === 'additionalProperties') { + error.detail = 'The frontmatter includes an unsupported property.' + const pathContext = error.instancePath ? ` from \`${error.instancePath}\`` : '' + error.context = `Remove the property \`${errorObj.params.additionalProperty}\`${pathContext}.` + error.errorProperty = errorObj.params.additionalProperty + error.searchProperty = error.errorProperty + } + + // required rule + if (errorObj.keyword === 'required') { + error.detail = 'The frontmatter has a missing required property' + const pathContext = error.instancePath ? ` from \`${error.instancePath}\`` : '' + error.context = `Add the missing property \`${errorObj.params.missingProperty}\`${pathContext}` + error.errorProperty = errorObj.params.missingProperty + error.searchProperty = error.instancePath.split('.').pop() + } + + // all other rules + if (!error.detail) { + error.detail = `Frontmatter ${errorObj.message}.` + error.context = Object.values(errorObj.params).join('') + error.errorProperty = error.context + error.searchProperty = error.errorProperty + } + + processedErrors.push(error as ProcessedValidationError) + }) + + return processedErrors +} + +// Alias for backward compatibility +export const processSchemaValidationErrors = formatAjvErrors + +// Schema validator interface - generic due to different schema types (AJV, JSON Schema, etc.) +interface SchemaValidator { + validate(data: unknown): boolean +} + +export function getSchemaValidator( + frontmatterLines: string[], +): (schema: SchemaValidator) => boolean { + const frontmatter = getFrontmatter(frontmatterLines) + return (schema: SchemaValidator) => schema.validate(frontmatter) +} diff --git a/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.js b/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts similarity index 79% rename from src/content-linter/lib/linting-rules/frontmatter-hidden-docs.js rename to src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts index 73f07184ed99..cdc9289da9cf 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.js +++ b/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts @@ -1,4 +1,6 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations import { addError } from 'markdownlint-rule-helpers' +import type { RuleParams, RuleErrorCallback } from '../../types' import { getFrontmatter } from '../helpers/utils' @@ -7,7 +9,7 @@ export const frontmatterHiddenDocs = { description: 'Articles with frontmatter property `hidden` can only be located in specific products', tags: ['frontmatter', 'feature', 'early-access'], - function: (params, onError) => { + function: (params: RuleParams, onError: RuleErrorCallback) => { const fm = getFrontmatter(params.lines) if (!fm || !fm.hidden) return @@ -24,7 +26,8 @@ export const frontmatterHiddenDocs = { if (allowedProductPaths.some((allowedPath) => params.name.includes(allowedPath))) return - const hiddenLine = params.lines.find((line) => line.startsWith('hidden:')) + const hiddenLine = params.lines.find((line: string) => line.startsWith('hidden:')) + if (!hiddenLine) return const lineNumber = params.lines.indexOf(hiddenLine) + 1 addError( diff --git a/src/content-linter/lib/linting-rules/github-owned-action-references.js b/src/content-linter/lib/linting-rules/github-owned-action-references.ts similarity index 83% rename from src/content-linter/lib/linting-rules/github-owned-action-references.js rename to src/content-linter/lib/linting-rules/github-owned-action-references.ts index 204bf2237153..797e1c88c34d 100644 --- a/src/content-linter/lib/linting-rules/github-owned-action-references.js +++ b/src/content-linter/lib/linting-rules/github-owned-action-references.ts @@ -1,16 +1,18 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations import { addError, ellipsify } from 'markdownlint-rule-helpers' +import type { RuleParams, RuleErrorCallback } from '../../types' import { getRange } from '../helpers/utils' /* This rule currently only checks for one hardcoded string but - can be generalized in the future to check for strings that + can be generalized in the future to check for strings that have data reusables. */ export const githubOwnedActionReferences = { names: ['GHD013', 'github-owned-action-references'], description: 'GitHub-owned action references should not be hardcoded', tags: ['feature', 'actions'], - function: (params, onError) => { + function: (params: RuleParams, onError: RuleErrorCallback) => { const filepath = params.name if (filepath.startsWith('data/reusables/actions/action-')) return diff --git a/src/content-linter/lib/linting-rules/hardcoded-data-variable.js b/src/content-linter/lib/linting-rules/hardcoded-data-variable.ts similarity index 83% rename from src/content-linter/lib/linting-rules/hardcoded-data-variable.js rename to src/content-linter/lib/linting-rules/hardcoded-data-variable.ts index 08b72c20ddea..e039d3debe3f 100644 --- a/src/content-linter/lib/linting-rules/hardcoded-data-variable.js +++ b/src/content-linter/lib/linting-rules/hardcoded-data-variable.ts @@ -1,11 +1,13 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations import { addError, ellipsify } from 'markdownlint-rule-helpers' +import type { RuleParams, RuleErrorCallback } from '../../types' import { getRange } from '../helpers/utils' import frontmatter from '@/frame/lib/read-frontmatter' /* This rule currently only checks for one hardcoded string but - can be generalized in the future to check for strings that + can be generalized in the future to check for strings that have data variables. */ export const hardcodedDataVariable = { @@ -13,7 +15,7 @@ export const hardcodedDataVariable = { description: 'Strings that contain "personal access token" should use the product variable instead', tags: ['single-source'], - function: (params, onError) => { + function: (params: RuleParams, onError: RuleErrorCallback) => { if (params.name.startsWith('data/variables/product.yml')) return const frontmatterString = params.frontMatterLines.join('\n') const fm = frontmatter(frontmatterString).data diff --git a/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js b/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.ts similarity index 84% rename from src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js rename to src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.ts index 829374722ef2..520a09e51e86 100644 --- a/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js +++ b/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.ts @@ -5,14 +5,15 @@ import { isStringQuoted, isStringPunctuated, } from '../helpers/utils' +import type { RuleParams, RuleErrorCallback } from '../../types' export const imageAltTextEndPunctuation = { names: ['GHD032', 'image-alt-text-end-punctuation'], description: 'Alternate text for images should end with punctuation', tags: ['accessibility', 'images'], parser: 'markdownit', - function: (params, onError) => { - forEachInlineChild(params, 'image', function forToken(token) { + function: (params: RuleParams, onError: RuleErrorCallback) => { + forEachInlineChild(params, 'image', function forToken(token: any) { const imageAltText = token.content.trim() // If the alt text is empty, there is nothing to check and you can't diff --git a/src/content-linter/lib/linting-rules/image-alt-text-length.js b/src/content-linter/lib/linting-rules/image-alt-text-length.ts similarity index 81% rename from src/content-linter/lib/linting-rules/image-alt-text-length.js rename to src/content-linter/lib/linting-rules/image-alt-text-length.ts index 80d2a04a5a99..043ba270827e 100644 --- a/src/content-linter/lib/linting-rules/image-alt-text-length.js +++ b/src/content-linter/lib/linting-rules/image-alt-text-length.ts @@ -1,17 +1,26 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations import { addError } from 'markdownlint-rule-helpers' +import type { RuleParams, RuleErrorCallback } from '../../types' import { liquid } from '@/content-render/index' import { allVersions } from '@/versions/lib/all-versions' import { forEachInlineChild, getRange } from '../helpers/utils' +interface ImageToken { + content: string + lineNumber: number + line: string + range: [number, number] +} + export const incorrectAltTextLength = { names: ['GHD033', 'incorrect-alt-text-length'], description: 'Images alternate text should be between 40-150 characters', tags: ['accessibility', 'images'], parser: 'markdownit', asynchronous: true, - function: (params, onError) => { - forEachInlineChild(params, 'image', async function forToken(token) { + function: (params: RuleParams, onError: RuleErrorCallback) => { + forEachInlineChild(params, 'image', async function forToken(token: ImageToken) { let renderedString = token.content if (token.content.includes('{%') || token.content.includes('{{')) { diff --git a/src/content-linter/lib/linting-rules/link-punctuation.js b/src/content-linter/lib/linting-rules/link-punctuation.ts similarity index 81% rename from src/content-linter/lib/linting-rules/link-punctuation.js rename to src/content-linter/lib/linting-rules/link-punctuation.ts index 669f34944807..778a92782ade 100644 --- a/src/content-linter/lib/linting-rules/link-punctuation.js +++ b/src/content-linter/lib/linting-rules/link-punctuation.ts @@ -1,4 +1,6 @@ +// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations import { addError, filterTokens } from 'markdownlint-rule-helpers' +import type { RuleParams, RuleErrorCallback } from '../../types' import { doesStringEndWithPeriod, getRange, isStringQuoted } from '../helpers/utils' @@ -7,8 +9,8 @@ export const linkPunctuation = { description: 'Internal link titles must not contain punctuation', tags: ['links', 'url'], parser: 'markdownit', - function: (params, onError) => { - filterTokens(params, 'inline', (token) => { + function: (params: RuleParams, onError: RuleErrorCallback) => { + filterTokens(params, 'inline', (token: any) => { const { children, line } = token let inLink = false for (const child of children) { diff --git a/src/content-linter/tests/learning-track-liquid.js b/src/content-linter/tests/learning-track-liquid.ts similarity index 74% rename from src/content-linter/tests/learning-track-liquid.js rename to src/content-linter/tests/learning-track-liquid.ts index b71a4b997a88..a9e6e69f5611 100644 --- a/src/content-linter/tests/learning-track-liquid.js +++ b/src/content-linter/tests/learning-track-liquid.ts @@ -18,7 +18,8 @@ describe('lint learning tracks', () => { if (yamlFileList.length < 1) return describe.each(yamlFileList)('%s', (yamlAbsPath) => { - let yamlContent + // Using any type because YAML content structure is dynamic and varies per file + let yamlContent: any beforeAll(async () => { const fileContents = await readFile(yamlAbsPath, 'utf8') @@ -26,8 +27,10 @@ describe('lint learning tracks', () => { }) test('contains valid liquid', () => { - const toLint = [] - Object.values(yamlContent).forEach(({ title, description }) => { + // Using any[] for toLint since it contains mixed string content from various YAML properties + const toLint: any[] = [] + // Using any for destructured params as YAML structure varies across different learning track files + Object.values(yamlContent).forEach(({ title, description }: any) => { toLint.push(title) toLint.push(description) }) diff --git a/src/content-linter/tests/unit/code-annotations.js b/src/content-linter/tests/unit/code-annotations.ts similarity index 100% rename from src/content-linter/tests/unit/code-annotations.js rename to src/content-linter/tests/unit/code-annotations.ts diff --git a/src/content-linter/tests/unit/frontmatter-hidden-docs.js b/src/content-linter/tests/unit/frontmatter-hidden-docs.ts similarity index 100% rename from src/content-linter/tests/unit/frontmatter-hidden-docs.js rename to src/content-linter/tests/unit/frontmatter-hidden-docs.ts diff --git a/src/content-linter/tests/unit/image-alt-text-length.js b/src/content-linter/tests/unit/image-alt-text-length.ts similarity index 81% rename from src/content-linter/tests/unit/image-alt-text-length.js rename to src/content-linter/tests/unit/image-alt-text-length.ts index 1e186bca150d..5990c3d10416 100644 --- a/src/content-linter/tests/unit/image-alt-text-length.js +++ b/src/content-linter/tests/unit/image-alt-text-length.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest' import { runRule } from '../../lib/init-test' import { incorrectAltTextLength } from '../../lib/linting-rules/image-alt-text-length' +import type { Rule } from '../../types' describe(incorrectAltTextLength.names.join(' - '), () => { test('image with incorrect alt text length fails', async () => { @@ -9,7 +10,7 @@ describe(incorrectAltTextLength.names.join(' - '), () => { `![${'x'.repeat(39)}](./image.png)`, `![${'x'.repeat(151)}](./image.png)`, ].join('\n') - const result = await runRule(incorrectAltTextLength, { strings: { markdown } }) + const result = await runRule(incorrectAltTextLength as Rule, { strings: { markdown } }) const errors = result.markdown expect(errors.length).toBe(2) expect(errors[0].lineNumber).toBe(1) @@ -22,7 +23,7 @@ describe(incorrectAltTextLength.names.join(' - '), () => { `![${'x'.repeat(40)}](./image.png)`, `![${'x'.repeat(150)}](./image.png)`, ].join('\n') - const result = await runRule(incorrectAltTextLength, { strings: { markdown } }) + const result = await runRule(incorrectAltTextLength as Rule, { strings: { markdown } }) const errors = result.markdown expect(errors.length).toBe(0) }) @@ -33,7 +34,7 @@ describe(incorrectAltTextLength.names.join(' - '), () => { // Completely empty '![](/images/this-is-ok.png)', ].join('\n') - const result = await runRule(incorrectAltTextLength, { strings: { markdown } }) + const result = await runRule(incorrectAltTextLength as Rule, { strings: { markdown } }) const errors = result.markdown expect(errors.length).toBe(1) expect(errors[0].lineNumber).toBe(3) diff --git a/src/content-linter/tests/unit/image-file-kebab-case.js b/src/content-linter/tests/unit/image-file-kebab-case.ts similarity index 100% rename from src/content-linter/tests/unit/image-file-kebab-case.js rename to src/content-linter/tests/unit/image-file-kebab-case.ts diff --git a/src/content-linter/tests/unit/image-no-gif.js b/src/content-linter/tests/unit/image-no-gif.ts similarity index 100% rename from src/content-linter/tests/unit/image-no-gif.js rename to src/content-linter/tests/unit/image-no-gif.ts diff --git a/src/content-linter/tests/unit/internal-links-no-lang.js b/src/content-linter/tests/unit/internal-links-no-lang.ts similarity index 87% rename from src/content-linter/tests/unit/internal-links-no-lang.js rename to src/content-linter/tests/unit/internal-links-no-lang.ts index 206a5e7755f5..e883adf5a55c 100644 --- a/src/content-linter/tests/unit/internal-links-no-lang.js +++ b/src/content-linter/tests/unit/internal-links-no-lang.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest' import { runRule } from '../../lib/init-test' import { internalLinksNoLang } from '../../lib/linting-rules/internal-links-no-lang' +import type { Rule } from '../../types' describe(internalLinksNoLang.names.join(' - '), () => { test('internal links with hardcoded language codes fail', async () => { @@ -10,7 +11,7 @@ describe(internalLinksNoLang.names.join(' - '), () => { '[Link to just a landing page in english](/en)', '[Korean Docs](/ko/actions)', ].join('\n') - const result = await runRule(internalLinksNoLang, { strings: { markdown } }) + const result = await runRule(internalLinksNoLang as Rule, { strings: { markdown } }) const errors = result.markdown expect(errors.length).toBe(3) expect(errors.map((error) => error.lineNumber)).toEqual([1, 2, 3]) @@ -31,7 +32,7 @@ describe(internalLinksNoLang.names.join(' - '), () => { // A link that starts with a language code '[Enterprise](/enterprise/overview)', ].join('\n') - const result = await runRule(internalLinksNoLang, { strings: { markdown } }) + const result = await runRule(internalLinksNoLang as Rule, { strings: { markdown } }) const errors = result.markdown expect(errors.length).toBe(0) }) diff --git a/src/content-linter/tests/unit/internal-links-old-version.js b/src/content-linter/tests/unit/internal-links-old-version.ts similarity index 85% rename from src/content-linter/tests/unit/internal-links-old-version.js rename to src/content-linter/tests/unit/internal-links-old-version.ts index b40909e66474..84c0510dd77e 100644 --- a/src/content-linter/tests/unit/internal-links-old-version.js +++ b/src/content-linter/tests/unit/internal-links-old-version.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest' import { runRule } from '../../lib/init-test' import { internalLinksOldVersion } from '../../lib/linting-rules/internal-links-old-version' +import type { Rule } from '../../types' describe(internalLinksOldVersion.names.join(' - '), () => { test('links with old hardcoded versioning fail', async () => { @@ -10,7 +11,7 @@ describe(internalLinksOldVersion.names.join(' - '), () => { '[Link to Enterprise 11.10.340](https://docs.github.com/enterprise/11.10.340/admin/yes)', '[Enterprise 2.8](http://help.github.com/enterprise/2.8/admin/)', ].join('\n') - const result = await runRule(internalLinksOldVersion, { strings: { markdown } }) + const result = await runRule(internalLinksOldVersion as Rule, { strings: { markdown } }) const errors = result.markdown expect(errors.length).toBe(3) expect(errors.map((error) => error.lineNumber)).toEqual([1, 2, 3]) @@ -26,7 +27,7 @@ describe(internalLinksOldVersion.names.join(' - '), () => { // Current versioning links is excluded from this test '[New versioning](/github/site-policy/enterprise/2.2/yes)', ].join('\n') - const result = await runRule(internalLinksOldVersion, { strings: { markdown } }) + const result = await runRule(internalLinksOldVersion as Rule, { strings: { markdown } }) const errors = result.markdown expect(errors.length).toBe(0) }) diff --git a/src/content-linter/tests/unit/link-quotation.js b/src/content-linter/tests/unit/link-quotation.ts similarity index 79% rename from src/content-linter/tests/unit/link-quotation.js rename to src/content-linter/tests/unit/link-quotation.ts index 52d57ab19f32..1e160672f691 100644 --- a/src/content-linter/tests/unit/link-quotation.js +++ b/src/content-linter/tests/unit/link-quotation.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest' import { runRule } from '../../lib/init-test' import { linkQuotation } from '../../lib/linting-rules/link-quotation' +import type { Rule } from '../../types' describe(linkQuotation.names.join(' - '), () => { test('links that are formatted correctly should not generate an error', async () => { @@ -9,7 +10,7 @@ describe(linkQuotation.names.join(' - '), () => { 'Random stuff [A title](./image.png)', '"This is a direct quote" [A title](./image.png)', ].join('\n') - const result = await runRule(linkQuotation, { strings: { markdown } }) + const result = await runRule(linkQuotation as Rule, { strings: { markdown } }) const errors = result.markdown expect(errors.length).toBe(0) }) @@ -26,11 +27,11 @@ describe(linkQuotation.names.join(' - '), () => { 'See "[AUTOTITLE](/foo/bar)," "[AUTOTITLE](/foo/bar2)," "[AUTOTITLE](/foo/bar3)," and "[AUTOTITLE](/foo/bar4)."', 'See "[Anchor link](#anchor-link)."', ].join('\n') - const result = await runRule(linkQuotation, { strings: { markdown } }) + const result = await runRule(linkQuotation as Rule, { strings: { markdown } }) const errors = result.markdown expect(errors.length).toBe(13) expect(errors[0].errorRange).toEqual([14, 25]) - expect(errors[0].fixInfo.insertText).toBe('[A title](./image.png).') - expect(errors[1].fixInfo.insertText).toBe('[A title](./image.png)?') + expect(errors[0].fixInfo?.insertText).toBe('[A title](./image.png).') + expect(errors[1].fixInfo?.insertText).toBe('[A title](./image.png)?') }) }) diff --git a/src/content-render/liquid/spotlight.js b/src/content-render/liquid/spotlight.js deleted file mode 100644 index 86c1d79476b3..000000000000 --- a/src/content-render/liquid/spotlight.js +++ /dev/null @@ -1,36 +0,0 @@ -export const tags = { - note: 'accent', - tip: 'success', - warning: 'attention', - danger: 'danger', -} - -const template = - '
{{ output }}
' - -export const Spotlight = { - type: 'block', - - parse(tagToken, remainTokens) { - this.tagName = tagToken.name - this.templates = [] - - const stream = this.liquid.parser.parseStream(remainTokens) - stream - .on(`tag:end${this.tagName}`, () => stream.stop()) - .on('template', (tpl) => this.templates.push(tpl)) - .on('end', () => { - throw new Error(`tag ${tagToken.getText()} not closed`) - }) - stream.start() - }, - - render: function* (scope) { - const output = yield this.liquid.renderer.renderTemplates(this.templates, scope) - - return yield this.liquid.parseAndRender(template, { - color: tags[this.tagName], - output, - }) - }, -} diff --git a/src/content-render/liquid/spotlight.ts b/src/content-render/liquid/spotlight.ts new file mode 100644 index 000000000000..3a895714092b --- /dev/null +++ b/src/content-render/liquid/spotlight.ts @@ -0,0 +1,64 @@ +interface LiquidToken { + name: string + getText(): string +} + +interface LiquidTemplate { + [key: string]: unknown +} + +interface LiquidStream { + on(event: string, callback: () => void): LiquidStream + stop(): void + start(): void +} + +interface LiquidEngine { + parser: { + parseStream(tokens: LiquidToken[]): LiquidStream + } + renderer: { + renderTemplates(templates: LiquidTemplate[], scope: Record): string + } + parseAndRender(template: string, context: Record): string +} + +export const tags: Record = { + note: 'accent', + tip: 'success', + warning: 'attention', + danger: 'danger', +} + +const template: string = + '
{{ output }}
' + +export const Spotlight = { + type: 'block' as const, + tagName: '' as string, + templates: [] as LiquidTemplate[], + liquid: null as LiquidEngine | null, + + parse(tagToken: LiquidToken, remainTokens: LiquidToken[]): void { + this.tagName = tagToken.name + this.templates = [] + + const stream = this.liquid!.parser.parseStream(remainTokens) + stream + .on(`tag:end${this.tagName}`, () => stream.stop()) + .on('template', (tpl: LiquidTemplate) => this.templates.push(tpl)) + .on('end', () => { + throw new Error(`tag ${tagToken.getText()} not closed`) + }) + stream.start() + }, + + render: function* (scope: Record): Generator { + const output = yield this.liquid!.renderer.renderTemplates(this.templates, scope) + + return yield this.liquid!.parseAndRender(template, { + color: tags[this.tagName], + output, + }) + }, +} diff --git a/src/content-render/unified/rewrite-thead-th-scope.js b/src/content-render/unified/rewrite-thead-th-scope.js deleted file mode 100644 index e5cd5c4d810a..000000000000 --- a/src/content-render/unified/rewrite-thead-th-scope.js +++ /dev/null @@ -1,35 +0,0 @@ -import { visitParents } from 'unist-util-visit-parents' - -/** - * Where it can mutate the AST to swap from: - * - * - * - * ... - * ... - * - * to: - * - * - * ... - * ... - * - * */ - -function matcher(node) { - return node.type === 'element' && node.tagName === 'th' && !('scope' in node.properties) -} - -function visitor(node, ancestors) { - const parent = ancestors.at(-1) - if (parent && parent.tagName === 'tr') { - const grandParent = ancestors.at(-2) - if (grandParent && grandParent.tagName === 'thead') { - node.properties.scope = 'col' - } - } -} - -export default function rewriteTheadThScope() { - return (tree) => visitParents(tree, matcher, visitor) -} diff --git a/src/content-render/unified/rewrite-thead-th-scope.ts b/src/content-render/unified/rewrite-thead-th-scope.ts new file mode 100644 index 000000000000..25ac2f9f15be --- /dev/null +++ b/src/content-render/unified/rewrite-thead-th-scope.ts @@ -0,0 +1,42 @@ +import { visitParents } from 'unist-util-visit-parents' +import type { Root } from 'hast' +import type { Transformer } from 'unified' + +/** + * Where it can mutate the AST to swap from: + * + * + * + * ... + * ... + * + * to: + * + * + * ... + * ... + * + * */ + +function matcher(node: any): boolean { + // Using any type due to complex type conflicts between different versions of + // @types/hast and @types/unist used by various dependencies. The node should be + // an Element with tagName 'th' and no existing 'scope' property. + return node.type === 'element' && node.tagName === 'th' && !('scope' in node.properties) +} + +function visitor(node: any, ancestors: any[]): void { + // Using any type for the same reason as matcher - complex type conflicts between + // hast/unist type definitions across different package versions + const parent = ancestors.at(-1) + if (parent && parent.tagName === 'tr') { + const grandParent = ancestors.at(-2) + if (grandParent && grandParent.tagName === 'thead') { + node.properties.scope = 'col' + } + } +} + +export default function rewriteTheadThScope(): Transformer { + return (tree: Root) => visitParents(tree, matcher, visitor) +} diff --git a/src/data-directory/lib/data-schemas/learning-tracks.js b/src/data-directory/lib/data-schemas/learning-tracks.ts similarity index 93% rename from src/data-directory/lib/data-schemas/learning-tracks.js rename to src/data-directory/lib/data-schemas/learning-tracks.ts index dcab4841cc74..765acc01251c 100644 --- a/src/data-directory/lib/data-schemas/learning-tracks.js +++ b/src/data-directory/lib/data-schemas/learning-tracks.ts @@ -2,7 +2,7 @@ import { schema } from '@/frame/lib/frontmatter' // Some learning tracks have `versions` blocks that match `versions` frontmatter, // so we can import that part of the FM schema. -const versionsProps = Object.assign({}, schema.properties.versions) +const versionsProps = Object.assign({}, (schema.properties as any).versions) // `versions` are not required in learning tracks the way they are in FM. delete versionsProps.required diff --git a/src/frame/lib/constants.js b/src/frame/lib/constants.ts similarity index 100% rename from src/frame/lib/constants.js rename to src/frame/lib/constants.ts diff --git a/src/frame/lib/get-toc-items.js b/src/frame/lib/get-toc-items.ts similarity index 51% rename from src/frame/lib/get-toc-items.js rename to src/frame/lib/get-toc-items.ts index fb0e6964e418..4cd40eb0ba8f 100644 --- a/src/frame/lib/get-toc-items.js +++ b/src/frame/lib/get-toc-items.ts @@ -1,4 +1,15 @@ import { productMap } from '@/products/lib/all-products' + +interface TocItem { + type: 'category' | 'subcategory' | 'article' + href: string +} + +interface Page { + relativePath: string + markdown: string +} + const productTOCs = Object.values(productMap) .filter((product) => !product.external) .map((product) => product.toc.replace('content/', '')) @@ -7,7 +18,7 @@ const linkString = /{% [^}]*?link.*? \/(.*?) ?%}/m const linksArray = new RegExp(linkString.source, 'gm') // return an array of objects like { type: 'category|subcategory|article', href: 'path' } -export default function getTocItems(page) { +export default function getTocItems(page: Page): TocItem[] | undefined { // only process product and category tocs if (!page.relativePath.endsWith('index.md')) return if (page.relativePath === 'index.md') return @@ -23,19 +34,24 @@ export default function getTocItems(page) { return [] } - return rawItems.map((item) => { - const tocItem = {} + return rawItems + .map((item: string) => { + const match = item.match(linkString) + if (!match) return null + + const tocItem: TocItem = {} as TocItem - // a product's toc items are always categories - // whereas a category's toc items can be either subcategories or articles - tocItem.type = productTOCs.includes(page.relativePath) - ? 'category' - : item.includes('topic_') - ? 'subcategory' - : 'article' + // a product's toc items are always categories + // whereas a category's toc items can be either subcategories or articles + tocItem.type = productTOCs.includes(page.relativePath) + ? 'category' + : page.relativePath.includes('/index.md') + ? 'subcategory' + : 'article' - tocItem.href = item.match(linkString)[1] + tocItem.href = match[1] - return tocItem - }) + return tocItem + }) + .filter((item): item is TocItem => item !== null) } diff --git a/src/frame/tests/content.js b/src/frame/tests/content.ts similarity index 66% rename from src/frame/tests/content.js rename to src/frame/tests/content.ts index 543c7dd6a1f7..624d3a81c642 100644 --- a/src/frame/tests/content.js +++ b/src/frame/tests/content.ts @@ -5,26 +5,35 @@ import walk from 'walk-sync' import createTree from '@/frame/lib/create-tree' +interface Page { + relativePath: string +} + +interface TreeNode { + page: Page + childPages?: TreeNode[] +} + describe('content files', () => { test.each(['content', 'src/fixtures/fixtures/content'])( 'no content files left orphaned without being in the tree in %s', - async (contentDir) => { + async (contentDir: string) => { const tree = await createTree(contentDir) - const traverse = (node) => { + const traverse = (node: TreeNode): string[] => { const relativeFiles = [node.page.relativePath] for (const child of node.childPages || []) { relativeFiles.push(...traverse(child)) } return relativeFiles } - const relativeFiles = traverse(tree).map((p) => path.join(contentDir, p)) + const relativeFiles = tree ? traverse(tree).map((p: string) => path.join(contentDir, p)) : [] const contentFiles = walk(contentDir, { includeBasePath: true, directories: false }).filter( - (file) => { + (file: string) => { return file.endsWith('.md') && !file.includes('README') }, - ) - const orphanedFiles = contentFiles.filter((file) => !relativeFiles.includes(file)) + ) as string[] + const orphanedFiles = contentFiles.filter((file: string) => !relativeFiles.includes(file)) expect( orphanedFiles.length, `${orphanedFiles} orphaned files found on disk but not in site tree`, diff --git a/src/frame/tests/find-page.js b/src/frame/tests/find-page.ts similarity index 61% rename from src/frame/tests/find-page.js rename to src/frame/tests/find-page.ts index 160fc438d51d..60189ae9f874 100644 --- a/src/frame/tests/find-page.js +++ b/src/frame/tests/find-page.ts @@ -18,7 +18,10 @@ describe('find page', () => { languageCode: 'en', }) - const englishPermalink = page.permalinks[0].href + const englishPermalink = page?.permalinks[0].href + if (!page || !englishPermalink) { + throw new Error('Page or permalink not found') + } const redirectToFind = '/some-old-path' // add named keys @@ -26,7 +29,12 @@ describe('find page', () => { [englishPermalink]: page, } - const redirectedPage = findPage(redirectToFind, pageMap, page.buildRedirects()) - expect(typeof redirectedPage.title).toBe('string') + const redirectedPage = findPage( + redirectToFind, + pageMap as any, // Using any due to type conflicts between different Page type definitions + page.buildRedirects(), + ) + expect(redirectedPage).toBeDefined() + expect(typeof redirectedPage?.title).toBe('string') }) }) diff --git a/src/graphql/scripts/utils/process-previews.js b/src/graphql/scripts/utils/process-previews.ts similarity index 64% rename from src/graphql/scripts/utils/process-previews.js rename to src/graphql/scripts/utils/process-previews.ts index 40b43a52aaaa..b333e9551881 100644 --- a/src/graphql/scripts/utils/process-previews.js +++ b/src/graphql/scripts/utils/process-previews.ts @@ -1,11 +1,27 @@ import { sentenceCase } from 'change-case' import GithubSlugger from 'github-slugger' + +interface RawPreview { + title: string + toggled_on: string[] + toggled_by: string + announcement?: unknown + updates?: unknown +} + +interface ProcessedPreview extends Omit { + accept_header: string + href: string +} + const slugger = new GithubSlugger() const inputOrPayload = /(Input|Payload)$/m -export default function processPreviews(previews) { +export default function processPreviews(previews: RawPreview[]): ProcessedPreview[] { // clean up raw yml data - previews.forEach((preview) => { + // Using any type because we're mutating the preview object to add new properties + // that don't exist in the RawPreview interface (accept_header, href) + previews.forEach((preview: any) => { preview.title = sentenceCase(preview.title) .replace(/ -.+/, '') // remove any extra info that follows a hyphen .replace('it hub', 'itHub') // fix overcorrected `git hub` from sentenceCasing @@ -16,7 +32,7 @@ export default function processPreviews(previews) { // filter out schema members that end in `Input` or `Payload` preview.toggled_on = preview.toggled_on.filter( - (schemaMember) => !inputOrPayload.test(schemaMember), + (schemaMember: string) => !inputOrPayload.test(schemaMember), ) // remove unnecessary leading colon @@ -32,5 +48,5 @@ export default function processPreviews(previews) { preview.href = `/graphql/overview/schema-previews#${slugger.slug(preview.title)}` }) - return previews + return previews as ProcessedPreview[] } diff --git a/src/observability/logger/lib/log-levels.js b/src/observability/logger/lib/log-levels.ts similarity index 67% rename from src/observability/logger/lib/log-levels.js rename to src/observability/logger/lib/log-levels.ts index d6f5373b2843..6294d4a45dbc 100644 --- a/src/observability/logger/lib/log-levels.js +++ b/src/observability/logger/lib/log-levels.ts @@ -12,6 +12,13 @@ export const LOG_LEVELS = { warn: 1, info: 2, debug: 3, +} as const + +type LogLevel = keyof typeof LOG_LEVELS +type LogLevelValue = (typeof LOG_LEVELS)[LogLevel] + +function isValidLogLevel(level: string): level is LogLevel { + return level in LOG_LEVELS } // We set the log level based on the LOG_LEVEL environment variable @@ -19,19 +26,22 @@ export const LOG_LEVELS = { // - 'info' in development // - 'debug' in production // - 'debug' in test - this is because `vitest` turns off logs unless --silent=false is passed -export function getLogLevelNumber() { - let defaultLogLevel = 'info' +export function getLogLevelNumber(): LogLevelValue { + let defaultLogLevel: LogLevel = 'info' if ( !process.env.LOG_LEVEL && (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') ) { defaultLogLevel = 'debug' } - const logLevel = process.env.LOG_LEVEL?.toLowerCase() || defaultLogLevel + + const envLogLevel = process.env.LOG_LEVEL?.toLowerCase() || defaultLogLevel + const logLevel = isValidLogLevel(envLogLevel) ? envLogLevel : defaultLogLevel + return LOG_LEVELS[logLevel] } -export const useProductionLogging = () => { +export const useProductionLogging = (): boolean => { return ( (process.env.NODE_ENV === 'production' && !process.env.CI) || process.env.LOG_LIKE_PRODUCTION === 'true' diff --git a/src/rest/scripts/utils/get-redirects.js b/src/rest/scripts/utils/get-redirects.ts similarity index 61% rename from src/rest/scripts/utils/get-redirects.js rename to src/rest/scripts/utils/get-redirects.ts index 6636b0a70d53..db39f3ff44f8 100644 --- a/src/rest/scripts/utils/get-redirects.js +++ b/src/rest/scripts/utils/get-redirects.ts @@ -1,10 +1,26 @@ import { readFile, writeFile } from 'fs/promises' + const STATIC_REDIRECTS = 'src/rest/data/client-side-rest-api-redirects.json' const REST_API_OVERRIDES = 'src/rest/lib/rest-api-overrides.json' +interface OperationUrl { + originalUrl: string + category: string + subcategory?: string +} + +interface RestApiOverrides { + operationUrls: Record + sectionUrls: Record +} + +interface RedirectMap { + [oldUrl: string]: string +} + // This is way to add redirects from one fragment to another from the // client's browser. -export async function syncRestRedirects() { +export async function syncRestRedirects(): Promise { const clientSideRedirects = await getClientSideRedirects() await writeFile(STATIC_REDIRECTS, JSON.stringify(clientSideRedirects, null, 2), 'utf8') @@ -13,11 +29,13 @@ export async function syncRestRedirects() { // Reads in src/rest/lib/rest-api-overrides.json and generates the // redirect file src/rest/data/client-side-rest-api-redirects.json -async function getClientSideRedirects() { - const { operationUrls, sectionUrls } = JSON.parse(await readFile(REST_API_OVERRIDES, 'utf8')) +async function getClientSideRedirects(): Promise { + const { operationUrls, sectionUrls }: RestApiOverrides = JSON.parse( + await readFile(REST_API_OVERRIDES, 'utf8'), + ) - const operationRedirects = {} - Object.values(operationUrls).forEach((value) => { + const operationRedirects: RedirectMap = {} + Object.values(operationUrls).forEach((value: OperationUrl) => { const oldUrl = value.originalUrl.replace('/rest/reference', '/rest') const anchor = oldUrl.split('#')[1] const subcategory = value.subcategory @@ -26,7 +44,7 @@ async function getClientSideRedirects() { : `/rest/${value.category}#${anchor}` operationRedirects[oldUrl] = redirectTo }) - const redirects = { + const redirects: RedirectMap = { ...operationRedirects, ...sectionUrls, } diff --git a/src/versions/tests/enterprise-versions.js b/src/versions/tests/enterprise-versions.ts similarity index 100% rename from src/versions/tests/enterprise-versions.js rename to src/versions/tests/enterprise-versions.ts