From 0007f4f6a08cfa0f16c4b52545697a08a0903ba8 Mon Sep 17 00:00:00 2001 From: Kanad Gupta <8854718+kanadgupta@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:17:25 -0500 Subject: [PATCH] refactor: enable TS strict mode (#889) * chore: let's go baby * refactor: add new AuthenticatedCommandOptions type * chore: bring AuthenticatedCommandOptions into createGHA file * refactor: version parameter in `cleanHeaders` * chore: other little typefixes in fetch file * chore: more little TS fixes * chore: more straightforward type fixes * fix: various version typings * chore: improved typing on oas prompt * fix: enable another tsconfig flag * chore: add overdue stricter type i don't feel great about this but progress is progress lol * refactor: backfill some types so we can re-enable lib check * chore: temporarily disable strict mode to see if tests pass * chore: remove some unnecessary overrides * revert: actually bring back skipLibCheck * fix: do not fallback to json * test: fix test * revert: re-enable strictness * chore: stricter option typing * refactor: remove function nesting this diff is kinda crazy but no actual changes here. an external contributor just did a bunch of unnecessary function nesting and TS didn't like that. * chore: fix typing in analyzer * refactor: clearer type names * chore: pr feedback Co-Authored-By: Jon Ursenbach * fix: lint --------- Co-authored-by: Jon Ursenbach --- __tests__/cmds/docs/edit.test.ts | 2 +- __tests__/lib/fetch.test.ts | 16 ++++----- src/cmds/categories/create.ts | 56 +++++++++++++------------------- src/cmds/categories/index.ts | 4 +-- src/cmds/changelogs.ts | 4 +-- src/cmds/custompages.ts | 4 +-- src/cmds/docs/edit.ts | 22 +++---------- src/cmds/docs/index.ts | 4 +-- src/cmds/docs/prune.ts | 4 +-- src/cmds/guides/index.ts | 4 +-- src/cmds/guides/prune.ts | 4 +-- src/cmds/login.ts | 4 +-- src/cmds/logout.ts | 4 +-- src/cmds/oas.ts | 4 +-- src/cmds/open.ts | 4 +-- src/cmds/openapi/convert.ts | 4 +-- src/cmds/openapi/index.ts | 37 ++++++++++----------- src/cmds/openapi/inspect.ts | 12 +++---- src/cmds/openapi/reduce.ts | 8 ++--- src/cmds/openapi/validate.ts | 6 ++-- src/cmds/swagger.ts | 4 +-- src/cmds/validate.ts | 4 +-- src/cmds/versions/create.ts | 10 +++--- src/cmds/versions/delete.ts | 4 +-- src/cmds/versions/index.ts | 4 +-- src/cmds/versions/update.ts | 10 +++--- src/cmds/whoami.ts | 4 +-- src/index.ts | 9 ++--- src/lib/analyzeOas.ts | 19 +++++------ src/lib/apiError.ts | 2 +- src/lib/baseCommand.ts | 26 ++++++++------- src/lib/castStringOptToBool.ts | 5 ++- src/lib/createGHA/index.ts | 18 +++++----- src/lib/deleteDoc.ts | 12 ++----- src/lib/getCategories.ts | 20 +++--------- src/lib/getDocs.ts | 18 +++++----- src/lib/getPkgVersion.ts | 6 +++- src/lib/logger.ts | 3 +- src/lib/prepareOas.ts | 17 +++++++--- src/lib/prompts.ts | 39 ++++++++++++---------- src/lib/readDoc.ts | 3 +- src/lib/readdirRecursive.ts | 2 +- src/lib/readmeAPIFetch.ts | 25 +++++++++----- src/lib/syncDocsPath.ts | 32 ++++-------------- src/lib/versionSelect.ts | 12 +++++-- tsconfig.json | 5 ++- 46 files changed, 244 insertions(+), 276 deletions(-) diff --git a/__tests__/cmds/docs/edit.test.ts b/__tests__/cmds/docs/edit.test.ts index 51e2719c4..bc6ef87bc 100644 --- a/__tests__/cmds/docs/edit.test.ts +++ b/__tests__/cmds/docs/edit.test.ts @@ -90,7 +90,7 @@ describe('rdme docs:edit', () => { fs.appendFile(filename, edits, cb.bind(null, 0)); } - await expect(docsEdit.run({ slug, key, version: '1.0.0', mockEditor })).resolves.toBeUndefined(); + await expect(docsEdit.run({ slug, key, version: '1.0.0', mockEditor })).resolves.toBe(''); getMock.done(); putMock.done(); diff --git a/__tests__/lib/fetch.test.ts b/__tests__/lib/fetch.test.ts index 0f1e47ff3..b7236154e 100644 --- a/__tests__/lib/fetch.test.ts +++ b/__tests__/lib/fetch.test.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment, no-console */ +/* eslint-disable no-console */ import { Headers } from 'node-fetch'; import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; @@ -328,19 +328,17 @@ describe('#cleanHeaders()', () => { }); it('should filter out undefined headers', () => { - expect( - // @ts-ignore Testing a quirk of `node-fetch`. - Array.from(cleanHeaders('test', new Headers({ 'x-readme-version': undefined }))), - ).toStrictEqual([['authorization', 'Basic dGVzdDo=']]); + expect(Array.from(cleanHeaders('test', undefined, new Headers({ 'x-something': undefined })))).toStrictEqual([ + ['authorization', 'Basic dGVzdDo='], + ]); }); it('should filter out null headers', () => { expect( - // @ts-ignore Testing a quirk of `node-fetch`. - Array.from(cleanHeaders('test', new Headers({ 'x-readme-version': '1234', Accept: null }))), + Array.from(cleanHeaders('test', undefined, new Headers({ 'x-something': '1234', Accept: null }))), ).toStrictEqual([ ['authorization', 'Basic dGVzdDo='], - ['x-readme-version', '1234'], + ['x-something', '1234'], ]); }); @@ -351,7 +349,7 @@ describe('#cleanHeaders()', () => { 'Content-Type': 'application/json', }); - expect(Array.from(cleanHeaders('test', headers))).toStrictEqual([ + expect(Array.from(cleanHeaders('test', undefined, headers))).toStrictEqual([ ['authorization', 'Basic dGVzdDo='], ['accept', 'text/plain'], ['content-type', 'application/json'], diff --git a/src/cmds/categories/create.ts b/src/cmds/categories/create.ts index b7a900796..5258e80f7 100644 --- a/src/cmds/categories/create.ts +++ b/src/cmds/categories/create.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import chalk from 'chalk'; import { Headers } from 'node-fetch'; @@ -52,7 +52,7 @@ export default class CategoriesCreateCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { categoryType, title, key, version, preventDuplicates } = opts; @@ -69,45 +69,33 @@ export default class CategoriesCreateCommand extends Command { Command.debug(`selectedVersion: ${selectedVersion}`); - async function matchCategory() { + if (preventDuplicates) { const allCategories = await getCategories(key, selectedVersion); - return allCategories.find((category: Category) => { + const matchedCategory = allCategories.find((category: Category) => { return category.title.trim().toLowerCase() === title.trim().toLowerCase() && category.type === categoryType; }); - } - async function createCategory() { - if (preventDuplicates) { - const matchedCategory = await matchCategory(); - if (typeof matchedCategory !== 'undefined') { - return Promise.reject( - new Error( - `The '${matchedCategory.title}' category with a type of '${matchedCategory.type}' already exists with an id of '${matchedCategory.id}'. A new category was not created.`, - ), - ); - } + if (typeof matchedCategory !== 'undefined') { + return Promise.reject( + new Error( + `The '${matchedCategory.title}' category with a type of '${matchedCategory.type}' already exists with an id of '${matchedCategory.id}'. A new category was not created.`, + ), + ); } - return readmeAPIFetch('/api/v1/categories', { - method: 'post', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - ), - body: JSON.stringify({ - title, - type: categoryType, - }), - }) - .then(handleRes) - .then(res => `🌱 successfully created '${res.title}' with a type of '${res.type}' and an id of '${res.id}'`); } - const createdCategory = chalk.green(await createCategory()); - - return Promise.resolve(createdCategory); + const createdCategory = await readmeAPIFetch('/api/v1/categories', { + method: 'post', + headers: cleanHeaders(key, selectedVersion, new Headers({ 'Content-Type': 'application/json' })), + body: JSON.stringify({ + title, + type: categoryType, + }), + }) + .then(handleRes) + .then(res => `🌱 successfully created '${res.title}' with a type of '${res.type}' and an id of '${res.id}'`); + + return Promise.resolve(chalk.green(createdCategory)); } } diff --git a/src/cmds/categories/index.ts b/src/cmds/categories/index.ts index 8cf829ec0..492e54907 100644 --- a/src/cmds/categories/index.ts +++ b/src/cmds/categories/index.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import Command, { CommandCategories } from '../../lib/baseCommand'; import getCategories from '../../lib/getCategories'; @@ -16,7 +16,7 @@ export default class CategoriesCommand extends Command { this.args = [this.getKeyArg(), this.getVersionArg()]; } - async run(opts: CommandOptions<{}>) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { key, version } = opts; diff --git a/src/cmds/changelogs.ts b/src/cmds/changelogs.ts index ed72bde0d..2e5eb7f25 100644 --- a/src/cmds/changelogs.ts +++ b/src/cmds/changelogs.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../lib/baseCommand'; import Command, { CommandCategories } from '../lib/baseCommand'; import supportsGHA from '../lib/decorators/supportsGHA'; @@ -37,7 +37,7 @@ export default class ChangelogsCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { dryRun, filePath, key } = opts; diff --git a/src/cmds/custompages.ts b/src/cmds/custompages.ts index 9d127c609..8d1981d8c 100644 --- a/src/cmds/custompages.ts +++ b/src/cmds/custompages.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../lib/baseCommand'; import Command, { CommandCategories } from '../lib/baseCommand'; import supportsGHA from '../lib/decorators/supportsGHA'; @@ -37,7 +37,7 @@ export default class CustomPagesCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { dryRun, filePath, key } = opts; diff --git a/src/cmds/docs/edit.ts b/src/cmds/docs/edit.ts index 9305dd57a..63fdd681d 100644 --- a/src/cmds/docs/edit.ts +++ b/src/cmds/docs/edit.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import fs from 'fs'; import { promisify } from 'util'; @@ -45,7 +45,7 @@ export default class DocsEditCommand extends Command { ]; } - async run(opts: CommandOptions): Promise { + async run(opts: AuthenticatedCommandOptions): Promise { Command.warn('`rdme docs:edit` is now deprecated and will be removed in a future release.'); await super.run(opts); @@ -63,13 +63,7 @@ export default class DocsEditCommand extends Command { const existingDoc = await readmeAPIFetch(`/api/v1/docs/${slug}`, { method: 'get', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - Accept: 'application/json', - }), - ), + headers: cleanHeaders(key, selectedVersion, new Headers({ Accept: 'application/json' })), }).then(handleRes); await writeFile(filename, existingDoc.body); @@ -86,13 +80,7 @@ export default class DocsEditCommand extends Command { return readmeAPIFetch(`/api/v1/docs/${slug}`, { method: 'put', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - ), + headers: cleanHeaders(key, selectedVersion, new Headers({ 'Content-Type': 'application/json' })), body: JSON.stringify( Object.assign(existingDoc, { body: updatedDoc, @@ -115,7 +103,7 @@ export default class DocsEditCommand extends Command { // Normally we should resolve with a value that is logged to the console, // but since we need to wait for the temporary file to be removed, // it's okay to resolve the promise with no value. - return resolve(undefined); + return resolve(''); }); }); }); diff --git a/src/cmds/docs/index.ts b/src/cmds/docs/index.ts index 65bf9dc4d..4ce475abc 100644 --- a/src/cmds/docs/index.ts +++ b/src/cmds/docs/index.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import Command, { CommandCategories } from '../../lib/baseCommand'; import createGHA from '../../lib/createGHA'; @@ -38,7 +38,7 @@ export default class DocsCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { dryRun, filePath, key, version } = opts; diff --git a/src/cmds/docs/prune.ts b/src/cmds/docs/prune.ts index a8729d7b5..41fc36024 100644 --- a/src/cmds/docs/prune.ts +++ b/src/cmds/docs/prune.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import chalk from 'chalk'; import prompts from 'prompts'; @@ -56,7 +56,7 @@ export default class DocsPruneCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { dryRun, folder, key, version } = opts; diff --git a/src/cmds/guides/index.ts b/src/cmds/guides/index.ts index 43ad6679f..562011696 100644 --- a/src/cmds/guides/index.ts +++ b/src/cmds/guides/index.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import type { Options } from '../docs'; import DocsCommand from '../docs'; @@ -12,7 +12,7 @@ export default class GuidesCommand extends DocsCommand { this.description = 'Alias for `rdme docs`.'; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { return super.run(opts); } } diff --git a/src/cmds/guides/prune.ts b/src/cmds/guides/prune.ts index b38bfc1e2..f2f0d8723 100644 --- a/src/cmds/guides/prune.ts +++ b/src/cmds/guides/prune.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import type { Options } from '../docs/prune'; import DocsPruneCommand from '../docs/prune'; @@ -12,7 +12,7 @@ export default class GuidesPruneCommand extends DocsPruneCommand { this.description = 'Alias for `rdme docs:prune`.'; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { return super.run(opts); } } diff --git a/src/cmds/login.ts b/src/cmds/login.ts index 887712ce3..4fb386248 100644 --- a/src/cmds/login.ts +++ b/src/cmds/login.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../lib/baseCommand'; import prompts from 'prompts'; @@ -45,7 +45,7 @@ export default class LoginCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: ZeroAuthCommandOptions) { await super.run(opts); prompts.override(opts); diff --git a/src/cmds/logout.ts b/src/cmds/logout.ts index 377bb79ab..00b99cc9c 100644 --- a/src/cmds/logout.ts +++ b/src/cmds/logout.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../lib/baseCommand'; import Command, { CommandCategories } from '../lib/baseCommand'; import config from '../lib/config'; @@ -16,7 +16,7 @@ export default class LogoutCommand extends Command { this.args = []; } - async run(opts: CommandOptions<{}>) { + async run(opts: ZeroAuthCommandOptions) { await super.run(opts); if (configStore.has('email') && configStore.has('project')) { diff --git a/src/cmds/oas.ts b/src/cmds/oas.ts index 1b617926c..9fdc57677 100644 --- a/src/cmds/oas.ts +++ b/src/cmds/oas.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../lib/baseCommand'; import Command, { CommandCategories } from '../lib/baseCommand'; import isHidden from '../lib/decorators/isHidden'; @@ -16,7 +16,7 @@ export default class OASCommand extends Command { this.args = []; } - async run(opts: CommandOptions<{}>) { + async run(opts: ZeroAuthCommandOptions) { await super.run(opts); const message = [ diff --git a/src/cmds/open.ts b/src/cmds/open.ts index 17ecce073..7d29c3e16 100644 --- a/src/cmds/open.ts +++ b/src/cmds/open.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../lib/baseCommand'; import chalk from 'chalk'; import open from 'open'; @@ -31,7 +31,7 @@ export default class OpenCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: ZeroAuthCommandOptions) { await super.run(opts); const { dash } = opts; diff --git a/src/cmds/openapi/convert.ts b/src/cmds/openapi/convert.ts index 78aabe96e..765bf3912 100644 --- a/src/cmds/openapi/convert.ts +++ b/src/cmds/openapi/convert.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../../lib/baseCommand'; import type { OASDocument } from 'oas/dist/rmoas.types'; import fs from 'fs'; @@ -43,7 +43,7 @@ export default class OpenAPIConvertCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: ZeroAuthCommandOptions) { await super.run(opts); const { spec, workingDirectory } = opts; diff --git a/src/cmds/openapi/index.ts b/src/cmds/openapi/index.ts index bcb72d278..f4a6ba6dd 100644 --- a/src/cmds/openapi/index.ts +++ b/src/cmds/openapi/index.ts @@ -1,4 +1,5 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; +import type { OpenAPIPromptOptions } from '../../lib/prompts'; import type { RequestInit, Response } from 'node-fetch'; import chalk from 'chalk'; @@ -86,7 +87,7 @@ export default class OpenAPICommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { dryRun, key, id, spec, create, raw, title, useSpecVersion, version, workingDirectory, update } = opts; @@ -222,11 +223,8 @@ export default class OpenAPICommand extends Command { const options: RequestInit = { headers: cleanHeaders( key, - new Headers({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'x-readme-version': selectedVersion, - }), + selectedVersion, + new Headers({ Accept: 'application/json', 'Content-Type': 'application/json' }), ), body: JSON.stringify({ registryUUID }), }; @@ -281,15 +279,16 @@ export default class OpenAPICommand extends Command { */ function getSpecs(url: string) { - return readmeAPIFetch(url, { - method: 'get', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - }), - ), - }); + if (url) { + return readmeAPIFetch(url, { + method: 'get', + headers: cleanHeaders(key, selectedVersion), + }); + } + + throw new Error( + 'There was an error retrieving your list of API definitions. Please get in touch with us at support@readme.io', + ); } if (create) { @@ -302,7 +301,7 @@ export default class OpenAPICommand extends Command { Command.debug('no id parameter, retrieving list of API specs'); const apiSettings = await getSpecs('/api/v1/api-specification'); - const totalPages = Math.ceil(parseInt(apiSettings.headers.get('x-total-count'), 10) / 10); + const totalPages = Math.ceil(parseInt(apiSettings.headers.get('x-total-count') || '0', 10) / 10); const parsedDocs = parse(apiSettings.headers.get('link')); Command.debug(`total pages: ${totalPages}`); Command.debug(`pagination result: ${JSON.stringify(parsedDocs)}`); @@ -320,9 +319,7 @@ export default class OpenAPICommand extends Command { return updateSpec(specId); } - // @todo: figure out how to add a stricter type here, see: - // https://github.com/readmeio/rdme/pull/570#discussion_r949715913 - const { option } = await promptTerminal( + const { option }: { option: OpenAPIPromptOptions } = await promptTerminal( promptHandler.createOasPrompt(apiSettingsBody, parsedDocs, totalPages, getSpecs), ); Command.debug(`selection result: ${option}`); diff --git a/src/cmds/openapi/inspect.ts b/src/cmds/openapi/inspect.ts index 49265f1ae..388fca366 100644 --- a/src/cmds/openapi/inspect.ts +++ b/src/cmds/openapi/inspect.ts @@ -1,5 +1,5 @@ import type { Analysis, AnalyzedFeature } from '../../lib/analyzeOas'; -import type { CommandOptions } from '../../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../../lib/baseCommand'; import type { OASDocument } from 'oas/dist/rmoas.types'; import chalk from 'chalk'; @@ -21,7 +21,7 @@ interface Options { } export default class OpenAPIInspectCommand extends Command { - definitionVersion: string; + definitionVersion!: string; tableBorder: Record; @@ -57,7 +57,7 @@ export default class OpenAPIInspectCommand extends Command { .reduce((prev, next) => Object.assign(prev, next)); } - getFeatureDocsURL(feature: AnalyzedFeature): string { + getFeatureDocsURL(feature: AnalyzedFeature): string | undefined { if (!feature.url) { return undefined; } @@ -178,12 +178,12 @@ export default class OpenAPIInspectCommand extends Command { [ { component: 'openapi', header: 'OpenAPI Features' }, { component: 'readme', header: 'ReadMe-Specific Features and Extensions' }, - ].forEach(({ component, header }: { component: 'openapi' | 'readme'; header: string }) => { + ].forEach(({ component, header }: { component: string; header: string }) => { const tableData: string[][] = [ [chalk.bold.green('Feature'), chalk.bold.green('Used?'), chalk.bold.green('Description')], ]; - Object.entries(analysis[component]).forEach(([feature, info]) => { + Object.entries(analysis[component as 'openapi' | 'readme']).forEach(([feature, info]) => { const descriptions: string[] = []; if (info.description) { descriptions.push(info.description); @@ -215,7 +215,7 @@ export default class OpenAPIInspectCommand extends Command { return report.join('\n'); } - async run(opts: CommandOptions) { + async run(opts: ZeroAuthCommandOptions) { await super.run(opts); const { spec, workingDirectory, feature: features } = opts; diff --git a/src/cmds/openapi/reduce.ts b/src/cmds/openapi/reduce.ts index 7aacb8eb9..46cd4d4a9 100644 --- a/src/cmds/openapi/reduce.ts +++ b/src/cmds/openapi/reduce.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../../lib/baseCommand'; import type { OASDocument } from 'oas/dist/rmoas.types'; import fs from 'fs'; @@ -70,7 +70,7 @@ export default class OpenAPIReduceCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: ZeroAuthCommandOptions) { await super.run(opts); const { spec, title, workingDirectory } = opts; @@ -130,7 +130,7 @@ export default class OpenAPIReduceCommand extends Command { message: 'Choose which paths to reduce by:', min: 1, choices: () => { - return Object.keys(parsedPreparedSpec.paths).map(p => ({ + return Object.keys(parsedPreparedSpec.paths || []).map(p => ({ title: p, value: p, })); @@ -144,7 +144,7 @@ export default class OpenAPIReduceCommand extends Command { choices: (prev, values) => { const paths: string[] = values.paths; let methods = paths - .map((p: string) => Object.keys(parsedPreparedSpec.paths[p] || {})) + .map((p: string) => Object.keys(parsedPreparedSpec.paths?.[p] || {})) .flat() .filter((method: string) => method.toLowerCase() !== 'parameters'); diff --git a/src/cmds/openapi/validate.ts b/src/cmds/openapi/validate.ts index dcbecf22a..cb92be1f1 100644 --- a/src/cmds/openapi/validate.ts +++ b/src/cmds/openapi/validate.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../../lib/baseCommand'; import chalk from 'chalk'; @@ -32,7 +32,7 @@ export default class OpenAPIValidateCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: ZeroAuthCommandOptions) { await super.run(opts); const { spec, workingDirectory } = opts; @@ -45,7 +45,7 @@ export default class OpenAPIValidateCommand extends Command { const { specPath, specType } = await prepareOas(spec, 'openapi:validate'); return Promise.resolve(chalk.green(`${specPath} is a valid ${specType} API definition!`)).then(msg => - createGHA(msg, this.command, this.args, { ...opts, spec: specPath } as CommandOptions), + createGHA(msg, this.command, this.args, { ...opts, spec: specPath } as ZeroAuthCommandOptions), ); } } diff --git a/src/cmds/swagger.ts b/src/cmds/swagger.ts index 891c9c79a..63e5a354c 100644 --- a/src/cmds/swagger.ts +++ b/src/cmds/swagger.ts @@ -1,5 +1,5 @@ import type { Options } from './openapi'; -import type { CommandOptions } from '../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../lib/baseCommand'; import Command from '../lib/baseCommand'; import isHidden from '../lib/decorators/isHidden'; @@ -16,7 +16,7 @@ export default class SwaggerCommand extends OpenAPICommand { this.description = 'Alias for `rdme openapi`. [deprecated]'; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { Command.warn('`rdme swagger` has been deprecated. Please use `rdme openapi` instead.'); return super.run(opts); } diff --git a/src/cmds/validate.ts b/src/cmds/validate.ts index fb4839a52..3c8fb1f70 100644 --- a/src/cmds/validate.ts +++ b/src/cmds/validate.ts @@ -1,5 +1,5 @@ import type { Options } from './openapi/validate'; -import type { CommandOptions } from '../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../lib/baseCommand'; import Command from '../lib/baseCommand'; import isHidden from '../lib/decorators/isHidden'; @@ -16,7 +16,7 @@ export default class ValidateAliasCommand extends OpenAPIValidateCommand { this.description = 'Alias for `rdme openapi:validate` [deprecated].'; } - async run(opts: CommandOptions) { + async run(opts: ZeroAuthCommandOptions) { Command.warn('`rdme validate` has been deprecated. Please use `rdme openapi:validate` instead.'); return super.run(opts); } diff --git a/src/cmds/versions/create.ts b/src/cmds/versions/create.ts index a16a67e07..60d698adb 100644 --- a/src/cmds/versions/create.ts +++ b/src/cmds/versions/create.ts @@ -1,5 +1,5 @@ import type { Version } from '.'; -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import { Headers } from 'node-fetch'; import prompts from 'prompts'; @@ -45,7 +45,7 @@ export default class CreateVersionCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); let versionList; @@ -91,10 +91,8 @@ export default class CreateVersionCommand extends Command { method: 'post', headers: cleanHeaders( key, - new Headers({ - Accept: 'application/json', - 'Content-Type': 'application/json', - }), + undefined, + new Headers({ Accept: 'application/json', 'Content-Type': 'application/json' }), ), body: JSON.stringify(body), }) diff --git a/src/cmds/versions/delete.ts b/src/cmds/versions/delete.ts index ff0c21d3d..9c7dfe360 100644 --- a/src/cmds/versions/delete.ts +++ b/src/cmds/versions/delete.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import Command, { CommandCategories } from '../../lib/baseCommand'; import readmeAPIFetch, { cleanHeaders, handleRes } from '../../lib/readmeAPIFetch'; @@ -24,7 +24,7 @@ export default class DeleteVersionCommand extends Command { ]; } - async run(opts: CommandOptions<{}>) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { key, version } = opts; diff --git a/src/cmds/versions/index.ts b/src/cmds/versions/index.ts index a4b6d6751..2b4eb2cad 100644 --- a/src/cmds/versions/index.ts +++ b/src/cmds/versions/index.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import Command, { CommandCategories } from '../../lib/baseCommand'; import readmeAPIFetch, { cleanHeaders, handleRes } from '../../lib/readmeAPIFetch'; @@ -33,7 +33,7 @@ export default class VersionsCommand extends Command { ]; } - async run(opts: CommandOptions<{}>) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { key, version } = opts; diff --git a/src/cmds/versions/update.ts b/src/cmds/versions/update.ts index fd587985b..bfa06e057 100644 --- a/src/cmds/versions/update.ts +++ b/src/cmds/versions/update.ts @@ -1,6 +1,6 @@ import type { Version } from '.'; import type { CommonOptions } from './create'; -import type { CommandOptions } from '../../lib/baseCommand'; +import type { AuthenticatedCommandOptions } from '../../lib/baseCommand'; import { Headers } from 'node-fetch'; import prompts from 'prompts'; @@ -37,7 +37,7 @@ export default class UpdateVersionCommand extends Command { ]; } - async run(opts: CommandOptions) { + async run(opts: AuthenticatedCommandOptions) { await super.run(opts); const { key, version, newVersion, codename, main, beta, isPublic, deprecated } = opts; @@ -78,10 +78,8 @@ export default class UpdateVersionCommand extends Command { method: 'put', headers: cleanHeaders( key, - new Headers({ - Accept: 'application/json', - 'Content-Type': 'application/json', - }), + undefined, + new Headers({ Accept: 'application/json', 'Content-Type': 'application/json' }), ), body: JSON.stringify(body), }) diff --git a/src/cmds/whoami.ts b/src/cmds/whoami.ts index d43df443b..c1191af4f 100644 --- a/src/cmds/whoami.ts +++ b/src/cmds/whoami.ts @@ -1,4 +1,4 @@ -import type { CommandOptions } from '../lib/baseCommand'; +import type { ZeroAuthCommandOptions } from '../lib/baseCommand'; import chalk from 'chalk'; @@ -18,7 +18,7 @@ export default class WhoAmICommand extends Command { this.args = []; } - async run(opts: CommandOptions<{}>) { + async run(opts: ZeroAuthCommandOptions) { await super.run(opts); const { email, project } = getCurrentConfig(); diff --git a/src/index.ts b/src/index.ts index 0ec4751de..200c4926f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ /* eslint-disable no-underscore-dangle */ import type Command from './lib/baseCommand'; import type { CommandOptions } from './lib/baseCommand'; +import type { CommandLineOptions } from 'command-line-args'; import chalk from 'chalk'; import cliArgs from 'command-line-args'; @@ -82,7 +83,7 @@ export default function rdme(rawProcessArgv: NodeJS.Process['argv']) { } try { - let cmdArgv: CommandOptions<{}>; + let cmdArgv: CommandOptions | CommandLineOptions; let bin: Command; // Handling for `rdme help` and `rdme help ` cases. @@ -91,7 +92,7 @@ export default function rdme(rawProcessArgv: NodeJS.Process['argv']) { return Promise.resolve(help.globalUsage(mainArgs)); } - if (argv._unknown.indexOf('-H') !== -1) { + if (argv._unknown?.indexOf('-H') !== -1) { return Promise.resolve(help.globalUsage(mainArgs)); } @@ -134,9 +135,9 @@ export default function rdme(rawProcessArgv: NodeJS.Process['argv']) { cmdArgv = { key, ...cmdArgv }; - return bin.run(cmdArgv).then((msg: string) => { + return bin.run(cmdArgv as CommandOptions).then((msg: string) => { if (bin.supportsGHA) { - return createGHA(msg, bin.command, bin.args, cmdArgv); + return createGHA(msg, bin.command, bin.args, cmdArgv as CommandOptions); } return msg; }); diff --git a/src/lib/analyzeOas.ts b/src/lib/analyzeOas.ts index ecd9e5942..6b3d939f8 100644 --- a/src/lib/analyzeOas.ts +++ b/src/lib/analyzeOas.ts @@ -169,19 +169,19 @@ const README_FEATURE_DOCS: Record { + return analyzer(definition).then(analysisResult => { + const analysis = analysisResult as Analysis; if (analysis.openapi) { - Object.entries(OPENAPI_FEATURE_DOCS).forEach(([feature, docs]: [keyof Analysis['openapi'], AnalyzedFeature]) => { - // eslint-disable-next-line no-param-reassign - analysis.openapi[feature] = { - ...analysis.openapi[feature], + Object.entries(OPENAPI_FEATURE_DOCS).forEach(([feature, docs]) => { + analysis.openapi[feature as keyof Analysis['openapi']] = { + ...analysis.openapi[feature as keyof Analysis['openapi']], ...docs, }; }); } if (analysis.readme) { - Object.entries(README_FEATURE_DOCS).forEach(([feature, docs]: [keyof Analysis['readme'], AnalyzedFeature]) => { + Object.entries(README_FEATURE_DOCS).forEach(([feature, docs]) => { // If this ReadMe feature isn't in our resulted analysis result then it's a deprecated // feature that this API definition doesn't contain so we don't need to inform the user of // something they neither use, can't use anyways, nor should know about. @@ -189,11 +189,10 @@ async function analyzeOas(definition: OASDocument) { return; } - // eslint-disable-next-line no-param-reassign - analysis.readme[feature] = { - ...analysis.readme[feature], + analysis.readme[feature as keyof Analysis['readme']] = { + ...analysis.readme[feature as keyof Analysis['readme']], ...docs, - }; + } as AnalyzedFeature; }); } diff --git a/src/lib/apiError.ts b/src/lib/apiError.ts index dee31cfc9..f8d71c36e 100644 --- a/src/lib/apiError.ts +++ b/src/lib/apiError.ts @@ -1,4 +1,4 @@ -interface APIErrorResponse { +export interface APIErrorResponse { docs?: string; error: string; help?: string; diff --git a/src/lib/baseCommand.ts b/src/lib/baseCommand.ts index c55bac59a..bc00dc829 100644 --- a/src/lib/baseCommand.ts +++ b/src/lib/baseCommand.ts @@ -1,6 +1,5 @@ /* eslint-disable class-methods-use-this */ import type commands from '../cmds'; -import type { CommandLineOptions } from 'command-line-args'; import type { OptionDefinition } from 'command-line-usage'; import chalk from 'chalk'; @@ -11,11 +10,16 @@ import isCI from './isCI'; import { debug, info, warn } from './logger'; import loginFlow from './loginFlow'; -export type CommandOptions = T & { - github?: boolean; - key?: string; +export type CommandOptions = ZeroAuthCommandOptions | AuthenticatedCommandOptions; + +export type AuthenticatedCommandOptions = Omit, 'key'> & { + key: string; version?: string; -} & CommandLineOptions; +}; + +export type ZeroAuthCommandOptions = T & { + github?: boolean; +} & { key?: never }; export enum CommandCategories { ADMIN = 'admin', @@ -34,21 +38,21 @@ export default class Command { * * @example openapi */ - command: keyof typeof commands; + command!: keyof typeof commands; /** * Example command usage, used on invidivual command help screens * * @example openapi [file] [options] */ - usage: string; + usage!: string; /** * The command description, used on help screens * * @example Upload, or resync, your OpenAPI/Swagger definition to ReadMe. */ - description: string; + description!: string; /** * The category that the command belongs to, used on @@ -58,7 +62,7 @@ export default class Command { * * @example CommandCategories.APIS */ - cmdCategory: CommandCategories; + cmdCategory!: CommandCategories; /** * Should the command be hidden from our `--help` screens? @@ -82,9 +86,9 @@ export default class Command { /** * All documented arguments for the command */ - args: OptionDefinition[]; + args!: OptionDefinition[]; - async run(opts: CommandOptions<{}>): Promise { + async run(opts: CommandOptions): Promise { Command.debug(`command: ${this.command}`); Command.debug(`opts: ${JSON.stringify(opts)}`); diff --git a/src/lib/castStringOptToBool.ts b/src/lib/castStringOptToBool.ts index 591f3ac65..7cd7394f1 100644 --- a/src/lib/castStringOptToBool.ts +++ b/src/lib/castStringOptToBool.ts @@ -5,7 +5,10 @@ import type { Options as UpdateOptions } from '../cmds/versions/update'; * Takes a CLI flag that is expected to be a 'true' or 'false' string * and casts it to a boolean. */ -export default function castStringOptToBool(opt: 'true' | 'false', optName: keyof CreateOptions | keyof UpdateOptions) { +export default function castStringOptToBool( + opt: 'true' | 'false' | undefined, + optName: keyof CreateOptions | keyof UpdateOptions, +) { if (!opt) { return undefined; } diff --git a/src/lib/createGHA/index.ts b/src/lib/createGHA/index.ts index 6be41db87..76e7465f3 100644 --- a/src/lib/createGHA/index.ts +++ b/src/lib/createGHA/index.ts @@ -48,9 +48,9 @@ export const getGHAFileName = (fileName: string) => { * Returns a redacted `key` if the current command uses authentication. * Otherwise, returns `false`. */ -function getKey(args: OptionDefinition[], opts: CommandOptions<{}>): string | false { +function getKey(args: OptionDefinition[], opts: CommandOptions): string | false { if (args.some(arg => arg.name === 'key')) { - return `••••••••••••${opts.key.slice(-5)}`; + return `••••••••••••${opts.key?.slice(-5) || ''}`; } return false; } @@ -58,14 +58,12 @@ function getKey(args: OptionDefinition[], opts: CommandOptions<{}>): string | fa /** * Constructs the command string that we pass into the workflow file. */ -function constructCmdString( - command: keyof typeof commands, - args: OptionDefinition[], - opts: CommandOptions>, -): string { +function constructCmdString(command: keyof typeof commands, args: OptionDefinition[], opts: CommandOptions): string { const optsString = args .sort(arg => (arg.defaultOption ? -1 : 0)) .map(arg => { + // @ts-expect-error by this point it's safe to assume that + // the argument names match the opts object. const val = opts[arg.name]; // if default option, return the value if (arg.defaultOption) return val; @@ -151,7 +149,7 @@ export default async function createGHA( msg: string, command: keyof typeof commands, args: OptionDefinition[], - opts: CommandOptions<{}>, + opts: CommandOptions, ) { debug(`running GHA onboarding for ${command} command`); debug(`opts used in createGHA: ${JSON.stringify(opts)}`); @@ -271,8 +269,8 @@ export default async function createGHA( let output = yamlBase; - Object.keys(data).forEach((key: keyof typeof data) => { - output = output.replace(new RegExp(`{{${key}}}`, 'g'), data[key]); + Object.keys(data).forEach(key => { + output = output.replace(new RegExp(`{{${key}}}`, 'g'), data[key as keyof typeof data]); }); if (!fs.existsSync(GITHUB_WORKFLOW_DIR)) { diff --git a/src/lib/deleteDoc.ts b/src/lib/deleteDoc.ts index b0be7cef6..1965e88cc 100644 --- a/src/lib/deleteDoc.ts +++ b/src/lib/deleteDoc.ts @@ -16,8 +16,8 @@ import readmeAPIFetch, { cleanHeaders, handleRes } from './readmeAPIFetch'; */ export default async function deleteDoc( key: string, - selectedVersion: string, - dryRun: boolean, + selectedVersion: string | undefined, + dryRun: boolean | undefined, slug: string, type: CommandCategories, ): Promise { @@ -26,13 +26,7 @@ export default async function deleteDoc( } return readmeAPIFetch(`/api/v1/${type}/${slug}`, { method: 'delete', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - ), + headers: cleanHeaders(key, selectedVersion, new Headers({ 'Content-Type': 'application/json' })), }) .then(handleRes) .then(() => `🗑️ successfully deleted \`${slug}\`.`); diff --git a/src/lib/getCategories.ts b/src/lib/getCategories.ts index b0886db85..257ee136d 100644 --- a/src/lib/getCategories.ts +++ b/src/lib/getCategories.ts @@ -9,21 +9,15 @@ import readmeAPIFetch, { cleanHeaders, handleRes } from './readmeAPIFetch'; * @param {String} selectedVersion project version * @returns An array of category objects */ -export default async function getCategories(key: string, selectedVersion: string) { +export default async function getCategories(key: string, selectedVersion: string | undefined) { function getNumberOfPages() { let totalCount = 0; return readmeAPIFetch('/api/v1/categories?perPage=20&page=1', { method: 'get', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - Accept: 'application/json', - }), - ), + headers: cleanHeaders(key, selectedVersion, new Headers({ Accept: 'application/json' })), }) .then(res => { - totalCount = Math.ceil(parseInt(res.headers.get('x-total-count'), 10) / 20); + totalCount = Math.ceil(parseInt(res.headers.get('x-total-count') || '0', 10) / 20); return handleRes(res); }) .then(res => { @@ -39,13 +33,7 @@ export default async function getCategories(key: string, selectedVersion: string [...new Array(totalCount + 1).keys()].slice(2).map(async page => { return readmeAPIFetch(`/api/v1/categories?perPage=20&page=${page}`, { method: 'get', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - Accept: 'application/json', - }), - ), + headers: cleanHeaders(key, selectedVersion, new Headers({ Accept: 'application/json' })), }).then(handleRes); }), )), diff --git a/src/lib/getDocs.ts b/src/lib/getDocs.ts index 1ad9fe947..35ba018fe 100644 --- a/src/lib/getDocs.ts +++ b/src/lib/getDocs.ts @@ -14,7 +14,7 @@ interface Document { function flatten(data: Document[][]): Document[] { const allDocs: Document[] = []; - const docs: Document[] = [].concat(...data); + const docs: Document[] = ([]).concat(...data); docs.forEach(doc => { allDocs.push(doc); if (doc.children) { @@ -31,16 +31,14 @@ function flatten(data: Document[][]): Document[] { return allDocs; } -async function getCategoryDocs(key: string, selectedVersion: string, category: string): Promise { +async function getCategoryDocs( + key: string, + selectedVersion: string | undefined, + category: string, +): Promise { return readmeAPIFetch(`/api/v1/categories/${category}/docs`, { method: 'get', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - ), + headers: cleanHeaders(key, selectedVersion, new Headers({ 'Content-Type': 'application/json' })), }).then(handleRes); } @@ -51,7 +49,7 @@ async function getCategoryDocs(key: string, selectedVersion: string, category: s * @param {String} selectedVersion the project version * @returns {Promise>} an array containing the docs */ -export default async function getDocs(key: string, selectedVersion: string): Promise { +export default async function getDocs(key: string, selectedVersion: string | undefined): Promise { return getCategories(key, selectedVersion) .then(categories => categories.filter(({ type }: { type: string }) => type === 'guide')) .then(categories => categories.map(({ slug }: { slug: string }) => getCategoryDocs(key, selectedVersion, slug))) diff --git a/src/lib/getPkgVersion.ts b/src/lib/getPkgVersion.ts index b043d1f49..ebcde02f2 100644 --- a/src/lib/getPkgVersion.ts +++ b/src/lib/getPkgVersion.ts @@ -20,7 +20,11 @@ type npmDistTag = 'latest'; */ export function getNodeVersion() { const { node } = pkg.engines; - return semver.minVersion(node).major; + const parsedVersion = semver.minVersion(node); + if (!parsedVersion) { + throw new Error('`version` value in package.json is invalid'); + } + return parsedVersion.major; } /** diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 648700c88..1368d31c2 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -13,8 +13,7 @@ const debugPackage = debugModule(config.cli); /** * Wrapper for debug statements. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function debug(input: any) { +function debug(input: unknown) { /* istanbul ignore next */ if (isGHA() && !isTest()) { if (typeof input === 'object') { diff --git a/src/lib/prepareOas.ts b/src/lib/prepareOas.ts index e4d944204..9149192be 100644 --- a/src/lib/prepareOas.ts +++ b/src/lib/prepareOas.ts @@ -11,10 +11,12 @@ import readdirRecursive from './readdirRecursive'; export type SpecFileType = OASNormalize['type']; +type SpecType = 'OpenAPI' | 'Swagger' | 'Postman'; + interface FoundSpecFile { /** path to the spec file */ filePath: string; - specType: 'OpenAPI' | 'Swagger' | 'Postman'; + specType: SpecType; /** * OpenAPI or Postman specification version * @example '3.1' @@ -26,6 +28,13 @@ interface FileSelection { file: string; } +// source: https://stackoverflow.com/a/58110124 +type Truthy = T extends false | '' | 0 | null | undefined ? never : T; + +function truthy(value: T): value is Truthy { + return !!value; +} + const capitalizeSpecType = (type: string) => type === 'openapi' ? 'OpenAPI' : type.charAt(0).toUpperCase() + type.slice(1); @@ -38,7 +47,7 @@ const capitalizeSpecType = (type: string) => * validation, or reducing one). */ export default async function prepareOas( - path: string, + path: string | undefined, command: 'openapi' | 'openapi:convert' | 'openapi:inspect' | 'openapi:reduce' | 'openapi:validate', opts: { /** @@ -103,7 +112,7 @@ export default async function prepareOas( debug(`specification type for ${file}: ${specification}`); debug(`version for ${file}: ${version}`); return ['openapi', 'swagger', 'postman'].includes(specification) - ? { filePath: file, specType: capitalizeSpecType(specification), version } + ? { filePath: file, specType: capitalizeSpecType(specification) as SpecType, version } : null; }) .catch(e => { @@ -112,7 +121,7 @@ export default async function prepareOas( }); }), ) - ).filter(Boolean); + ).filter(truthy); debug(`number of possible OpenAPI/Swagger files found: ${possibleSpecFiles.length}`); diff --git a/src/lib/prompts.ts b/src/lib/prompts.ts index 331b41960..5f3ea676d 100644 --- a/src/lib/prompts.ts +++ b/src/lib/prompts.ts @@ -13,6 +13,8 @@ interface Spec { title: string; } +export type OpenAPIPromptOptions = 'create' | 'update'; + type SpecList = Spec[]; interface ParsedDocs { @@ -26,7 +28,12 @@ interface ParsedDocs { }; } -function specOptions(specList: SpecList, parsedDocs: ParsedDocs, currPage: number, totalPages: number): Choice[] { +function specOptions( + specList: SpecList, + parsedDocs: ParsedDocs | null, + currPage: number, + totalPages: number, +): Choice[] { const specs = specList.map(s => { return { description: `API Definition ID: ${s._id}`, // eslint-disable-line no-underscore-dangle @@ -53,11 +60,11 @@ function specOptions(specList: SpecList, parsedDocs: ParsedDocs, currPage: numbe const updateOasPrompt = ( specList: SpecList, - parsedDocs: ParsedDocs, + parsedDocs: ParsedDocs | null, currPage: number, totalPages: number, getSpecs: (url: string) => Promise, -): PromptObject[] => [ +): PromptObject<'specId'>[] => [ { type: 'select', name: 'specId', @@ -66,12 +73,10 @@ const updateOasPrompt = ( async format(spec: string) { if (spec === 'prev') { try { - const newSpecs = await getSpecs(`${parsedDocs.prev.url}`); + const newSpecs = await getSpecs(`${parsedDocs?.prev?.url || ''}`); const newParsedDocs = parse(newSpecs.headers.get('link')); const newSpecList = await handleRes(newSpecs); - // @todo: figure out how to add a stricter type here, see: - // https://github.com/readmeio/rdme/pull/570#discussion_r949715913 - const { specId } = await promptTerminal( + const { specId }: { specId: string } = await promptTerminal( updateOasPrompt(newSpecList, newParsedDocs, currPage - 1, totalPages, getSpecs), ); return specId; @@ -80,12 +85,10 @@ const updateOasPrompt = ( } } else if (spec === 'next') { try { - const newSpecs = await getSpecs(`${parsedDocs.next.url}`); + const newSpecs = await getSpecs(`${parsedDocs?.next?.url || ''}`); const newParsedDocs = parse(newSpecs.headers.get('link')); const newSpecList = await handleRes(newSpecs); - // @todo: figure out how to add a stricter type here, see: - // https://github.com/readmeio/rdme/pull/570#discussion_r949715913 - const { specId } = await promptTerminal( + const { specId }: { specId: string } = await promptTerminal( updateOasPrompt(newSpecList, newParsedDocs, currPage + 1, totalPages, getSpecs), ); return specId; @@ -101,10 +104,10 @@ const updateOasPrompt = ( export function createOasPrompt( specList: SpecList, - parsedDocs: ParsedDocs, + parsedDocs: ParsedDocs | null, totalPages: number, - getSpecs: ((url: string) => Promise) | null, -): PromptObject[] { + getSpecs: (url: string) => Promise, +): PromptObject<'option'>[] { return [ { type: 'select', @@ -114,11 +117,11 @@ export function createOasPrompt( { title: 'Update existing', value: 'update' }, { title: 'Create a new spec', value: 'create' }, ], - async format(picked: 'update' | 'create') { + async format(picked: OpenAPIPromptOptions) { if (picked === 'update') { - // @todo: figure out how to add a stricter type here, see: - // https://github.com/readmeio/rdme/pull/570#discussion_r949715913 - const { specId } = await promptTerminal(updateOasPrompt(specList, parsedDocs, 1, totalPages, getSpecs)); + const { specId }: { specId: string } = await promptTerminal( + updateOasPrompt(specList, parsedDocs, 1, totalPages, getSpecs), + ); return specId; } diff --git a/src/lib/readDoc.ts b/src/lib/readDoc.ts index 8d4f4d80e..6561ac816 100644 --- a/src/lib/readDoc.ts +++ b/src/lib/readDoc.ts @@ -10,8 +10,7 @@ interface ReadDocMetadata { /** The contents of the file below the YAML front matter */ content: string; /** A JSON object with the YAML front matter */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: Record; + data: Record; /** * A hash of the file contents (including the front matter) */ diff --git a/src/lib/readdirRecursive.ts b/src/lib/readdirRecursive.ts index 47ec98017..98f13143a 100644 --- a/src/lib/readdirRecursive.ts +++ b/src/lib/readdirRecursive.ts @@ -45,7 +45,7 @@ export default function readdirRecursive(folderToSearch: string, ignoreGit = fal const folders = filesInFolder.filter(fileHandle => fileHandle.isDirectory()); - const subFiles = [].concat( + const subFiles = ([]).concat( ...folders.map(fileHandle => readdirRecursive(path.join(folderToSearch, fileHandle.name), ignoreGit)), ); diff --git a/src/lib/readmeAPIFetch.ts b/src/lib/readmeAPIFetch.ts index 21833c2ed..48d94732b 100644 --- a/src/lib/readmeAPIFetch.ts +++ b/src/lib/readmeAPIFetch.ts @@ -67,7 +67,7 @@ function parseWarningHeader(header: string): WarningHeader[] { let previous: WarningHeader; - return warnings.reduce((all, w) => { + return warnings.reduce((all, w) => { // eslint-disable-next-line no-param-reassign w = w.trim(); const newError = w.match(/^([0-9]{3}) (.*)/); @@ -147,11 +147,11 @@ export default async function readmeAPIFetch( if (isGHA()) { source = 'cli-gh'; - headers.set('x-github-repository', process.env.GITHUB_REPOSITORY); - headers.set('x-github-run-attempt', process.env.GITHUB_RUN_ATTEMPT); - headers.set('x-github-run-id', process.env.GITHUB_RUN_ID); - headers.set('x-github-run-number', process.env.GITHUB_RUN_NUMBER); - headers.set('x-github-sha', process.env.GITHUB_SHA); + if (process.env.GITHUB_REPOSITORY) headers.set('x-github-repository', process.env.GITHUB_REPOSITORY); + if (process.env.GITHUB_RUN_ATTEMPT) headers.set('x-github-run-attempt', process.env.GITHUB_RUN_ATTEMPT); + if (process.env.GITHUB_RUN_ID) headers.set('x-github-run-id', process.env.GITHUB_RUN_ID); + if (process.env.GITHUB_RUN_NUMBER) headers.set('x-github-run-number', process.env.GITHUB_RUN_NUMBER); + if (process.env.GITHUB_SHA) headers.set('x-github-sha', process.env.GITHUB_SHA); const filePath = await normalizeFilePath(fileOpts); @@ -217,7 +217,7 @@ export default async function readmeAPIFetch( * */ async function handleRes(res: Response, rejectOnJsonError = true) { - const contentType = res.headers.get('content-type'); + const contentType = res.headers.get('content-type') || ''; const extension = mime.extension(contentType); if (extension === 'json') { const body = await res.json(); @@ -242,12 +242,21 @@ async function handleRes(res: Response, rejectOnJsonError = true) { * Returns the basic auth header and any other defined headers for use in `node-fetch` API calls. * */ -function cleanHeaders(key: string, inputHeaders: Headers = new Headers()) { +function cleanHeaders( + key: string, + /** used for `x-readme-header` */ + version?: string, + inputHeaders: Headers = new Headers(), +) { const encodedKey = Buffer.from(`${key}:`).toString('base64'); const headers = new Headers({ Authorization: `Basic ${encodedKey}`, }); + if (version) { + headers.set('x-readme-version', version); + } + for (const header of inputHeaders.entries()) { // If you supply `undefined` or `null` to the `Headers` API it'll convert that those to a // string. diff --git a/src/lib/syncDocsPath.ts b/src/lib/syncDocsPath.ts index 3f0652645..de0ad10de 100644 --- a/src/lib/syncDocsPath.ts +++ b/src/lib/syncDocsPath.ts @@ -25,7 +25,7 @@ import readmeAPIFetch, { cleanHeaders, handleRes } from './readmeAPIFetch'; */ async function pushDoc( key: string, - selectedVersion: string, + selectedVersion: string | undefined, dryRun: boolean, filePath: string, type: CommandCategories, @@ -66,13 +66,7 @@ async function pushDoc( `/api/v1/${type}`, { method: 'post', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - ), + headers: cleanHeaders(key, selectedVersion, new Headers({ 'Content-Type': 'application/json' })), body: JSON.stringify({ slug, ...payload, @@ -103,13 +97,7 @@ async function pushDoc( `/api/v1/${type}/${slug}`, { method: 'put', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - ), + headers: cleanHeaders(key, selectedVersion, new Headers({ 'Content-Type': 'application/json' })), body: JSON.stringify(payload), }, { filePath, fileType: 'path' }, @@ -120,13 +108,7 @@ async function pushDoc( return readmeAPIFetch(`/api/v1/${type}/${slug}`, { method: 'get', - headers: cleanHeaders( - key, - new Headers({ - 'x-readme-version': selectedVersion, - Accept: 'application/json', - }), - ), + headers: cleanHeaders(key, selectedVersion, new Headers({ Accept: 'application/json' })), }) .then(async res => { const body = await handleRes(res, false); @@ -154,15 +136,15 @@ export default async function syncDocsPath( /** Project API key */ key: string, /** ReadMe project version */ - selectedVersion: string, + selectedVersion: string | undefined, /** module within ReadMe to update (e.g. docs, changelogs, etc.) */ cmdType: CommandCategories, /** Example command usage, used in error message */ usage: string, /** Path input, can either be a directory or a single file */ - pathInput: string, + pathInput: string | undefined, /** boolean indicating dry run mode */ - dryRun: boolean, + dryRun: boolean = false, /** array of allowed file extensions */ allowedFileExtensions = ['.markdown', '.md'], ) { diff --git a/src/lib/versionSelect.ts b/src/lib/versionSelect.ts index f5be750c5..c59a908f1 100644 --- a/src/lib/versionSelect.ts +++ b/src/lib/versionSelect.ts @@ -1,3 +1,4 @@ +import type { APIErrorResponse } from './apiError'; import type { Version } from '../cmds/versions'; import APIError from './apiError'; @@ -13,7 +14,11 @@ import readmeAPIFetch, { cleanHeaders, handleRes } from './readmeAPIFetch'; * @param key project API key * @returns a cleaned up project version */ -export async function getProjectVersion(versionFlag: string, key: string, returnStable = false): Promise { +export async function getProjectVersion( + versionFlag: string | undefined, + key: string, + returnStable = false, +): Promise { try { if (versionFlag) { return await readmeAPIFetch(`/api/v1/version/${versionFlag}`, { @@ -40,6 +45,9 @@ export async function getProjectVersion(versionFlag: string, key: string, return if (returnStable) { const stableVersion = versionList.find(v => v.is_stable === true); + if (!stableVersion) { + throw new Error('Unexpected version response from the ReadMe API. Get in touch with us at support@readme.io!'); + } return stableVersion.version; } @@ -57,6 +65,6 @@ export async function getProjectVersion(versionFlag: string, key: string, return return versionSelection; } catch (err) { - return Promise.reject(new APIError(err)); + return Promise.reject(new APIError(err as APIErrorResponse)); } } diff --git a/tsconfig.json b/tsconfig.json index 57482d737..6b01edbb9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,10 @@ "editor": [".sink.d.ts"] }, "resolveJsonModule": true, - "target": "ES2020" + "skipLibCheck": true, + "strict": true, + "target": "ES2020", + "useUnknownInCatchVariables": false }, "include": ["./src/**/*"] }