Skip to content

Commit 4bc98c5

Browse files
authored
refactor(docs): move --cleanup option into docs:prune command (#644)
* 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
1 parent 0009d20 commit 4bc98c5

File tree

10 files changed

+242
-173
lines changed

10 files changed

+242
-173
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,13 @@ rdme docs path-to-markdown-files --version={project-version}
207207

208208
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).
209209

210-
#### Cleanup
210+
#### Prune
211211

212-
If you wish to delete documents from ReadMe that are no longer present in your local directory, pass the `--cleanup` option to the command.
212+
If you wish to delete documents from ReadMe that are no longer present in your local directory:
213+
214+
```sh
215+
rdme docs:prune path-to-markdown-files
216+
```
213217

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

__tests__/cmds/docs/index.test.ts

Lines changed: 0 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint-disable no-console */
2-
import crypto from 'crypto';
32
import fs from 'fs';
43
import path from 'path';
54

@@ -390,124 +389,6 @@ describe('rdme docs', () => {
390389
});
391390
});
392391

393-
describe('cleanup docs', () => {
394-
const folder = `./__tests__/${fixturesBaseDir}/delete-docs`;
395-
const someDocContent = fs.readFileSync(path.join(folder, 'some-doc.md'));
396-
const lastUpdatedHash = crypto.createHash('sha1').update(someDocContent).digest('hex');
397-
let consoleWarnSpy;
398-
399-
function getWarningCommandOutput() {
400-
return [consoleWarnSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n');
401-
}
402-
403-
beforeEach(() => {
404-
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
405-
});
406-
407-
afterEach(() => {
408-
consoleWarnSpy.mockRestore();
409-
});
410-
411-
it('should delete doc if file is missing and --cleanup option is used', async () => {
412-
const versionMock = getAPIMock()
413-
.get(`/api/v1/version/${version}`)
414-
.basicAuth({ user: key })
415-
.reply(200, { version });
416-
417-
const apiMocks = getAPIMockWithVersionHeader(version)
418-
.get('/api/v1/categories?perPage=20&page=1')
419-
.basicAuth({ user: key })
420-
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
421-
.get('/api/v1/categories/category1/docs')
422-
.basicAuth({ user: key })
423-
.reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }, { slug: 'some-doc' }])
424-
.delete('/api/v1/docs/this-doc-should-be-missing-in-folder')
425-
.basicAuth({ user: key })
426-
.reply(204, '')
427-
.get('/api/v1/docs/some-doc')
428-
.basicAuth({ user: key })
429-
.reply(200, { lastUpdatedHash });
430-
431-
await expect(
432-
docs.run({
433-
folder,
434-
key,
435-
version,
436-
cleanup: true,
437-
})
438-
).resolves.toBe(
439-
'🗑️ successfully deleted `this-doc-should-be-missing-in-folder`.\n' +
440-
'`some-doc` was not updated because there were no changes.'
441-
);
442-
const warningOutput = getWarningCommandOutput();
443-
expect(warningOutput).toBe(
444-
"⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs."
445-
);
446-
447-
apiMocks.done();
448-
versionMock.done();
449-
});
450-
451-
it('should return doc delete info for dry run', async () => {
452-
const versionMock = getAPIMock()
453-
.get(`/api/v1/version/${version}`)
454-
.basicAuth({ user: key })
455-
.reply(200, { version });
456-
const apiMocks = getAPIMockWithVersionHeader(version)
457-
.get('/api/v1/categories?perPage=20&page=1')
458-
.basicAuth({ user: key })
459-
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
460-
.get('/api/v1/categories/category1/docs')
461-
.basicAuth({ user: key })
462-
.reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }])
463-
.get('/api/v1/docs/some-doc')
464-
.basicAuth({ user: key })
465-
.reply(200, { lastUpdatedHash });
466-
await expect(
467-
docs.run({
468-
folder,
469-
key,
470-
version,
471-
cleanup: true,
472-
dryRun: true,
473-
})
474-
).resolves.toBe(
475-
'🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.\n' +
476-
'🎭 dry run! `some-doc` will not be updated because there were no changes.'
477-
);
478-
const warningOutput = getWarningCommandOutput();
479-
expect(warningOutput).toBe(
480-
"⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs."
481-
);
482-
483-
apiMocks.done();
484-
versionMock.done();
485-
});
486-
487-
it('should do nothing if using --cleanup but the folder is empty and the user aborted', async () => {
488-
prompts.inject([false]);
489-
490-
const versionMock = getAPIMock()
491-
.get(`/api/v1/version/${version}`)
492-
.basicAuth({ user: key })
493-
.reply(200, { version });
494-
495-
await expect(
496-
docs.run({
497-
folder: './__tests__/__fixtures__/ref-oas',
498-
key,
499-
version,
500-
cleanup: true,
501-
})
502-
).rejects.toStrictEqual(new Error('Aborting, no changes were made.'));
503-
504-
const warningOutput = getWarningCommandOutput();
505-
expect(warningOutput).toBe('');
506-
507-
versionMock.done();
508-
});
509-
});
510-
511392
describe('slug metadata', () => {
512393
it('should use provided slug', async () => {
513394
const slug = 'new-doc-slug';

__tests__/cmds/docs/prune.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import nock from 'nock';
2+
import prompts from 'prompts';
3+
4+
import DocsPruneCommand from '../../../src/cmds/docs/prune';
5+
import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock';
6+
7+
const docsPrune = new DocsPruneCommand();
8+
9+
const fixturesBaseDir = '__fixtures__/docs';
10+
11+
const key = 'API_KEY';
12+
const version = '1.0.0';
13+
14+
describe('rdme docs:prune', () => {
15+
const folder = `./__tests__/${fixturesBaseDir}/delete-docs`;
16+
17+
beforeAll(() => nock.disableNetConnect());
18+
19+
afterAll(() => nock.cleanAll());
20+
21+
it('should prompt for login if no API key provided', async () => {
22+
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
23+
prompts.inject(['this-is-not-an-email', 'password', 'subdomain']);
24+
await expect(docsPrune.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.'));
25+
consoleInfoSpy.mockRestore();
26+
});
27+
28+
it('should error in CI if no API key provided', async () => {
29+
process.env.TEST_CI = 'true';
30+
await expect(docsPrune.run({})).rejects.toStrictEqual(
31+
new Error('No project API key provided. Please use `--key`.')
32+
);
33+
delete process.env.TEST_CI;
34+
});
35+
36+
it('should error if no folder provided', () => {
37+
return expect(docsPrune.run({ key, version: '1.0.0' })).rejects.toStrictEqual(
38+
new Error('No folder provided. Usage `rdme docs:prune <folder> [options]`.')
39+
);
40+
});
41+
42+
it('should error if the argument is not a folder', async () => {
43+
const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });
44+
45+
await expect(docsPrune.run({ key, version: '1.0.0', folder: 'not-a-folder' })).rejects.toThrow(
46+
"ENOENT: no such file or directory, scandir 'not-a-folder'"
47+
);
48+
49+
versionMock.done();
50+
});
51+
52+
it('should do nothing if the user aborted', async () => {
53+
prompts.inject([false]);
54+
55+
const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });
56+
57+
await expect(
58+
docsPrune.run({
59+
folder,
60+
key,
61+
version,
62+
})
63+
).rejects.toStrictEqual(new Error('Aborting, no changes were made.'));
64+
65+
versionMock.done();
66+
});
67+
68+
it('should delete doc if file is missing', async () => {
69+
prompts.inject([true]);
70+
71+
const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });
72+
73+
const apiMocks = getAPIMockWithVersionHeader(version)
74+
.get('/api/v1/categories?perPage=20&page=1')
75+
.basicAuth({ user: key })
76+
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
77+
.get('/api/v1/categories/category1/docs')
78+
.basicAuth({ user: key })
79+
.reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }, { slug: 'some-doc' }])
80+
.delete('/api/v1/docs/this-doc-should-be-missing-in-folder')
81+
.basicAuth({ user: key })
82+
.reply(204, '');
83+
84+
await expect(
85+
docsPrune.run({
86+
folder,
87+
key,
88+
version,
89+
})
90+
).resolves.toBe('🗑️ successfully deleted `this-doc-should-be-missing-in-folder`.');
91+
92+
apiMocks.done();
93+
versionMock.done();
94+
});
95+
96+
it('should return doc delete info for dry run', async () => {
97+
prompts.inject([true]);
98+
99+
const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });
100+
const apiMocks = getAPIMockWithVersionHeader(version)
101+
.get('/api/v1/categories?perPage=20&page=1')
102+
.basicAuth({ user: key })
103+
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
104+
.get('/api/v1/categories/category1/docs')
105+
.basicAuth({ user: key })
106+
.reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }]);
107+
108+
await expect(
109+
docsPrune.run({
110+
folder,
111+
key,
112+
version,
113+
dryRun: true,
114+
})
115+
).resolves.toBe('🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.');
116+
117+
apiMocks.done();
118+
versionMock.done();
119+
});
120+
});

__tests__/lib/__snapshots__/commands.test.ts.snap

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,17 @@ exports[`utils #listByCategory should list commands by category 1`] = `
119119
"name": "docs",
120120
"position": 1,
121121
},
122+
{
123+
"description": "Delete any docs from ReadMe if their slugs are not found in the target folder.",
124+
"hidden": false,
125+
"name": "docs:prune",
126+
"position": 2,
127+
},
122128
{
123129
"description": "Edit a single file from your ReadMe project without saving locally. [deprecated]",
124130
"hidden": true,
125131
"name": "docs:edit",
126-
"position": 2,
132+
"position": 3,
127133
},
128134
{
129135
"description": "Sync a single Markdown file to your ReadMe project.",

src/cmds/docs/edit.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ export type Options = {
2424
};
2525

2626
@isHidden
27-
export default class EditDocsCommand extends Command {
27+
export default class DocsEditCommand extends Command {
2828
constructor() {
2929
super();
3030

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

3737
this.hiddenArgs = ['slug'];
3838
this.args = [

src/cmds/docs/index.ts

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,15 @@ import config from 'config';
55

66
import Command, { CommandCategories } from '../../lib/baseCommand';
77
import createGHA from '../../lib/createGHA';
8-
import deleteDoc from '../../lib/deleteDoc';
9-
import getDocs from '../../lib/getDocs';
10-
import * as promptHandler from '../../lib/prompts';
11-
import promptTerminal from '../../lib/promptWrapper';
128
import pushDoc from '../../lib/pushDoc';
139
import readdirRecursive from '../../lib/readdirRecursive';
14-
import readDoc from '../../lib/readDoc';
1510
import { getProjectVersion } from '../../lib/versionSelect';
1611

1712
export type Options = {
1813
dryRun?: boolean;
1914
folder?: string;
20-
cleanup?: boolean;
2115
};
2216

23-
function getSlug(filename: string): string {
24-
const { slug } = readDoc(filename);
25-
return slug;
26-
}
27-
2817
export default class DocsCommand extends Command {
2918
constructor() {
3019
super();
@@ -50,18 +39,13 @@ export default class DocsCommand extends Command {
5039
type: Boolean,
5140
description: 'Runs the command without creating/updating any docs in ReadMe. Useful for debugging.',
5241
},
53-
{
54-
name: 'cleanup',
55-
type: Boolean,
56-
description: 'Delete any docs from ReadMe if their slugs are not found in the target folder.',
57-
},
5842
];
5943
}
6044

6145
async run(opts: CommandOptions<Options>) {
6246
await super.run(opts);
6347

64-
const { dryRun, folder, key, version, cleanup } = opts;
48+
const { dryRun, folder, key, version } = opts;
6549

6650
if (!folder) {
6751
return Promise.reject(new Error(`No folder provided. Usage \`${config.get('cli')} ${this.usage}\`.`));
@@ -81,25 +65,7 @@ export default class DocsCommand extends Command {
8165

8266
Command.debug(`number of files: ${files.length}`);
8367

84-
const changes: string[] = [];
85-
if (cleanup) {
86-
if (!files.length) {
87-
const { deleteAll } = await promptTerminal(promptHandler.deleteDocsPrompt(selectedVersion));
88-
if (!deleteAll) {
89-
return Promise.reject(new Error('Aborting, no changes were made.'));
90-
}
91-
}
92-
93-
Command.warn(`We're going to delete from ReadMe any document that isn't found in ${folder}.`);
94-
const docs = await getDocs(key, selectedVersion);
95-
const docSlugs = docs.map(({ slug }: { slug: string }) => slug);
96-
const fileSlugs = new Set(files.map(getSlug));
97-
const slugsToDelete = docSlugs.filter((slug: string) => !fileSlugs.has(slug));
98-
const deletedDocs = await Promise.all(
99-
slugsToDelete.map((slug: string) => deleteDoc(key, selectedVersion, dryRun, slug, this.cmdCategory))
100-
);
101-
changes.push(...deletedDocs);
102-
} else if (!files.length) {
68+
if (!files.length) {
10369
return Promise.reject(new Error(`We were unable to locate Markdown files in ${folder}.`));
10470
}
10571

@@ -108,8 +74,8 @@ export default class DocsCommand extends Command {
10874
return pushDoc(key, selectedVersion, dryRun, filename, this.cmdCategory);
10975
})
11076
);
111-
changes.push(...updatedDocs);
112-
return Promise.resolve(chalk.green(changes.join('\n'))).then(msg =>
77+
78+
return Promise.resolve(chalk.green(updatedDocs.join('\n'))).then(msg =>
11379
createGHA(msg, this.command, this.args, { ...opts, version: selectedVersion })
11480
);
11581
}

0 commit comments

Comments
 (0)