From bbc3d0c06bc7124169249251daf4cb4263d960c7 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Thu, 14 Sep 2023 18:26:54 -0500 Subject: [PATCH 1/2] refactor: remove `oas`, `swagger`, `docs:edit` BREAKING CHANGE: removes several deprecated commands --- README.md | 2 +- __tests__/cmds/docs/edit.test.ts | 178 ------------------ __tests__/cmds/openapi/index.test.ts | 18 -- __tests__/index.test.ts | 8 - .../lib/__snapshots__/commands.test.ts.snap | 15 -- src/cmds/docs/edit.ts | 111 ----------- src/cmds/index.ts | 6 - src/cmds/oas.ts | 29 --- src/cmds/swagger.ts | 23 --- src/index.ts | 2 +- 10 files changed, 2 insertions(+), 390 deletions(-) delete mode 100644 __tests__/cmds/docs/edit.test.ts delete mode 100644 src/cmds/docs/edit.ts delete mode 100644 src/cmds/oas.ts delete mode 100644 src/cmds/swagger.ts diff --git a/README.md b/README.md index ec0467db4..76f2a869d 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ The following examples use JSON files, but `rdme` supports API Definitions that `rdme openapi` locates your API definition (if [you don't supply one](#omitting-the-file-path)), validates it, and then syncs it to your API reference on ReadMe. > [!NOTE] -> The `rdme openapi` command supports both OpenAPI and Swagger API definitions. The `rdme swagger` command is an alias for `rdme openapi` and is deprecated. +> The `rdme openapi` command supports both OpenAPI and Swagger API definitions. If you wish to programmatically access any of this script's results (such as the API definition ID or the link to the corresponding docs in your dashboard), supply the `--raw` flag and the command will return a JSON output. diff --git a/__tests__/cmds/docs/edit.test.ts b/__tests__/cmds/docs/edit.test.ts deleted file mode 100644 index 18655767a..000000000 --- a/__tests__/cmds/docs/edit.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import fs from 'node:fs'; - -import nock from 'nock'; -import prompts from 'prompts'; -import { describe, beforeAll, afterAll, beforeEach, afterEach, it, expect, vi } from 'vitest'; - -import DocsEditCommand from '../../../src/cmds/docs/edit.js'; -import APIError from '../../../src/lib/apiError.js'; -import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock.js'; - -const docsEdit = new DocsEditCommand(); - -const key = 'API_KEY'; -const version = '1.0.0'; -const category = 'CATEGORY_ID'; - -describe('rdme docs:edit', () => { - let consoleWarnSpy; - - function getWarningCommandOutput() { - return [consoleWarnSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); - } - - beforeAll(() => { - nock.disableNetConnect(); - }); - - beforeEach(() => { - consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleWarnSpy.mockRestore(); - }); - - afterAll(() => nock.cleanAll()); - - it('should prompt for login if no API key provided', async () => { - const consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); - prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); - await expect(docsEdit.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); - consoleInfoSpy.mockRestore(); - }); - - it('should error in CI if no API key provided', async () => { - process.env.TEST_RDME_CI = 'true'; - await expect(docsEdit.run({})).rejects.toStrictEqual(new Error('No project API key provided. Please use `--key`.')); - delete process.env.TEST_RDME_CI; - }); - - it('should log deprecation notice', async () => { - process.env.TEST_RDME_CI = 'true'; - await expect(docsEdit.run({})).rejects.toStrictEqual(new Error('No project API key provided. Please use `--key`.')); - delete process.env.TEST_RDME_CI; - expect(getWarningCommandOutput()).toMatch('is now deprecated'); - }); - - it('should error if no slug provided', () => { - return expect(docsEdit.run({ key, version: '1.0.0' })).rejects.toStrictEqual( - new Error('No slug provided. Usage `rdme docs:edit [options]`.'), - ); - }); - - it('should fetch the doc from the api', async () => { - expect.assertions(5); - const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); - const slug = 'getting-started'; - const body = 'abcdef'; - const edits = 'ghijkl'; - - const getMock = getAPIMockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(200, { category, slug, body }); - - const putMock = getAPIMockWithVersionHeader(version) - .put(`/api/v1/docs/${slug}`, { - category, - slug, - body: `${body}${edits}`, - }) - .basicAuth({ user: key }) - .reply(200, { category, slug }); - - const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); - - function mockEditor(filename, cb) { - expect(filename).toBe(`${slug}.md`); - expect(fs.existsSync(filename)).toBe(true); - fs.appendFile(filename, edits, cb.bind(null, 0)); - } - - await expect(docsEdit.run({ slug, key, version: '1.0.0', mockEditor })).resolves.toBe(''); - - getMock.done(); - putMock.done(); - versionMock.done(); - - expect(fs.existsSync(`${slug}.md`)).toBe(false); - // eslint-disable-next-line no-console - expect(console.info).toHaveBeenCalledWith('ℹ️ Doc successfully updated. Cleaning up local file.'); - consoleSpy.mockRestore(); - }); - - it('should error if remote doc does not exist', async () => { - const slug = 'no-such-doc'; - - const errorObject = { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const getMock = getAPIMock().get(`/api/v1/docs/${slug}`).reply(404, errorObject); - - const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); - - await expect(docsEdit.run({ slug, key, version: '1.0.0' })).rejects.toStrictEqual(new APIError(errorObject)); - - getMock.done(); - versionMock.done(); - }); - - it('should error if doc fails validation', async () => { - const slug = 'getting-started'; - const body = 'abcdef'; - - const errorObject = { - error: 'DOC_INVALID', - message: `We couldn't save this doc (${slug})`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const getMock = getAPIMock().get(`/api/v1/docs/${slug}`).reply(200, { body }); - const putMock = getAPIMock().put(`/api/v1/docs/${slug}`).reply(400, errorObject); - const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); - - function mockEditor(filename, cb) { - return cb(0); - } - - await expect(docsEdit.run({ slug, key, version: '1.0.0', mockEditor })).rejects.toStrictEqual( - new APIError(errorObject), - ); - - getMock.done(); - putMock.done(); - versionMock.done(); - - expect(fs.existsSync(`${slug}.md`)).toBe(true); - fs.unlinkSync(`${slug}.md`); - }); - - it('should handle error if $EDITOR fails', async () => { - const slug = 'getting-started'; - const body = 'abcdef'; - - const getMock = getAPIMock() - .get(`/api/v1/docs/${slug}`) - .reply(200, { body }) - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - function mockEditor(filename, cb) { - return cb(1); - } - - await expect(docsEdit.run({ slug, key, version: '1.0.0', mockEditor })).rejects.toStrictEqual( - new Error('Non zero exit code from $EDITOR'), - ); - - getMock.done(); - fs.unlinkSync(`${slug}.md`); - }); -}); diff --git a/__tests__/cmds/openapi/index.test.ts b/__tests__/cmds/openapi/index.test.ts index d0a9a024d..93484c2c8 100644 --- a/__tests__/cmds/openapi/index.test.ts +++ b/__tests__/cmds/openapi/index.test.ts @@ -9,7 +9,6 @@ import prompts from 'prompts'; import { describe, beforeAll, beforeEach, afterEach, it, expect, vi } from 'vitest'; import OpenAPICommand from '../../../src/cmds/openapi/index.js'; -import SwaggerCommand from '../../../src/cmds/swagger.js'; import APIError from '../../../src/lib/apiError.js'; import config from '../../../src/lib/config.js'; import petstoreWeird from '../../__fixtures__/petstore-simple-weird-version.json' assert { type: 'json' }; @@ -18,7 +17,6 @@ import { after, before } from '../../helpers/get-gha-setup.js'; import { after as afterGHAEnv, before as beforeGHAEnv } from '../../helpers/setup-gha-env.js'; const openapi = new OpenAPICommand(); -const swagger = new SwaggerCommand(); let consoleInfoSpy; let consoleWarnSpy; @@ -1395,19 +1393,3 @@ describe('rdme openapi', () => { }); }); }); - -describe('rdme swagger', () => { - beforeEach(() => { - consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleWarnSpy.mockRestore(); - }); - - it('should run `rdme openapi`', () => { - return expect(swagger.run({ spec: 'some-non-existent-path', key, id, version })).rejects.toThrow( - "ENOENT: no such file or directory, open 'some-non-existent-path'", - ); - }); -}); diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 911eec0c9..d8558d579 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -213,14 +213,6 @@ describe('cli', () => { }); }); - it('should error with `rdme oas` arguments passed in', () => { - return expect(cli(['oas', 'endpoint'])).rejects.toStrictEqual( - new Error( - "This `oas` integration is now inactive.\n\nIf you're looking to create an OpenAPI definition, we recommend https://npm.im/swagger-inline", - ), - ); - }); - describe('GHA onboarding via @supportsGHA decorator', () => { let consoleInfoSpy; const key = '123'; diff --git a/__tests__/lib/__snapshots__/commands.test.ts.snap b/__tests__/lib/__snapshots__/commands.test.ts.snap index cf58abcd7..4846239fd 100644 --- a/__tests__/lib/__snapshots__/commands.test.ts.snap +++ b/__tests__/lib/__snapshots__/commands.test.ts.snap @@ -49,11 +49,6 @@ exports[`utils > #listByCategory > should list commands by category 1`] = ` "hidden": false, "name": "openapi:validate", }, - { - "description": "Alias for \`rdme openapi\`. [deprecated]", - "hidden": true, - "name": "swagger", - }, { "description": "Alias for \`rdme openapi:validate\` [deprecated].", "hidden": true, @@ -109,11 +104,6 @@ exports[`utils > #listByCategory > should list commands by category 1`] = ` "hidden": false, "name": "docs:prune", }, - { - "description": "Edit a single file from your ReadMe project without saving locally. [deprecated]", - "hidden": true, - "name": "docs:edit", - }, { "description": "Alias for \`rdme docs\`.", "hidden": false, @@ -129,11 +119,6 @@ exports[`utils > #listByCategory > should list commands by category 1`] = ` }, "utilities": { "commands": [ - { - "description": "Helpful OpenAPI generation tooling. [inactive]", - "hidden": true, - "name": "oas", - }, { "description": "Open your current ReadMe project in the browser.", "hidden": false, diff --git a/src/cmds/docs/edit.ts b/src/cmds/docs/edit.ts deleted file mode 100644 index 1de43b9b8..000000000 --- a/src/cmds/docs/edit.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { AuthenticatedCommandOptions } from '../../lib/baseCommand.js'; - -import fs from 'node:fs'; -import { promisify } from 'node:util'; - -import { Headers } from 'node-fetch'; - -import editor from 'editor'; - -import APIError from '../../lib/apiError.js'; -import Command, { CommandCategories } from '../../lib/baseCommand.js'; -import config from '../../lib/config.js'; -import isHidden from '../../lib/decorators/isHidden.js'; -import readmeAPIFetch, { cleanHeaders, handleRes } from '../../lib/readmeAPIFetch.js'; -import { getProjectVersion } from '../../lib/versionSelect.js'; - -const writeFile = promisify(fs.writeFile); -const readFile = promisify(fs.readFile); -const unlink = promisify(fs.unlink); - -interface Options { - mockEditor?: (filename: string, cb: () => void) => void; - slug?: string; -} - -@isHidden -export default class DocsEditCommand extends Command { - constructor() { - super(); - - this.command = 'docs:edit'; - this.usage = 'docs:edit [options]'; - this.description = 'Edit a single file from your ReadMe project without saving locally. [deprecated]'; - this.cmdCategory = CommandCategories.DOCS; - - this.hiddenArgs = ['slug']; - this.args = [ - this.getKeyArg(), - this.getVersionArg(), - { - name: 'slug', - type: String, - defaultOption: true, - }, - ]; - } - - 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); - - const { slug, key, version } = opts; - - if (!slug) { - return Promise.reject(new Error(`No slug provided. Usage \`${config.cli} ${this.usage}\`.`)); - } - - const selectedVersion = await getProjectVersion(version, key); - - Command.debug(`selectedVersion: ${selectedVersion}`); - - const filename = `${slug}.md`; - - const existingDoc = await readmeAPIFetch(`/api/v1/docs/${slug}`, { - method: 'get', - headers: cleanHeaders(key, selectedVersion, new Headers({ Accept: 'application/json' })), - }).then(handleRes); - - await writeFile(filename, existingDoc.body); - - Command.debug(`wrote to local file: ${filename}, opening editor`); - - return new Promise((resolve, reject) => { - (opts.mockEditor || editor)(filename, async (code: number) => { - Command.debug(`editor closed with code ${code}`); - if (code !== 0) return reject(new Error('Non zero exit code from $EDITOR')); - const updatedDoc = await readFile(filename, 'utf8'); - - Command.debug(`read edited contents of ${filename}, sending to ReadMe`); - - return readmeAPIFetch(`/api/v1/docs/${slug}`, { - method: 'put', - headers: cleanHeaders(key, selectedVersion, new Headers({ 'Content-Type': 'application/json' })), - body: JSON.stringify( - Object.assign(existingDoc, { - body: updatedDoc, - }), - ), - }) - .then(res => handleRes(res, false)) - .then(async res => { - // We need to use the `reject` function from - // the Promise that's wrapping this function. - if (res.error) { - return reject(new APIError(res)); - } - - Command.info('Doc successfully updated. Cleaning up local file.'); - - await unlink(filename); - Command.debug('file unlinked'); - - // 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(''); - }); - }); - }); - } -} diff --git a/src/cmds/index.ts b/src/cmds/index.ts index a60ec1abc..bd6d706d8 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -2,21 +2,18 @@ import CategoriesCreateCommand from './categories/create.js'; import CategoriesCommand from './categories/index.js'; import ChangelogsCommand from './changelogs.js'; import CustomPagesCommand from './custompages.js'; -import DocsEditCommand from './docs/edit.js'; import DocsCommand from './docs/index.js'; import DocsPruneCommand from './docs/prune.js'; import GuidesCommand from './guides/index.js'; import GuidesPruneCommand from './guides/prune.js'; import LoginCommand from './login.js'; import LogoutCommand from './logout.js'; -import OASCommand from './oas.js'; import OpenCommand from './open.js'; import OpenAPIConvertCommand from './openapi/convert.js'; import OpenAPICommand from './openapi/index.js'; import OpenAPIInspectCommand from './openapi/inspect.js'; import OpenAPIReduceCommand from './openapi/reduce.js'; import OpenAPIValidateCommand from './openapi/validate.js'; -import SwaggerCommand from './swagger.js'; import ValidateAliasCommand from './validate.js'; import CreateVersionCommand from './versions/create.js'; import DeleteVersionCommand from './versions/delete.js'; @@ -33,7 +30,6 @@ const commands = { docs: DocsCommand, 'docs:prune': DocsPruneCommand, - 'docs:edit': DocsEditCommand, guides: GuidesCommand, 'guides:prune': GuidesPruneCommand, @@ -44,7 +40,6 @@ const commands = { login: LoginCommand, logout: LogoutCommand, - oas: OASCommand, open: OpenCommand, openapi: OpenAPICommand, @@ -53,7 +48,6 @@ const commands = { 'openapi:reduce': OpenAPIReduceCommand, 'openapi:validate': OpenAPIValidateCommand, - swagger: SwaggerCommand, validate: ValidateAliasCommand, whoami: WhoAmICommand, }; diff --git a/src/cmds/oas.ts b/src/cmds/oas.ts deleted file mode 100644 index 0d17a8a98..000000000 --- a/src/cmds/oas.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ZeroAuthCommandOptions } from '../lib/baseCommand.js'; - -import Command, { CommandCategories } from '../lib/baseCommand.js'; -import isHidden from '../lib/decorators/isHidden.js'; - -@isHidden -export default class OASCommand extends Command { - constructor() { - super(); - - this.command = 'oas'; - this.usage = 'oas'; - this.description = 'Helpful OpenAPI generation tooling. [inactive]'; - this.cmdCategory = CommandCategories.UTILITIES; - - this.args = []; - } - - async run(opts: ZeroAuthCommandOptions) { - await super.run(opts); - - const message = [ - 'This `oas` integration is now inactive.', - "If you're looking to create an OpenAPI definition, we recommend https://npm.im/swagger-inline", - ]; - - return Promise.reject(new Error(message.join('\n\n'))); - } -} diff --git a/src/cmds/swagger.ts b/src/cmds/swagger.ts deleted file mode 100644 index 5ed5db8f5..000000000 --- a/src/cmds/swagger.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Options } from './openapi/index.js'; -import type { AuthenticatedCommandOptions } from '../lib/baseCommand.js'; - -import Command from '../lib/baseCommand.js'; -import isHidden from '../lib/decorators/isHidden.js'; - -import OpenAPICommand from './openapi/index.js'; - -@isHidden -export default class SwaggerCommand extends OpenAPICommand { - constructor() { - super(); - - this.command = 'swagger'; - this.usage = 'swagger [file] [options]'; - this.description = 'Alias for `rdme openapi`. [deprecated]'; - } - - async run(opts: AuthenticatedCommandOptions) { - Command.warn('`rdme swagger` has been deprecated. Please use `rdme openapi` instead.'); - return super.run(opts); - } -} diff --git a/src/index.ts b/src/index.ts index 5998a3d61..af2aab528 100644 --- a/src/index.ts +++ b/src/index.ts @@ -126,7 +126,7 @@ export default function rdme(rawProcessArgv: NodeJS.Process['argv']) { // // Instead of failing out to the user with an undecipherable "Unknown value: ..." error, let's // try to parse their request again but a tad less eager. - if ((e.name !== 'UNKNOWN_VALUE' || (e.name === 'UNKNOWN_VALUE' && !argv.version)) && argv.command !== 'oas') { + if (e.name !== 'UNKNOWN_VALUE' || (e.name === 'UNKNOWN_VALUE' && !argv.version)) { throw e; } From 7b951e4cb2f5266cffd8626cfeffdf8bca103759 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Thu, 14 Sep 2023 18:30:27 -0500 Subject: [PATCH 2/2] chore: knip cleanup --- knip.ts | 1 - package-lock.json | 6 ------ package.json | 1 - src/.sink.d.ts | 2 -- src/cmds/openapi/index.ts | 2 +- 5 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 src/.sink.d.ts diff --git a/knip.ts b/knip.ts index 439c5c8d9..de7c0b751 100644 --- a/knip.ts +++ b/knip.ts @@ -3,7 +3,6 @@ import type { KnipConfig } from 'knip'; const config: KnipConfig = { ignore: ['bin/*.js', 'vitest.single-threaded.config.ts'], ignoreBinaries: ['semantic-release'], - ignoreDependencies: ['editor'], }; export default config; diff --git a/package-lock.json b/package-lock.json index e65b973c8..218080d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "command-line-usage": "^7.0.1", "configstore": "^6.0.0", "debug": "^4.3.3", - "editor": "^1.0.0", "formdata-node": "^5.0.1", "gray-matter": "^4.0.1", "ignore": "^5.2.0", @@ -4935,11 +4934,6 @@ "wcwidth": "^1.0.1" } }, - "node_modules/editor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/editor/-/editor-1.0.0.tgz", - "integrity": "sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw==" - }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", diff --git a/package.json b/package.json index 5ed1bfc36..fb585b7ff 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "command-line-usage": "^7.0.1", "configstore": "^6.0.0", "debug": "^4.3.3", - "editor": "^1.0.0", "formdata-node": "^5.0.1", "gray-matter": "^4.0.1", "ignore": "^5.2.0", diff --git a/src/.sink.d.ts b/src/.sink.d.ts deleted file mode 100644 index eebce80a4..000000000 --- a/src/.sink.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// These packages don't have any TS types so we need to declare a module in order to use them. -declare module 'editor'; diff --git a/src/cmds/openapi/index.ts b/src/cmds/openapi/index.ts index 3c41211a4..8bebe0f6b 100644 --- a/src/cmds/openapi/index.ts +++ b/src/cmds/openapi/index.ts @@ -17,7 +17,7 @@ import readmeAPIFetch, { cleanHeaders, handleRes } from '../../lib/readmeAPIFetc import streamSpecToRegistry from '../../lib/streamSpecToRegistry.js'; import { getProjectVersion } from '../../lib/versionSelect.js'; -export interface Options { +interface Options { create?: boolean; dryRun?: boolean; id?: string;