From bb978344bf1916057734abfbb182d59cc4e705c4 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Wed, 26 Oct 2022 19:00:59 -0400 Subject: [PATCH 1/8] refactor(docs): move cleanup into separate command --- __tests__/cmds/docs/cleanup.test.ts | 142 ++++++++++++++++++++++++++++ __tests__/cmds/docs/index.test.ts | 119 ----------------------- src/cmds/docs/cleanup.ts | 102 ++++++++++++++++++++ src/cmds/docs/edit.ts | 2 +- src/cmds/docs/index.ts | 42 +------- src/cmds/index.ts | 2 + src/lib/prompts.ts | 8 -- 7 files changed, 251 insertions(+), 166 deletions(-) create mode 100644 __tests__/cmds/docs/cleanup.test.ts create mode 100644 src/cmds/docs/cleanup.ts diff --git a/__tests__/cmds/docs/cleanup.test.ts b/__tests__/cmds/docs/cleanup.test.ts new file mode 100644 index 000000000..538c5e02b --- /dev/null +++ b/__tests__/cmds/docs/cleanup.test.ts @@ -0,0 +1,142 @@ +import nock from 'nock'; +import prompts from 'prompts'; + +import DocsCleanupCommand from '../../../src/cmds/docs/cleanup'; +import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock'; + +const docsCleanup = new DocsCleanupCommand(); + +const fixturesBaseDir = '__fixtures__/docs'; + +const key = 'API_KEY'; +const version = '1.0.0'; + +describe('rdme docs:cleanup', () => { + const folder = `./__tests__/${fixturesBaseDir}/delete-docs`; + let consoleWarnSpy; + + function getWarningCommandOutput() { + return [consoleWarnSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); + } + + beforeAll(() => nock.disableNetConnect()); + + afterAll(() => nock.cleanAll()); + + beforeEach(() => { + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + }); + + afterEach(() => { + consoleWarnSpy.mockRestore(); + }); + + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(docsCleanup.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_CI = 'true'; + await expect(docsCleanup.run({})).rejects.toStrictEqual( + new Error('No project API key provided. Please use `--key`.') + ); + delete process.env.TEST_CI; + }); + + it('should error if no folder provided', () => { + return expect(docsCleanup.run({ key, version: '1.0.0' })).rejects.toThrow( + 'No folder provided. Usage `rdme docs:cleanup [options]`.' + ); + }); + + it('should error if the argument is not a folder', async () => { + const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); + + await expect(docsCleanup.run({ key, version: '1.0.0', folder: 'not-a-folder' })).rejects.toThrow( + "ENOENT: no such file or directory, scandir 'not-a-folder'" + ); + + versionMock.done(); + }); + + it('should do nothing if the folder is empty and the user aborted', async () => { + prompts.inject([false]); + + const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); + + await expect( + docsCleanup.run({ + folder: '.github/workflows', + key, + version, + }) + ).rejects.toStrictEqual(new Error('Aborting, no changes were made.')); + + const warningOutput = getWarningCommandOutput(); + expect(warningOutput).toBe(''); + + versionMock.done(); + }); + + it('should delete doc if file is missing', async () => { + const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); + + const apiMocks = getAPIMockWithVersionHeader(version) + .get('/api/v1/categories?perPage=20&page=1') + .basicAuth({ user: key }) + .reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' }) + .get('/api/v1/categories/category1/docs') + .basicAuth({ user: key }) + .reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }, { slug: 'some-doc' }]) + .delete('/api/v1/docs/this-doc-should-be-missing-in-folder') + .basicAuth({ user: key }) + .reply(204, ''); + + await expect( + docsCleanup.run({ + folder, + key, + version, + }) + ).resolves.toBe('🗑️ successfully deleted `this-doc-should-be-missing-in-folder`.'); + const warningOutput = getWarningCommandOutput(); + expect(warningOutput).toBe( + "⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs." + ); + + apiMocks.done(); + versionMock.done(); + }); + + it('should return doc delete info for dry run', async () => { + const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); + const apiMocks = getAPIMockWithVersionHeader(version) + .get('/api/v1/categories?perPage=20&page=1') + .basicAuth({ user: key }) + .reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' }) + .get('/api/v1/categories/category1/docs') + .basicAuth({ user: key }) + .reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }]); + + await expect( + docsCleanup.run({ + folder, + key, + version, + cleanup: true, + dryRun: true, + }) + ).resolves.toBe('🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.'); + + const warningOutput = getWarningCommandOutput(); + expect(warningOutput).toBe( + "⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs." + ); + + apiMocks.done(); + versionMock.done(); + }); +}); diff --git a/__tests__/cmds/docs/index.test.ts b/__tests__/cmds/docs/index.test.ts index d92d26aff..ae265eea6 100644 --- a/__tests__/cmds/docs/index.test.ts +++ b/__tests__/cmds/docs/index.test.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; @@ -390,124 +389,6 @@ describe('rdme docs', () => { }); }); - describe('cleanup docs', () => { - const folder = `./__tests__/${fixturesBaseDir}/delete-docs`; - const someDocContent = fs.readFileSync(path.join(folder, 'some-doc.md')); - const lastUpdatedHash = crypto.createHash('sha1').update(someDocContent).digest('hex'); - let consoleWarnSpy; - - function getWarningCommandOutput() { - return [consoleWarnSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); - } - - beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); - }); - - afterEach(() => { - consoleWarnSpy.mockRestore(); - }); - - it('should delete doc if file is missing and --cleanup option is used', async () => { - const versionMock = getAPIMock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const apiMocks = getAPIMockWithVersionHeader(version) - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' }) - .get('/api/v1/categories/category1/docs') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }, { slug: 'some-doc' }]) - .delete('/api/v1/docs/this-doc-should-be-missing-in-folder') - .basicAuth({ user: key }) - .reply(204, '') - .get('/api/v1/docs/some-doc') - .basicAuth({ user: key }) - .reply(200, { lastUpdatedHash }); - - await expect( - docs.run({ - folder, - key, - version, - cleanup: true, - }) - ).resolves.toBe( - '🗑️ successfully deleted `this-doc-should-be-missing-in-folder`.\n' + - '`some-doc` was not updated because there were no changes.' - ); - const warningOutput = getWarningCommandOutput(); - expect(warningOutput).toBe( - "⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs." - ); - - apiMocks.done(); - versionMock.done(); - }); - - it('should return doc delete info for dry run', async () => { - const versionMock = getAPIMock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - const apiMocks = getAPIMockWithVersionHeader(version) - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' }) - .get('/api/v1/categories/category1/docs') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }]) - .get('/api/v1/docs/some-doc') - .basicAuth({ user: key }) - .reply(200, { lastUpdatedHash }); - await expect( - docs.run({ - folder, - key, - version, - cleanup: true, - dryRun: true, - }) - ).resolves.toBe( - '🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.\n' + - '🎭 dry run! `some-doc` will not be updated because there were no changes.' - ); - const warningOutput = getWarningCommandOutput(); - expect(warningOutput).toBe( - "⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs." - ); - - apiMocks.done(); - versionMock.done(); - }); - - it('should do nothing if using --cleanup but the folder is empty and the user aborted', async () => { - prompts.inject([false]); - - const versionMock = getAPIMock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - docs.run({ - folder: './__tests__/__fixtures__/ref-oas', - key, - version, - cleanup: true, - }) - ).rejects.toStrictEqual(new Error('Aborting, no changes were made.')); - - const warningOutput = getWarningCommandOutput(); - expect(warningOutput).toBe(''); - - versionMock.done(); - }); - }); - describe('slug metadata', () => { it('should use provided slug', async () => { const slug = 'new-doc-slug'; diff --git a/src/cmds/docs/cleanup.ts b/src/cmds/docs/cleanup.ts new file mode 100644 index 000000000..b7c4f6aee --- /dev/null +++ b/src/cmds/docs/cleanup.ts @@ -0,0 +1,102 @@ +import type { CommandOptions } from '../../lib/baseCommand'; + +import chalk from 'chalk'; +import config from 'config'; + +import Command, { CommandCategories } from '../../lib/baseCommand'; +import createGHA from '../../lib/createGHA'; +import deleteDoc from '../../lib/deleteDoc'; +import getDocs from '../../lib/getDocs'; +import promptTerminal from '../../lib/promptWrapper'; +import readdirRecursive from '../../lib/readdirRecursive'; +import readDoc from '../../lib/readDoc'; +import { getProjectVersion } from '../../lib/versionSelect'; + +export type Options = { + dryRun?: boolean; + folder?: string; +}; + +function getSlug(filename: string): string { + const { slug } = readDoc(filename); + return slug; +} + +export default class DocsCleanupCommand extends Command { + constructor() { + super(); + + this.command = 'docs:cleanup'; + this.usage = 'docs:cleanup [options]'; + this.description = 'Delete any docs from ReadMe if their slugs are not found in the target folder.'; + this.cmdCategory = CommandCategories.DOCS; + this.position = 2; + + this.hiddenArgs = ['folder']; + this.args = [ + this.getKeyArg(), + this.getVersionArg(), + { + name: 'folder', + type: String, + defaultOption: true, + }, + this.getGitHubArg(), + { + name: 'dryRun', + type: Boolean, + description: 'Runs the command without creating/updating any docs in ReadMe. Useful for debugging.', + }, + ]; + } + + async run(opts: CommandOptions) { + await super.run(opts); + + const { dryRun, folder, key, version } = opts; + + if (!folder) { + return Promise.reject(new Error(`No folder provided. Usage \`${config.get('cli')} ${this.usage}\`.`)); + } + + // TODO: should we allow version selection at all here? + // Let's revisit this once we re-evaluate our category logic in the API. + // Ideally we should ignore this parameter entirely if the category is included. + const selectedVersion = await getProjectVersion(version, key); + + Command.debug(`selectedVersion: ${selectedVersion}`); + + // Strip out non-markdown files + const files = readdirRecursive(folder).filter( + file => file.toLowerCase().endsWith('.md') || file.toLowerCase().endsWith('.markdown') + ); + + Command.debug(`number of files: ${files.length}`); + + if (!files.length) { + const { deleteAll } = await promptTerminal({ + type: 'confirm', + name: 'deleteAll', + message: `This action will delete all docs under version ${selectedVersion} from ReadMe, would you like to continue?`, + }); + + if (!deleteAll) { + return Promise.reject(new Error('Aborting, no changes were made.')); + } + } + + Command.warn(`We're going to delete from ReadMe any document that isn't found in ${folder}.`); + + const docs = await getDocs(key, selectedVersion); + const docSlugs = docs.map(({ slug }: { slug: string }) => slug); + const fileSlugs = new Set(files.map(getSlug)); + const slugsToDelete = docSlugs.filter((slug: string) => !fileSlugs.has(slug)); + const deletedDocs = await Promise.all( + slugsToDelete.map((slug: string) => deleteDoc(key, selectedVersion, dryRun, slug, this.cmdCategory)) + ); + + return Promise.resolve(chalk.green(deletedDocs.join('\n'))).then(msg => + createGHA(msg, this.command, this.args, { ...opts, version: selectedVersion }) + ); + } +} diff --git a/src/cmds/docs/edit.ts b/src/cmds/docs/edit.ts index f2556dbe3..4adc44771 100644 --- a/src/cmds/docs/edit.ts +++ b/src/cmds/docs/edit.ts @@ -30,7 +30,7 @@ export default class EditDocsCommand extends Command { this.usage = 'docs:edit [options]'; this.description = 'Edit a single file from your ReadMe project without saving locally.'; this.cmdCategory = CommandCategories.DOCS; - this.position = 2; + this.position = 3; this.hiddenArgs = ['slug']; this.args = [ diff --git a/src/cmds/docs/index.ts b/src/cmds/docs/index.ts index ba0835075..7f2061c28 100644 --- a/src/cmds/docs/index.ts +++ b/src/cmds/docs/index.ts @@ -5,26 +5,15 @@ import config from 'config'; import Command, { CommandCategories } from '../../lib/baseCommand'; import createGHA from '../../lib/createGHA'; -import deleteDoc from '../../lib/deleteDoc'; -import getDocs from '../../lib/getDocs'; -import * as promptHandler from '../../lib/prompts'; -import promptTerminal from '../../lib/promptWrapper'; import pushDoc from '../../lib/pushDoc'; import readdirRecursive from '../../lib/readdirRecursive'; -import readDoc from '../../lib/readDoc'; import { getProjectVersion } from '../../lib/versionSelect'; export type Options = { dryRun?: boolean; folder?: string; - cleanup?: boolean; }; -function getSlug(filename: string): string { - const { slug } = readDoc(filename); - return slug; -} - export default class DocsCommand extends Command { constructor() { super(); @@ -50,18 +39,13 @@ export default class DocsCommand extends Command { type: Boolean, description: 'Runs the command without creating/updating any docs in ReadMe. Useful for debugging.', }, - { - name: 'cleanup', - type: Boolean, - description: 'Delete any docs from ReadMe if their slugs are not found in the target folder.', - }, ]; } async run(opts: CommandOptions) { await super.run(opts); - const { dryRun, folder, key, version, cleanup } = opts; + const { dryRun, folder, key, version } = opts; if (!folder) { return Promise.reject(new Error(`No folder provided. Usage \`${config.get('cli')} ${this.usage}\`.`)); @@ -81,25 +65,7 @@ export default class DocsCommand extends Command { Command.debug(`number of files: ${files.length}`); - const changes: string[] = []; - if (cleanup) { - if (!files.length) { - const { deleteAll } = await promptTerminal(promptHandler.deleteDocsPrompt(selectedVersion)); - if (!deleteAll) { - return Promise.reject(new Error('Aborting, no changes were made.')); - } - } - - Command.warn(`We're going to delete from ReadMe any document that isn't found in ${folder}.`); - const docs = await getDocs(key, selectedVersion); - const docSlugs = docs.map(({ slug }: { slug: string }) => slug); - const fileSlugs = new Set(files.map(getSlug)); - const slugsToDelete = docSlugs.filter((slug: string) => !fileSlugs.has(slug)); - const deletedDocs = await Promise.all( - slugsToDelete.map((slug: string) => deleteDoc(key, selectedVersion, dryRun, slug, this.cmdCategory)) - ); - changes.push(...deletedDocs); - } else if (!files.length) { + if (!files.length) { return Promise.reject(new Error(`We were unable to locate Markdown files in ${folder}.`)); } @@ -108,8 +74,8 @@ export default class DocsCommand extends Command { return pushDoc(key, selectedVersion, dryRun, filename, this.cmdCategory); }) ); - changes.push(...updatedDocs); - return Promise.resolve(chalk.green(changes.join('\n'))).then(msg => + + return Promise.resolve(chalk.green(updatedDocs.join('\n'))).then(msg => createGHA(msg, this.command, this.args, { ...opts, version: selectedVersion }) ); } diff --git a/src/cmds/index.ts b/src/cmds/index.ts index 3a1e61554..abdf4db8f 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -5,6 +5,7 @@ import SingleChangelogCommand from './changelogs/single'; import CustomPagesCommand from './custompages'; import SingleCustomPageCommand from './custompages/single'; import DocsCommand from './docs'; +import DocsCleanupCommand from './docs/cleanup'; import EditDocsCommand from './docs/edit'; import SingleDocCommand from './docs/single'; import LoginCommand from './login'; @@ -33,6 +34,7 @@ const commands = { 'custompages:single': SingleCustomPageCommand, docs: DocsCommand, + 'docs:cleanup': DocsCleanupCommand, 'docs:edit': EditDocsCommand, 'docs:single': SingleDocCommand, diff --git a/src/lib/prompts.ts b/src/lib/prompts.ts index 6d93d2021..33f1c1627 100644 --- a/src/lib/prompts.ts +++ b/src/lib/prompts.ts @@ -182,11 +182,3 @@ export function createVersionPrompt( }, ]; } - -export function deleteDocsPrompt(version: string): PromptObject { - return { - type: 'confirm', - name: 'deleteAll', - message: `This action will delete all docs under version ${version} from ReadMe, would you like to continue?`, - }; -} From 7ba08c7243e62c8836beb86cdd7ad76d8d7275cb Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Wed, 26 Oct 2022 19:02:48 -0400 Subject: [PATCH 2/8] docs: update --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cb4a37332..c16fcb152 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,11 @@ This command also has a dry run mode, which can be useful for initial setup and #### Cleanup -If you wish to delete documents from ReadMe that are no longer present in your local directory, pass the `--cleanup` option to the command. +If you wish to delete documents from ReadMe that are no longer present in your local directory: + +```sh +rdme docs:cleanup path-to-markdown-files +``` #### Edit a Single ReadMe Doc on Your Local Machine From 7f605628267fed3748ea42d7635e014d28fb8930 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Wed, 26 Oct 2022 19:06:36 -0400 Subject: [PATCH 3/8] test: fix snapshot god i hate this test lol --- __tests__/lib/__snapshots__/commands.test.ts.snap | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/__tests__/lib/__snapshots__/commands.test.ts.snap b/__tests__/lib/__snapshots__/commands.test.ts.snap index c86da70f4..acdfccc2b 100644 --- a/__tests__/lib/__snapshots__/commands.test.ts.snap +++ b/__tests__/lib/__snapshots__/commands.test.ts.snap @@ -119,11 +119,17 @@ exports[`utils #listByCategory should list commands by category 1`] = ` "name": "docs", "position": 1, }, + { + "description": "Delete any docs from ReadMe if their slugs are not found in the target folder.", + "hidden": false, + "name": "docs:cleanup", + "position": 2, + }, { "description": "Edit a single file from your ReadMe project without saving locally.", "hidden": false, "name": "docs:edit", - "position": 2, + "position": 3, }, { "description": "Sync a single Markdown file to your ReadMe project.", From c9382291213efd61cde23891a32b77855303220f Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Thu, 27 Oct 2022 09:41:58 -0400 Subject: [PATCH 4/8] test: smol cleanup --- __tests__/cmds/docs/cleanup.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/__tests__/cmds/docs/cleanup.test.ts b/__tests__/cmds/docs/cleanup.test.ts index 538c5e02b..163592746 100644 --- a/__tests__/cmds/docs/cleanup.test.ts +++ b/__tests__/cmds/docs/cleanup.test.ts @@ -126,7 +126,6 @@ describe('rdme docs:cleanup', () => { folder, key, version, - cleanup: true, dryRun: true, }) ).resolves.toBe('🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.'); From 863d06cdbeedf06a3cc2c6473f42035a04b8609f Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Thu, 27 Oct 2022 09:44:00 -0400 Subject: [PATCH 5/8] chore: rename cleanup to prune --- README.md | 4 ++-- .../docs/{cleanup.test.ts => prune.test.ts} | 22 +++++++++---------- .../lib/__snapshots__/commands.test.ts.snap | 2 +- src/cmds/docs/{cleanup.ts => prune.ts} | 6 ++--- src/cmds/index.ts | 4 ++-- 5 files changed, 19 insertions(+), 19 deletions(-) rename __tests__/cmds/docs/{cleanup.test.ts => prune.test.ts} (86%) rename src/cmds/docs/{cleanup.ts => prune.ts} (95%) diff --git a/README.md b/README.md index c16fcb152..326d8d817 100644 --- a/README.md +++ b/README.md @@ -207,12 +207,12 @@ rdme docs path-to-markdown-files --version={project-version} This command also has a dry run mode, which can be useful for initial setup and debugging. You can read more about dry run mode [in our docs](https://docs.readme.com/docs/rdme#dry-run-mode). -#### Cleanup +#### Prune If you wish to delete documents from ReadMe that are no longer present in your local directory: ```sh -rdme docs:cleanup path-to-markdown-files +rdme docs:prune path-to-markdown-files ``` #### Edit a Single ReadMe Doc on Your Local Machine diff --git a/__tests__/cmds/docs/cleanup.test.ts b/__tests__/cmds/docs/prune.test.ts similarity index 86% rename from __tests__/cmds/docs/cleanup.test.ts rename to __tests__/cmds/docs/prune.test.ts index 163592746..50bc8fdf7 100644 --- a/__tests__/cmds/docs/cleanup.test.ts +++ b/__tests__/cmds/docs/prune.test.ts @@ -1,17 +1,17 @@ import nock from 'nock'; import prompts from 'prompts'; -import DocsCleanupCommand from '../../../src/cmds/docs/cleanup'; +import DocsPruneCommand from '../../../src/cmds/docs/prune'; import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock'; -const docsCleanup = new DocsCleanupCommand(); +const docsPrune = new DocsPruneCommand(); const fixturesBaseDir = '__fixtures__/docs'; const key = 'API_KEY'; const version = '1.0.0'; -describe('rdme docs:cleanup', () => { +describe('rdme docs:prune', () => { const folder = `./__tests__/${fixturesBaseDir}/delete-docs`; let consoleWarnSpy; @@ -34,28 +34,28 @@ describe('rdme docs:cleanup', () => { it('should prompt for login if no API key provided', async () => { const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); - await expect(docsCleanup.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + await expect(docsPrune.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_CI = 'true'; - await expect(docsCleanup.run({})).rejects.toStrictEqual( + await expect(docsPrune.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); delete process.env.TEST_CI; }); it('should error if no folder provided', () => { - return expect(docsCleanup.run({ key, version: '1.0.0' })).rejects.toThrow( - 'No folder provided. Usage `rdme docs:cleanup [options]`.' + return expect(docsPrune.run({ key, version: '1.0.0' })).rejects.toThrow( + 'No folder provided. Usage `rdme docs:prune [options]`.' ); }); it('should error if the argument is not a folder', async () => { const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); - await expect(docsCleanup.run({ key, version: '1.0.0', folder: 'not-a-folder' })).rejects.toThrow( + await expect(docsPrune.run({ key, version: '1.0.0', folder: 'not-a-folder' })).rejects.toThrow( "ENOENT: no such file or directory, scandir 'not-a-folder'" ); @@ -68,7 +68,7 @@ describe('rdme docs:cleanup', () => { const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); await expect( - docsCleanup.run({ + docsPrune.run({ folder: '.github/workflows', key, version, @@ -96,7 +96,7 @@ describe('rdme docs:cleanup', () => { .reply(204, ''); await expect( - docsCleanup.run({ + docsPrune.run({ folder, key, version, @@ -122,7 +122,7 @@ describe('rdme docs:cleanup', () => { .reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }]); await expect( - docsCleanup.run({ + docsPrune.run({ folder, key, version, diff --git a/__tests__/lib/__snapshots__/commands.test.ts.snap b/__tests__/lib/__snapshots__/commands.test.ts.snap index acdfccc2b..676f7e800 100644 --- a/__tests__/lib/__snapshots__/commands.test.ts.snap +++ b/__tests__/lib/__snapshots__/commands.test.ts.snap @@ -122,7 +122,7 @@ exports[`utils #listByCategory should list commands by category 1`] = ` { "description": "Delete any docs from ReadMe if their slugs are not found in the target folder.", "hidden": false, - "name": "docs:cleanup", + "name": "docs:prune", "position": 2, }, { diff --git a/src/cmds/docs/cleanup.ts b/src/cmds/docs/prune.ts similarity index 95% rename from src/cmds/docs/cleanup.ts rename to src/cmds/docs/prune.ts index b7c4f6aee..0c91e191b 100644 --- a/src/cmds/docs/cleanup.ts +++ b/src/cmds/docs/prune.ts @@ -22,12 +22,12 @@ function getSlug(filename: string): string { return slug; } -export default class DocsCleanupCommand extends Command { +export default class DocsPruneCommand extends Command { constructor() { super(); - this.command = 'docs:cleanup'; - this.usage = 'docs:cleanup [options]'; + this.command = 'docs:prune'; + this.usage = 'docs:prune [options]'; this.description = 'Delete any docs from ReadMe if their slugs are not found in the target folder.'; this.cmdCategory = CommandCategories.DOCS; this.position = 2; diff --git a/src/cmds/index.ts b/src/cmds/index.ts index abdf4db8f..6ac229c01 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -5,7 +5,7 @@ import SingleChangelogCommand from './changelogs/single'; import CustomPagesCommand from './custompages'; import SingleCustomPageCommand from './custompages/single'; import DocsCommand from './docs'; -import DocsCleanupCommand from './docs/cleanup'; +import DocsPruneCommand from './docs/prune'; import EditDocsCommand from './docs/edit'; import SingleDocCommand from './docs/single'; import LoginCommand from './login'; @@ -34,7 +34,7 @@ const commands = { 'custompages:single': SingleCustomPageCommand, docs: DocsCommand, - 'docs:cleanup': DocsCleanupCommand, + 'docs:prune': DocsPruneCommand, 'docs:edit': EditDocsCommand, 'docs:single': SingleDocCommand, From 7f6d74aa9d5aa04f2e64e54398cc1578a15f84eb Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Thu, 27 Oct 2022 09:48:15 -0400 Subject: [PATCH 6/8] chore: small naming convention change, lint --- src/cmds/docs/edit.ts | 2 +- src/cmds/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmds/docs/edit.ts b/src/cmds/docs/edit.ts index 4adc44771..4da653193 100644 --- a/src/cmds/docs/edit.ts +++ b/src/cmds/docs/edit.ts @@ -22,7 +22,7 @@ export type Options = { slug?: string; }; -export default class EditDocsCommand extends Command { +export default class DocsEditCommand extends Command { constructor() { super(); diff --git a/src/cmds/index.ts b/src/cmds/index.ts index 6ac229c01..898057ddd 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -5,8 +5,8 @@ import SingleChangelogCommand from './changelogs/single'; import CustomPagesCommand from './custompages'; import SingleCustomPageCommand from './custompages/single'; import DocsCommand from './docs'; +import DocsEditCommand from './docs/edit'; import DocsPruneCommand from './docs/prune'; -import EditDocsCommand from './docs/edit'; import SingleDocCommand from './docs/single'; import LoginCommand from './login'; import LogoutCommand from './logout'; @@ -35,7 +35,7 @@ const commands = { docs: DocsCommand, 'docs:prune': DocsPruneCommand, - 'docs:edit': EditDocsCommand, + 'docs:edit': DocsEditCommand, 'docs:single': SingleDocCommand, versions: VersionsCommand, From c5033549f1426b26cd88bb5436955eedf36b614a Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Thu, 27 Oct 2022 10:01:51 -0400 Subject: [PATCH 7/8] chore: cleanup JSDoc --- src/lib/getDocs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/getDocs.ts b/src/lib/getDocs.ts index 7c629b1c2..4b64ee1d3 100644 --- a/src/lib/getDocs.ts +++ b/src/lib/getDocs.ts @@ -31,7 +31,7 @@ async function getCategoryDocs(key: string, selectedVersion: string, category: s } /** - * Retrieve the docs under all categories or under a specific one + * Retrieve the docs under all categories * * @param {String} key the project API key * @param {String} selectedVersion the project version From ac5580717a7aee33f3efe4034c34f17260c73172 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Thu, 27 Oct 2022 10:02:22 -0400 Subject: [PATCH 8/8] fix: use prompt for every call instead of warning --- __tests__/cmds/docs/prune.test.ts | 37 +++++++------------------------ src/cmds/docs/prune.ts | 20 +++++++---------- 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/__tests__/cmds/docs/prune.test.ts b/__tests__/cmds/docs/prune.test.ts index 50bc8fdf7..e313351be 100644 --- a/__tests__/cmds/docs/prune.test.ts +++ b/__tests__/cmds/docs/prune.test.ts @@ -13,24 +13,11 @@ const version = '1.0.0'; describe('rdme docs:prune', () => { const folder = `./__tests__/${fixturesBaseDir}/delete-docs`; - let consoleWarnSpy; - - function getWarningCommandOutput() { - return [consoleWarnSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); - } beforeAll(() => nock.disableNetConnect()); afterAll(() => nock.cleanAll()); - beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); - }); - - afterEach(() => { - consoleWarnSpy.mockRestore(); - }); - it('should prompt for login if no API key provided', async () => { const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); @@ -47,8 +34,8 @@ describe('rdme docs:prune', () => { }); it('should error if no folder provided', () => { - return expect(docsPrune.run({ key, version: '1.0.0' })).rejects.toThrow( - 'No folder provided. Usage `rdme docs:prune [options]`.' + return expect(docsPrune.run({ key, version: '1.0.0' })).rejects.toStrictEqual( + new Error('No folder provided. Usage `rdme docs:prune [options]`.') ); }); @@ -62,26 +49,25 @@ describe('rdme docs:prune', () => { versionMock.done(); }); - it('should do nothing if the folder is empty and the user aborted', async () => { + it('should do nothing if the user aborted', async () => { prompts.inject([false]); const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); await expect( docsPrune.run({ - folder: '.github/workflows', + folder, key, version, }) ).rejects.toStrictEqual(new Error('Aborting, no changes were made.')); - const warningOutput = getWarningCommandOutput(); - expect(warningOutput).toBe(''); - versionMock.done(); }); it('should delete doc if file is missing', async () => { + prompts.inject([true]); + const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); const apiMocks = getAPIMockWithVersionHeader(version) @@ -102,16 +88,14 @@ describe('rdme docs:prune', () => { version, }) ).resolves.toBe('🗑️ successfully deleted `this-doc-should-be-missing-in-folder`.'); - const warningOutput = getWarningCommandOutput(); - expect(warningOutput).toBe( - "⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs." - ); apiMocks.done(); versionMock.done(); }); it('should return doc delete info for dry run', async () => { + prompts.inject([true]); + const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); const apiMocks = getAPIMockWithVersionHeader(version) .get('/api/v1/categories?perPage=20&page=1') @@ -130,11 +114,6 @@ describe('rdme docs:prune', () => { }) ).resolves.toBe('🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.'); - const warningOutput = getWarningCommandOutput(); - expect(warningOutput).toBe( - "⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs." - ); - apiMocks.done(); versionMock.done(); }); diff --git a/src/cmds/docs/prune.ts b/src/cmds/docs/prune.ts index 0c91e191b..dc80cf087 100644 --- a/src/cmds/docs/prune.ts +++ b/src/cmds/docs/prune.ts @@ -73,20 +73,16 @@ export default class DocsPruneCommand extends Command { Command.debug(`number of files: ${files.length}`); - if (!files.length) { - const { deleteAll } = await promptTerminal({ - type: 'confirm', - name: 'deleteAll', - message: `This action will delete all docs under version ${selectedVersion} from ReadMe, would you like to continue?`, - }); - - if (!deleteAll) { - return Promise.reject(new Error('Aborting, no changes were made.')); - } + const { continueWithDeletion } = await promptTerminal({ + type: 'confirm', + name: 'continueWithDeletion', + message: `This command will delete all guides page from your ReadMe project (version ${selectedVersion}) that are not also in ${folder}, would you like to continue?`, + }); + + if (!continueWithDeletion) { + return Promise.reject(new Error('Aborting, no changes were made.')); } - Command.warn(`We're going to delete from ReadMe any document that isn't found in ${folder}.`); - const docs = await getDocs(key, selectedVersion); const docSlugs = docs.map(({ slug }: { slug: string }) => slug); const fileSlugs = new Set(files.map(getSlug));