From a91e77679ac553abbd870b3fc3555d8be7e2ecc0 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 19 Sep 2025 11:57:12 -0700 Subject: [PATCH] Convert 12 JavaScript files to TypeScript (#57575) --- ...s.js => github-owned-action-references.ts} | 10 +++-- ...rsioning.js => table-liquid-versioning.ts} | 12 +++-- .../unified/{text-only.js => text-only.ts} | 5 ++- .../data-schemas/{features.js => features.ts} | 22 ++++++---- .../lib/data-schemas/glossaries-candidates.js | 18 -------- .../lib/data-schemas/glossaries-candidates.ts | 43 ++++++++++++++++++ .../lib/data-schemas/glossaries-external.js | 18 -------- .../lib/data-schemas/glossaries-external.ts | 44 +++++++++++++++++++ ...{filename-to-key.js => filename-to-key.ts} | 6 +-- src/frame/tests/{api.js => api.ts} | 4 +- .../scripts/utils/process-upcoming-changes.js | 15 ------- .../scripts/utils/process-upcoming-changes.ts | 28 ++++++++++++ src/rest/lib/{config.js => config.ts} | 6 +-- src/tests/helpers/caching-headers.js | 16 ------- src/tests/helpers/caching-headers.ts | 34 ++++++++++++++ ...es-range.js => version-satisfies-range.ts} | 14 +++++- 16 files changed, 200 insertions(+), 95 deletions(-) rename src/content-linter/tests/unit/{github-owned-action-references.js => github-owned-action-references.ts} (76%) rename src/content-linter/tests/unit/{table-liquid-versioning.js => table-liquid-versioning.ts} (61%) rename src/content-render/unified/{text-only.js => text-only.ts} (82%) rename src/data-directory/lib/data-schemas/{features.js => features.ts} (54%) delete mode 100644 src/data-directory/lib/data-schemas/glossaries-candidates.js create mode 100644 src/data-directory/lib/data-schemas/glossaries-candidates.ts delete mode 100644 src/data-directory/lib/data-schemas/glossaries-external.js create mode 100644 src/data-directory/lib/data-schemas/glossaries-external.ts rename src/data-directory/tests/{filename-to-key.js => filename-to-key.ts} (68%) rename src/frame/tests/{api.js => api.ts} (84%) delete mode 100644 src/graphql/scripts/utils/process-upcoming-changes.js create mode 100644 src/graphql/scripts/utils/process-upcoming-changes.ts rename src/rest/lib/{config.js => config.ts} (82%) delete mode 100644 src/tests/helpers/caching-headers.js create mode 100644 src/tests/helpers/caching-headers.ts rename src/versions/lib/{version-satisfies-range.js => version-satisfies-range.ts} (66%) diff --git a/src/content-linter/tests/unit/github-owned-action-references.js b/src/content-linter/tests/unit/github-owned-action-references.ts similarity index 76% rename from src/content-linter/tests/unit/github-owned-action-references.js rename to src/content-linter/tests/unit/github-owned-action-references.ts index c502cb87ec..371132cb8b 100644 --- a/src/content-linter/tests/unit/github-owned-action-references.js +++ b/src/content-linter/tests/unit/github-owned-action-references.ts @@ -4,8 +4,8 @@ import { runRule } from '../../lib/init-test' import { githubOwnedActionReferences } from '../../lib/linting-rules/github-owned-action-references' describe(githubOwnedActionReferences.names.join(' - '), () => { - test('Using hardcoded GitHub-owned actions causes error', async () => { - const markdown = [ + test('Using hardcoded GitHub-owned actions causes error', async (): Promise => { + const markdown: string = [ 'Hello actions/checkout@v2 apps.', 'A actions/delete-package-versions@v2 for apps.', 'Hello actions/download-artifact@v2.', @@ -13,7 +13,11 @@ describe(githubOwnedActionReferences.names.join(' - '), () => { 'actions/cache@', '[link title](/actions/cache)', ].join('\n') - const result = await runRule(githubOwnedActionReferences, { strings: { markdown } }) + const result = await runRule(githubOwnedActionReferences, { + strings: { markdown }, + files: undefined, + ruleConfig: true, + }) const errors = result.markdown expect(errors.length).toBe(3) }) diff --git a/src/content-linter/tests/unit/table-liquid-versioning.js b/src/content-linter/tests/unit/table-liquid-versioning.ts similarity index 61% rename from src/content-linter/tests/unit/table-liquid-versioning.js rename to src/content-linter/tests/unit/table-liquid-versioning.ts index 1527ba3f21..4563c14ad4 100644 --- a/src/content-linter/tests/unit/table-liquid-versioning.js +++ b/src/content-linter/tests/unit/table-liquid-versioning.ts @@ -6,12 +6,16 @@ import { tableLiquidVersioning } from '../../lib/linting-rules/table-liquid-vers const FIXTURE_FILEPATH = 'src/content-linter/tests/fixtures/tables.md' describe(tableLiquidVersioning.names.join(' - '), () => { - test('non-early access file with early access references fails', async () => { - const result = await runRule(tableLiquidVersioning, { files: [FIXTURE_FILEPATH] }) + test('non-early access file with early access references fails', async (): Promise => { + const result = await runRule(tableLiquidVersioning, { + strings: undefined, + files: [FIXTURE_FILEPATH], + ruleConfig: true, + }) const errors = result[FIXTURE_FILEPATH] expect(errors.length).toBe(11) - const lineNumbers = errors.map((error) => error.lineNumber) - const expectedErrorLines = [38, 40, 43, 44, 51, 53, 54, 55, 57, 58, 59] + const lineNumbers: number[] = errors.map((error) => error.lineNumber) + const expectedErrorLines: number[] = [38, 40, 43, 44, 51, 53, 54, 55, 57, 58, 59] expect(JSON.stringify(lineNumbers)).toEqual(JSON.stringify(expectedErrorLines)) }) }) diff --git a/src/content-render/unified/text-only.js b/src/content-render/unified/text-only.ts similarity index 82% rename from src/content-render/unified/text-only.js rename to src/content-render/unified/text-only.ts index 86138c1b70..6488b1f905 100644 --- a/src/content-render/unified/text-only.js +++ b/src/content-render/unified/text-only.ts @@ -7,11 +7,12 @@ import { decode } from 'html-entities' // Take advantage of the subtle fact that a lot of the times, the html value // we get here is a single line that starts with `

` and ends with `

` // and contains no longer HTML tags. -export function fastTextOnly(html) { +export function fastTextOnly(html: string): string { if (!html) return '' if (html.startsWith('

') && html.endsWith('

')) { const middle = html.slice(3, -4) if (!middle.includes('<')) return decode(middle.trim()) } - return cheerio.load(html, { xmlMode: true }).text().trim() + const $ = cheerio.load(html, { xmlMode: true }) + return $.root().text().trim() } diff --git a/src/data-directory/lib/data-schemas/features.js b/src/data-directory/lib/data-schemas/features.ts similarity index 54% rename from src/data-directory/lib/data-schemas/features.js rename to src/data-directory/lib/data-schemas/features.ts index d75bc2a7ec..8cf101ff16 100644 --- a/src/data-directory/lib/data-schemas/features.js +++ b/src/data-directory/lib/data-schemas/features.ts @@ -1,20 +1,24 @@ import { schema } from '@/frame/lib/frontmatter' -// Copy the properties from the frontmatter schema. -const featureVersions = { +interface FeatureVersionsSchema { + type: 'object' properties: { - versions: Object.assign({}, schema.properties.versions), + versions: any + } + additionalProperties: false +} + +// Copy the properties from the frontmatter schema. +const featureVersions: FeatureVersionsSchema = { + type: 'object', + properties: { + versions: Object.assign({}, (schema.properties as any).versions), }, + additionalProperties: false, } // Remove the feature versions properties. // We don't want to allow features within features! We just want pure versioning. delete featureVersions.properties.versions.properties.feature -// Call it invalid if any properties other than version properties are found. -featureVersions.additionalProperties = false - -// avoid ajv strict warning -featureVersions.type = 'object' - export default featureVersions diff --git a/src/data-directory/lib/data-schemas/glossaries-candidates.js b/src/data-directory/lib/data-schemas/glossaries-candidates.js deleted file mode 100644 index 0a73712100..0000000000 --- a/src/data-directory/lib/data-schemas/glossaries-candidates.js +++ /dev/null @@ -1,18 +0,0 @@ -export const term = { - type: 'string', - minLength: 1, - pattern: '^((?!\\*).)*$', // no asterisks allowed -} - -export default { - type: 'array', - items: { - type: 'object', - required: ['term'], - additionalProperties: false, - properties: { - term, - }, - }, - minItems: 21, -} diff --git a/src/data-directory/lib/data-schemas/glossaries-candidates.ts b/src/data-directory/lib/data-schemas/glossaries-candidates.ts new file mode 100644 index 0000000000..cfe6393c32 --- /dev/null +++ b/src/data-directory/lib/data-schemas/glossaries-candidates.ts @@ -0,0 +1,43 @@ +export interface TermSchema { + type: 'string' + minLength: number + pattern: string +} + +export const term: TermSchema = { + type: 'string', + minLength: 1, + pattern: '^((?!\\*).)*$', // no asterisks allowed +} + +export interface GlossaryCandidateItem { + term: string +} + +export interface GlossaryCandidatesSchema { + type: 'array' + items: { + type: 'object' + required: ['term'] + additionalProperties: false + properties: { + term: TermSchema + } + } + minItems: number +} + +const schema: GlossaryCandidatesSchema = { + type: 'array', + items: { + type: 'object', + required: ['term'], + additionalProperties: false, + properties: { + term, + }, + }, + minItems: 21, +} + +export default schema diff --git a/src/data-directory/lib/data-schemas/glossaries-external.js b/src/data-directory/lib/data-schemas/glossaries-external.js deleted file mode 100644 index d37102e20d..0000000000 --- a/src/data-directory/lib/data-schemas/glossaries-external.js +++ /dev/null @@ -1,18 +0,0 @@ -import { term } from './glossaries-candidates' - -export default { - type: 'array', - items: { - type: 'object', - required: ['term', 'description'], - additionalProperties: false, - properties: { - term, - description: { - type: 'string', - lintable: true, - }, - }, - }, - minItems: 21, -} diff --git a/src/data-directory/lib/data-schemas/glossaries-external.ts b/src/data-directory/lib/data-schemas/glossaries-external.ts new file mode 100644 index 0000000000..e6bbe3f589 --- /dev/null +++ b/src/data-directory/lib/data-schemas/glossaries-external.ts @@ -0,0 +1,44 @@ +import { term, type TermSchema } from './glossaries-candidates' + +export interface GlossaryExternalItem { + term: string + description: string +} + +export interface DescriptionSchema { + type: 'string' + lintable: boolean +} + +export interface GlossariesExternalSchema { + type: 'array' + items: { + type: 'object' + required: ['term', 'description'] + additionalProperties: false + properties: { + term: TermSchema + description: DescriptionSchema + } + } + minItems: number +} + +const schema: GlossariesExternalSchema = { + type: 'array', + items: { + type: 'object', + required: ['term', 'description'], + additionalProperties: false, + properties: { + term, + description: { + type: 'string', + lintable: true, + }, + }, + }, + minItems: 21, +} + +export default schema diff --git a/src/data-directory/tests/filename-to-key.js b/src/data-directory/tests/filename-to-key.ts similarity index 68% rename from src/data-directory/tests/filename-to-key.js rename to src/data-directory/tests/filename-to-key.ts index 88b47ae37c..6a483b3e81 100644 --- a/src/data-directory/tests/filename-to-key.js +++ b/src/data-directory/tests/filename-to-key.ts @@ -3,15 +3,15 @@ import { describe, expect, test } from 'vitest' import filenameToKey from '@/data-directory/lib/filename-to-key' describe('filename-to-key', () => { - test('converts filenames to object keys', () => { + test('converts filenames to object keys', (): void => { expect(filenameToKey('foo/bar/baz.txt')).toBe('foo.bar.baz') }) - test('ignores leading slash on filenames', () => { + test('ignores leading slash on filenames', (): void => { expect(filenameToKey('/foo/bar/baz.txt')).toBe('foo.bar.baz') }) - test('supports MS Windows paths', () => { + test('supports MS Windows paths', (): void => { expect(filenameToKey('path\\to\\file.txt')).toBe('path.to.file') }) }) diff --git a/src/frame/tests/api.js b/src/frame/tests/api.ts similarity index 84% rename from src/frame/tests/api.js rename to src/frame/tests/api.ts index a7b26676de..4ff4d57770 100644 --- a/src/frame/tests/api.js +++ b/src/frame/tests/api.ts @@ -5,13 +5,13 @@ import { get } from '@/tests/helpers/e2etest' describe('general /api pages', () => { vi.setConfig({ testTimeout: 60 * 1000 }) - test("any /api URL that isn't found should JSON", async () => { + test("any /api URL that isn't found should JSON", async (): Promise => { const res = await get('/api') expect(res.statusCode).toBe(404) expect(res.headers['content-type']).toMatch(/application\/json/) }) - test("any /api/* URL that isn't found should be JSON", async () => { + test("any /api/* URL that isn't found should be JSON", async (): Promise => { const res = await get('/api/yadayada') expect(res.statusCode).toBe(404) expect(JSON.parse(res.body).error).toBe('/yadayada not found') diff --git a/src/graphql/scripts/utils/process-upcoming-changes.js b/src/graphql/scripts/utils/process-upcoming-changes.js deleted file mode 100644 index c7fb0b9b5a..0000000000 --- a/src/graphql/scripts/utils/process-upcoming-changes.js +++ /dev/null @@ -1,15 +0,0 @@ -import yaml from 'js-yaml' -import { groupBy } from 'lodash-es' -import { renderContent } from '@/content-render/index' - -export default async function processUpcomingChanges(upcomingChangesYml) { - const upcomingChanges = yaml.load(upcomingChangesYml).upcoming_changes - - for (const change of upcomingChanges) { - change.date = change.date.slice(0, 10) - change.reason = await renderContent(change.reason) - change.description = await renderContent(change.description) - } - - return groupBy(upcomingChanges.reverse(), 'date') -} diff --git a/src/graphql/scripts/utils/process-upcoming-changes.ts b/src/graphql/scripts/utils/process-upcoming-changes.ts new file mode 100644 index 0000000000..99167eb98b --- /dev/null +++ b/src/graphql/scripts/utils/process-upcoming-changes.ts @@ -0,0 +1,28 @@ +import yaml from 'js-yaml' +import { groupBy } from 'lodash-es' +import { renderContent } from '@/content-render/index' + +interface UpcomingChange { + date: string + reason: string + description: string + [key: string]: unknown +} + +interface UpcomingChangesData { + upcoming_changes: UpcomingChange[] +} + +export default async function processUpcomingChanges( + upcomingChangesYml: string, +): Promise> { + const upcomingChanges = (yaml.load(upcomingChangesYml) as UpcomingChangesData).upcoming_changes + + for (const change of upcomingChanges) { + change.date = change.date.slice(0, 10) + change.reason = await renderContent(change.reason) + change.description = await renderContent(change.description) + } + + return groupBy(upcomingChanges.reverse(), 'date') +} diff --git a/src/rest/lib/config.js b/src/rest/lib/config.ts similarity index 82% rename from src/rest/lib/config.js rename to src/rest/lib/config.ts index b2848017ed..a1bdb88dd5 100644 --- a/src/rest/lib/config.js +++ b/src/rest/lib/config.ts @@ -5,15 +5,15 @@ // in the JSON file. // These paths must match the paths in src/pages/[versionId]/rest -export const nonAutomatedRestPaths = [ +export const nonAutomatedRestPaths: readonly string[] = [ '/rest/quickstart', '/rest/about-the-rest-api', '/rest/using-the-rest-api', '/rest/authentication', '/rest/guides', -] +] as const // This path is used to set the page in the // src/rest/components/ApiVersionPicker.tsx component. That component // has a link to the page that describes what api versioning is. -export const apiVersionPath = '/rest/about-the-rest-api/api-versions' +export const apiVersionPath: string = '/rest/about-the-rest-api/api-versions' diff --git a/src/tests/helpers/caching-headers.js b/src/tests/helpers/caching-headers.js deleted file mode 100644 index c36e5b63ed..0000000000 --- a/src/tests/helpers/caching-headers.js +++ /dev/null @@ -1,16 +0,0 @@ -import { expect } from 'vitest' - -import { SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key' - -export function checkCachingHeaders(res, defaultSurrogateKey = false, minMaxAge = 60 * 60) { - expect(res.headers['set-cookie']).toBeUndefined() - expect(res.headers['cache-control']).toContain('public') - const maxAgeSeconds = parseInt(res.headers['cache-control'].match(/max-age=(\d+)/)[1], 10) - // Let's not be too specific in the tests, just as long as it's testing - // that it's a reasonably large number of seconds. - expect(maxAgeSeconds).toBeGreaterThanOrEqual(minMaxAge) - // Because it doesn't have a unique URL - expect(res.headers['surrogate-key'].split(/\s/g)[0]).toBe( - defaultSurrogateKey ? SURROGATE_ENUMS.DEFAULT : SURROGATE_ENUMS.MANUAL, - ) -} diff --git a/src/tests/helpers/caching-headers.ts b/src/tests/helpers/caching-headers.ts new file mode 100644 index 0000000000..dc5474578f --- /dev/null +++ b/src/tests/helpers/caching-headers.ts @@ -0,0 +1,34 @@ +import { expect } from 'vitest' + +import { SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key' + +interface ResponseWithHeaders { + headers: Record +} + +export function checkCachingHeaders( + res: ResponseWithHeaders, + defaultSurrogateKey: boolean = false, + minMaxAge: number = 60 * 60, +): void { + expect(res.headers['set-cookie']).toBeUndefined() + expect(res.headers['cache-control']).toContain('public') + + const cacheControlHeader = res.headers['cache-control'] as string + const maxAgeMatch = cacheControlHeader.match(/max-age=(\d+)/) + + if (!maxAgeMatch) { + throw new Error('Cache-Control header does not contain max-age directive') + } + + const maxAgeSeconds = parseInt(maxAgeMatch[1], 10) + // Let's not be too specific in the tests, just as long as it's testing + // that it's a reasonably large number of seconds. + expect(maxAgeSeconds).toBeGreaterThanOrEqual(minMaxAge) + + // Because it doesn't have a unique URL + const surrogateKeyHeader = res.headers['surrogate-key'] as string + expect(surrogateKeyHeader.split(/\s/g)[0]).toBe( + defaultSurrogateKey ? SURROGATE_ENUMS.DEFAULT : SURROGATE_ENUMS.MANUAL, + ) +} diff --git a/src/versions/lib/version-satisfies-range.js b/src/versions/lib/version-satisfies-range.ts similarity index 66% rename from src/versions/lib/version-satisfies-range.js rename to src/versions/lib/version-satisfies-range.ts index 79ef8dbd5c..b4abc8089e 100644 --- a/src/versions/lib/version-satisfies-range.js +++ b/src/versions/lib/version-satisfies-range.ts @@ -2,7 +2,12 @@ import semver from 'semver' // Where "release" is a release number, like `3.1` for Enterprise Server, // and "range" is a semver range operator with another number, like `<=3.2`. -export default function versionSatisfiesRange(release, range) { +export default function versionSatisfiesRange(release: string | undefined, range: string): boolean { + // Handle undefined release + if (!release) { + return false + } + // workaround for Enterprise Server 11.10.340 because we can't use semver to // compare it to 2.x like we can with 2.0+ if (release === '11.10.340') return range.startsWith('<') @@ -15,5 +20,10 @@ export default function versionSatisfiesRange(release, range) { release = '1.0' } - return semver.satisfies(semver.coerce(release), range) + const coercedRelease = semver.coerce(release) + if (!coercedRelease) { + throw new Error(`Unable to coerce release version: ${release}`) + } + + return semver.satisfies(coercedRelease, range) }