Skip to content

Commit

Permalink
refactor(docs): move --cleanup option into docs:prune command (#644)
Browse files Browse the repository at this point in the history
* refactor(docs): move cleanup into separate command

* docs: update

* test: fix snapshot

god i hate this test lol

* test: smol cleanup

* chore: rename cleanup to prune

* chore: small naming convention change, lint

* chore: cleanup JSDoc

* fix: use prompt for every call instead of warning
  • Loading branch information
kanadgupta authored Oct 27, 2022
1 parent 0009d20 commit 4bc98c5
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 173 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,13 @@ 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, 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:prune path-to-markdown-files
```

#### Edit a Single ReadMe Doc on Your Local Machine

Expand Down
119 changes: 0 additions & 119 deletions __tests__/cmds/docs/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable no-console */
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';

Expand Down Expand Up @@ -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';
Expand Down
120 changes: 120 additions & 0 deletions __tests__/cmds/docs/prune.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import nock from 'nock';
import prompts from 'prompts';

import DocsPruneCommand from '../../../src/cmds/docs/prune';
import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock';

const docsPrune = new DocsPruneCommand();

const fixturesBaseDir = '__fixtures__/docs';

const key = 'API_KEY';
const version = '1.0.0';

describe('rdme docs:prune', () => {
const folder = `./__tests__/${fixturesBaseDir}/delete-docs`;

beforeAll(() => nock.disableNetConnect());

afterAll(() => nock.cleanAll());

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(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(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(docsPrune.run({ key, version: '1.0.0' })).rejects.toStrictEqual(
new Error('No folder provided. Usage `rdme docs:prune <folder> [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(docsPrune.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 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,
key,
version,
})
).rejects.toStrictEqual(new Error('Aborting, no changes were made.'));

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)
.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(
docsPrune.run({
folder,
key,
version,
})
).resolves.toBe('🗑️ successfully deleted `this-doc-should-be-missing-in-folder`.');

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')
.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(
docsPrune.run({
folder,
key,
version,
dryRun: true,
})
).resolves.toBe('🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.');

apiMocks.done();
versionMock.done();
});
});
8 changes: 7 additions & 1 deletion __tests__/lib/__snapshots__/commands.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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:prune",
"position": 2,
},
{
"description": "Edit a single file from your ReadMe project without saving locally. [deprecated]",
"hidden": true,
"name": "docs:edit",
"position": 2,
"position": 3,
},
{
"description": "Sync a single Markdown file to your ReadMe project.",
Expand Down
4 changes: 2 additions & 2 deletions src/cmds/docs/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ export type Options = {
};

@isHidden
export default class EditDocsCommand extends Command {
export default class DocsEditCommand extends Command {
constructor() {
super();

this.command = 'docs:edit';
this.usage = 'docs:edit <slug> [options]';
this.description = 'Edit a single file from your ReadMe project without saving locally. [deprecated]';
this.cmdCategory = CommandCategories.DOCS;
this.position = 2;
this.position = 3;

this.hiddenArgs = ['slug'];
this.args = [
Expand Down
42 changes: 4 additions & 38 deletions src/cmds/docs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<Options>) {
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}\`.`));
Expand All @@ -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}.`));
}

Expand All @@ -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 })
);
}
Expand Down
Loading

0 comments on commit 4bc98c5

Please sign in to comment.