diff --git a/eslint.config.ts b/eslint.config.ts index 8f1bdaf46126..b4b7a1af3dd4 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -225,12 +225,10 @@ export default [ // Legacy files with @typescript-eslint/no-explicit-any violations (see github/docs-engineering#5797) { files: [ - 'src/article-api/scripts/generate-api-docs.ts', 'src/article-api/transformers/audit-logs-transformer.ts', 'src/article-api/transformers/rest-transformer.ts', 'src/codeql-cli/scripts/convert-markdown-for-docs.ts', 'src/content-linter/scripts/lint-content.ts', - 'src/content-render/liquid/index.ts', 'src/content-render/scripts/liquid-tags.ts', 'src/content-render/scripts/move-content.ts', @@ -248,11 +246,8 @@ export default [ 'src/graphql/scripts/utils/schema-helpers.ts', 'src/graphql/tests/validate-schema.ts', 'src/landings/components/CookBookFilter.tsx', - 'src/landings/components/SidebarProduct.tsx', - 'src/landings/pages/product.tsx', 'src/languages/lib/correct-translation-content.ts', 'src/languages/lib/render-with-fallback.ts', - 'src/languages/lib/translation-utils.ts', 'src/links/lib/update-internal-links.ts', 'src/links/scripts/check-github-github-links.ts', 'src/rest/components/get-rest-code-samples.ts', @@ -266,20 +261,16 @@ export default [ 'src/rest/scripts/utils/update-markdown.ts', 'src/rest/tests/get-rest-code-samples-2.ts', 'src/rest/tests/get-rest-code-samples.ts', - 'src/rest/tests/openapi-schema.ts', 'src/rest/tests/rendering.ts', 'src/search/components/hooks/useAISearchAutocomplete.ts', 'src/search/components/hooks/useAISearchLocalStorageCache.ts', - 'src/search/components/input/AskAIResults.tsx', 'src/search/components/input/SearchOverlay.tsx', 'src/search/lib/get-elasticsearch-results/ai-search-autocomplete.ts', 'src/search/lib/get-elasticsearch-results/general-search.ts', 'src/search/lib/routes/combined-search-route.ts', 'src/search/lib/search-request-params/get-search-from-request-params.ts', - 'src/search/middleware/search-routes.ts', 'src/search/scripts/index/index-cli.ts', 'src/search/scripts/index/utils/indexing-elasticsearch-utils.ts', - 'src/search/scripts/scrape/lib/parse-page-sections-into-records.ts', 'src/tests/helpers/check-url.ts', 'src/tests/scripts/copy-fixture-data.ts', 'src/tests/vitest.setup.ts', diff --git a/package-lock.json b/package-lock.json index 4c514a1adb5c..d71830f02528 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "imurmurhash": "^0.1.4", "is-svg": "6.0.0", "javascript-stringify": "^2.1.0", - "js-cookie": "^3.0.5", + "js-cookie": "^3.0.7", "js-yaml": "^4.1.1", "liquidjs": "^10.25.7", "lodash": "^4.18.0", @@ -12279,12 +12279,12 @@ } }, "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz", + "integrity": "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/js-tokens": { diff --git a/package.json b/package.json index c94f5fc265c7..c2f0a55689f7 100644 --- a/package.json +++ b/package.json @@ -223,7 +223,7 @@ "imurmurhash": "^0.1.4", "is-svg": "6.0.0", "javascript-stringify": "^2.1.0", - "js-cookie": "^3.0.5", + "js-cookie": "^3.0.7", "js-yaml": "^4.1.1", "liquidjs": "^10.25.7", "lodash": "^4.18.0", diff --git a/src/article-api/scripts/generate-api-docs.ts b/src/article-api/scripts/generate-api-docs.ts index 3e09c2764426..ba1b64c5f83d 100644 --- a/src/article-api/scripts/generate-api-docs.ts +++ b/src/article-api/scripts/generate-api-docs.ts @@ -1,5 +1,15 @@ import { writeFileSync, readFileSync, existsSync } from 'fs' +type ApiDoc = { + method: string + path: string + description: string + params: string[] + returns: string + examples: string + throws: string[] +} + function main({ sources, outputPath }: { sources: string[]; outputPath: string }): void { // Extract API documentation comments from all source files const allDocs = sources.flatMap((sourcePath) => extractApiDocs(sourcePath)) @@ -14,8 +24,8 @@ function main({ sources, outputPath }: { sources: string[]; outputPath: string } } // Extract API docs from comments in the file -function extractApiDocs(file: string): string[] { - const apiDocs: any[] = [] +function extractApiDocs(file: string): ApiDoc[] { + const apiDocs: ApiDoc[] = [] // get the content from the api routes const content = readFileSync(file, 'utf8') @@ -114,7 +124,7 @@ function extractExample(commentBlock: string): string { } // Generate markdown from parsed documentation -function generateMarkdown(apiDocs: any[]): string { +function generateMarkdown(apiDocs: ApiDoc[]): string { let markdown = '## Reference: API endpoints\n\n' for (const doc of apiDocs) { diff --git a/src/landings/components/SidebarProduct.tsx b/src/landings/components/SidebarProduct.tsx index 833e978d8e1b..a03261ac7830 100644 --- a/src/landings/components/SidebarProduct.tsx +++ b/src/landings/components/SidebarProduct.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { NavList } from '@primer/react' import { ProductTreeNode, useMainContext } from '@/frame/components/context/MainContext' @@ -172,8 +172,8 @@ function RestNavListItem({ category }: { category: ProductTreeNode }) { { + // Using React.MouseEvent because Primer React's NavList doesn't export proper event types + onClick={(event: React.MouseEvent) => { event.preventDefault() push(childPage.href) }} diff --git a/src/landings/pages/product.tsx b/src/landings/pages/product.tsx index 5f4eb31f8b84..138524af7cfe 100644 --- a/src/landings/pages/product.tsx +++ b/src/landings/pages/product.tsx @@ -1,7 +1,11 @@ import { useEffect } from 'react' import { GetServerSideProps } from 'next' +import type { Response } from 'express' import { useRouter } from 'next/router' +import type { ExtendedRequest } from '@/types' +import type { JourneyTrack } from '@/journeys/lib/journey-path-resolver' + // "legacy" javascript needed to maintain existing functionality // typically operating on elements **within** an article. import copyCode from '@/frame/components/lib/copy-code' @@ -132,8 +136,8 @@ const GlobalPage = ({ export default GlobalPage export const getServerSideProps: GetServerSideProps = async (context) => { - const req = context.req as any - const res = context.res as any + const req = context.req as unknown as ExtendedRequest + const res = context.res as unknown as Response const props: Props = { mainContext: await getMainContext(req, res), @@ -151,8 +155,9 @@ export const getServerSideProps: GetServerSideProps = async (context) => // journey tracks are resolved in middleware and added to the request // so we need to add them to the journey context here - if ((req.context.page as any).resolvedJourneyTracks) { - props.journeyContext.journeyTracks = (req.context.page as any).resolvedJourneyTracks + const page = req.context?.page as { resolvedJourneyTracks?: JourneyTrack[] } | undefined + if (page?.resolvedJourneyTracks) { + props.journeyContext.journeyTracks = page.resolvedJourneyTracks } additionalUINamespaces.push('journey_landing', 'product_landing') @@ -161,15 +166,21 @@ export const getServerSideProps: GetServerSideProps = async (context) => additionalUINamespaces.push('product_landing', 'carousels') } else if (relativePath?.endsWith('index.md')) { if (currentLayoutName === 'category-landing') { - props.categoryLandingContext = getCategoryLandingContextFromRequest(req) + props.categoryLandingContext = getCategoryLandingContextFromRequest( + req as unknown as Parameters[0], + ) } else { - props.tocLandingContext = getTocLandingContextFromRequest(req) + props.tocLandingContext = getTocLandingContextFromRequest( + req as unknown as Parameters[0], + ) } } else if (props.mainContext.page) { // All articles that might have hover cards needs this additionalUINamespaces.push('popovers') - props.articleContext = getArticleContextFromRequest(req) + props.articleContext = getArticleContextFromRequest( + req as unknown as Parameters[0], + ) if (props.articleContext.currentJourneyTrack?.trackId) { additionalUINamespaces.push('journey_track_nav') } diff --git a/src/languages/lib/translation-utils.ts b/src/languages/lib/translation-utils.ts index e190e94b87d1..56f31fdcefd0 100644 --- a/src/languages/lib/translation-utils.ts +++ b/src/languages/lib/translation-utils.ts @@ -87,7 +87,7 @@ export function createTranslationFunctions(uiData: UIStrings, namespaces: string return {} as UIStrings } }, - t: (strings: TemplateStringsArray | string, ...values: Array) => { + t: (strings: TemplateStringsArray | string, ...values: Array) => { const key = typeof strings === 'string' ? strings : String.raw(strings, ...values) // Provide specific fallbacks for common 404 page keys const commonFallbacks: Record = { diff --git a/src/rest/tests/openapi-schema.ts b/src/rest/tests/openapi-schema.ts index 55aca84121d6..9351ef5a3f90 100644 --- a/src/rest/tests/openapi-schema.ts +++ b/src/rest/tests/openapi-schema.ts @@ -5,18 +5,18 @@ import walk from 'walk-sync' import { isPlainObject, difference } from 'lodash-es' import { isApiVersioned, allVersions } from '@/versions/lib/all-versions' -import getRest, { getRestCategories } from '../lib/index' +import getRest, { getRestCategories, type RestOperationCategory } from '../lib/index' import readFrontmatter from '@/frame/lib/read-frontmatter' import frontmatter from '@/frame/lib/frontmatter' import getApplicableVersions from '../../versions/lib/get-applicable-versions' import { getAutomatedMarkdownFiles } from '../scripts/test-open-api-schema' import { nonAutomatedRestPaths } from '../lib/config' +import type { Operation } from '@/rest/components/types' const schemasPath = 'src/rest/data' -// Operations have dynamic structure from OpenAPI schema - using any to avoid complex type definitions -async function getFlatListOfOperations(version: string): Promise { - const flatList = [] +async function getFlatListOfOperations(version: string): Promise { + const flatList: Operation[] = [] if (isApiVersioned(version)) { for (const apiVersion of allVersions[version].apiVersions) { @@ -38,16 +38,17 @@ async function getFlatListOfOperations(version: string): Promise { describe('markdown for each rest version', () => { // Unique set of all categories across all versions of the OpenAPI schema const allCategories = new Set() - // Entire schema including categories and subcategories - using any due to dynamic OpenAPI structure - const openApiSchema: Record = {} + // Entire schema including categories and subcategories, keyed by version then category + const openApiSchema: Record> = {} // All applicable version of categories based on frontmatter in the categories index.md file - const categoryApplicableVersions: Record = {} + const categoryApplicableVersions: Record = {} function getApplicableVersionFromFile(file: string) { const currentFile = fs.readFileSync(file, 'utf8') - // Frontmatter data structure is dynamic based on file content - const { data } = frontmatter(currentFile) as { data: any } - return getApplicableVersions(data.versions, file) + const fm = frontmatter(currentFile) as unknown as { + data?: { versions?: string | Record } + } + return getApplicableVersions(fm.data?.versions, file) } function getCategorySubcategory(file: string) { @@ -127,12 +128,15 @@ describe('markdown for each rest version', () => { describe('rest file structure', () => { test('children of content/rest/index.md are in alphabetical order', async () => { const indexContent = fs.readFileSync('content/rest/index.md', 'utf8') - // Frontmatter data structure is dynamic based on file content - const { data } = readFrontmatter(indexContent) as { data: any } + const fm = readFrontmatter(indexContent) as unknown as { + data?: { children?: string[] } + } + const children = fm.data?.children + expect(Array.isArray(children)).toBe(true) const nonAutomatedChildren = nonAutomatedRestPaths.map((child: string) => child.replace('/rest', ''), ) - const sortableChildren = data.children.filter( + const sortableChildren = (children as string[]).filter( (child: string) => !nonAutomatedChildren.includes(child), ) expect(sortableChildren).toStrictEqual([...sortableChildren].sort()) @@ -203,11 +207,12 @@ describe('code examples are defined', () => { } const operation = await findOperation(version, 'GET', '/repos/{owner}/{repo}') + expect(operation).toBeDefined() + if (!operation) continue expect(operation.serverUrl).toBe(domain) expect(isPlainObject(operation)).toBe(true) expect(operation.codeExamples).toBeDefined() - // Code examples have dynamic structure from OpenAPI schema - for (const example of operation.codeExamples as any[]) { + for (const example of operation.codeExamples) { expect(isPlainObject(example.request)).toBe(true) expect(isPlainObject(example.response)).toBe(true) } diff --git a/src/search/components/input/AskAIResults.tsx b/src/search/components/input/AskAIResults.tsx index 4df1b247c23d..754a6d298534 100644 --- a/src/search/components/input/AskAIResults.tsx +++ b/src/search/components/input/AskAIResults.tsx @@ -235,12 +235,20 @@ export function AskAIResults({ let leftover = '' // <= carry‑over buffer setInitialLoading(false) - const processLine = (parsedLine: any) => { + type ParsedLine = { + chunkType?: string + conversation_id?: string + sources?: AIReference[] + text?: string + errors?: unknown + } + + const processLine = (parsedLine: ParsedLine) => { switch (parsedLine.chunkType) { // A conversation ID will still be sent when a question cannot be answered case 'CONVERSATION_ID': - conversationIdBuffer = parsedLine.conversation_id - setConversationId(parsedLine.conversation_id) + conversationIdBuffer = parsedLine.conversation_id ?? '' + setConversationId(parsedLine.conversation_id ?? '') break case 'NO_CONTENT_SIGNAL': @@ -251,7 +259,7 @@ export function AskAIResults({ case 'SOURCES': if (!isCancelled) { sourcesBuffer = uniqBy( - sourcesBuffer.concat(parsedLine.sources as AIReference[]), + sourcesBuffer.concat((parsedLine.sources ?? []) as AIReference[]), 'url', ) setReferences(sourcesBuffer) @@ -260,7 +268,7 @@ export function AskAIResults({ case 'MESSAGE_CHUNK': if (!isCancelled) { - messageBuffer += parsedLine.text + messageBuffer += parsedLine.text ?? '' setMessage(messageBuffer) } break @@ -298,7 +306,7 @@ export function AskAIResults({ for (const raw of lines) { if (!raw.trim()) continue - let parsedLine: any + let parsedLine: ParsedLine try { parsedLine = JSON.parse(raw) if (parsedLine?.errors) { @@ -331,7 +339,7 @@ export function AskAIResults({ console.warn('Failed to parse tail JSON:', leftover, err) } } - } catch (error: any) { + } catch (error: unknown) { if (!isCancelled) { console.error('Failed to fetch search results:', error) setAISearchError() diff --git a/src/search/middleware/search-routes.ts b/src/search/middleware/search-routes.ts index fdb85d187ddd..90ab88d3838f 100644 --- a/src/search/middleware/search-routes.ts +++ b/src/search/middleware/search-routes.ts @@ -30,23 +30,35 @@ router.get('/combined-search/v1', catchMiddlewareError(combinedSearchRoute)) export async function handleGetSearchResultsError( req: Request, res: Response, - error: any, - options: any, + error: unknown, + options: unknown, ) { + const errorMessage = error instanceof Error ? error.message : String(error) if (process.env.NODE_ENV === 'development') { console.error(`Error calling getSearchResults(${options})`, error) } else { - const reports = FailBot.report(error, { url: req.url, ...options }) + const extra: Record = + options && typeof options === 'object' && !Array.isArray(options) + ? Object.fromEntries( + Object.entries(options as Record).map(([k, v]) => [ + k, + v && typeof v === 'object' ? JSON.stringify(v) : v, + ]), + ) + : { options: typeof options === 'object' ? JSON.stringify(options) : options } + extra.url = req.url + const errorForReport = error instanceof Error ? error : new Error(errorMessage) + const reports = FailBot.report(errorForReport, extra) if (reports) await Promise.all(reports) } // Avoid "Cannot set headers after they are sent to the client" error // if response was already partially sent before the error occurred if (!res.headersSent) { - res.status(500).json({ error: error.message }) + res.status(500).json({ error: errorMessage }) } else { logger.warn('Response headers already sent; unable to send error response.', { url: req.url, - message: error?.message, + message: errorMessage, }) } }