Skip to content

Commit

Permalink
feat: review!
Browse files Browse the repository at this point in the history
  • Loading branch information
kellyjosephprice committed Feb 14, 2024
1 parent 699b7e2 commit afe367d
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 58 deletions.
6 changes: 6 additions & 0 deletions __tests__/__fixtures__/docs/docs-with-parent-ids/child.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Child
parentDocSlug: parent
---

# Child Body
5 changes: 5 additions & 0 deletions __tests__/__fixtures__/docs/docs-with-parent-ids/friend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Friend
---

# Friend Body
5 changes: 5 additions & 0 deletions __tests__/__fixtures__/docs/docs-with-parent-ids/parent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Parent
---

# Parent Body
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: With Parent Doc
parentDoc: 1234
---

# With Parent Doc Body
6 changes: 6 additions & 0 deletions __tests__/__fixtures__/docs/multiple-docs-no-parents/child.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Child
parentDocSlug: parent
---

# Child Body
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Friend
---

# Friend Body
93 changes: 87 additions & 6 deletions __tests__/cmds/docs/multiple.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,99 @@ describe('rdme docs (multiple)', () => {
versionMock.done();
});

it('should return an error message when it encounters a cycle', async () => {
const dir = 'multiple-docs-cycle';
it('should upload docs with parent doc ids first', async () => {
const dir = 'docs-with-parent-ids';
const slugs = ['child', 'friend', 'with-parent-doc', 'parent'];
let id = 1234;

const mocks = slugs.flatMap(slug => {
const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));
const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));

return [
getAPIMockWithVersionHeader(version)
.get(`/api/v1/docs/${slug}`)
.basicAuth({ user: key })
.reply(404, {
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 [email protected] and mention log "fake-metrics-uuid".',
}),
getAPIMockWithVersionHeader(version)
.post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash })
.basicAuth({ user: key })
// eslint-disable-next-line no-plusplus
.reply(201, { slug, _id: id++, body: doc.content, ...doc.data, lastUpdatedHash: hash }),
];
});

const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

const promise = docs.run({ filePath: `./__tests__/${fixturesBaseDir}/${dir}`, key, version });

await expect(promise).resolves.toStrictEqual(
[
`🌱 successfully created 'with-parent-doc' (ID: 1236) with contents from __tests__/${fixturesBaseDir}/${dir}/with-parent-doc.md`,
`🌱 successfully created 'friend' (ID: 1235) with contents from __tests__/${fixturesBaseDir}/${dir}/friend.md`,
`🌱 successfully created 'parent' (ID: 1237) with contents from __tests__/${fixturesBaseDir}/${dir}/parent.md`,
`🌱 successfully created 'child' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/${dir}/child.md`,
].join('\n'),
);

mocks.forEach(mock => mock.done());
versionMock.done();
});

it('should upload child docs without the parent', async () => {
const dir = 'multiple-docs-no-parents';
const slugs = ['child', 'friend'];
let id = 1234;

const mocks = slugs.flatMap(slug => {
const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));
const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));

return [
getAPIMockWithVersionHeader(version)
.get(`/api/v1/docs/${slug}`)
.basicAuth({ user: key })
.reply(404, {
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 [email protected] and mention log "fake-metrics-uuid".',
}),
getAPIMockWithVersionHeader(version)
.post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash })
.basicAuth({ user: key })
// eslint-disable-next-line no-plusplus
.reply(201, { slug, _id: id++, body: doc.content, ...doc.data, lastUpdatedHash: hash }),
];
});

const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

const promise = docs.run({ filePath: `./__tests__/${fixturesBaseDir}/${dir}`, key, version });

await expect(promise).rejects.toStrictEqual(
new Error(
`Unable to resolve docs parent hierarchy. A cycle was found with: __tests__/${fixturesBaseDir}/${dir}/parent.md`,
),
await expect(promise).resolves.toStrictEqual(
[
`🌱 successfully created 'child' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/${dir}/child.md`,
`🌱 successfully created 'friend' (ID: 1235) with contents from __tests__/${fixturesBaseDir}/${dir}/friend.md`,
].join('\n'),
);

mocks.forEach(mock => mock.done());
versionMock.done();
});

it('should return an error message when it encounters a cycle', async () => {
const dir = 'multiple-docs-cycle';
const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

const promise = docs.run({ filePath: `./__tests__/${fixturesBaseDir}/${dir}`, key, version });

await expect(promise).rejects.toThrow('Cyclic dependency');
versionMock.done();
});
});
18 changes: 10 additions & 8 deletions src/lib/readDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import grayMatter from 'gray-matter';

import { debug } from './logger.js';

interface ReadDocMetadata {
export interface ReadDocMetadata {
/** The contents of the file below the YAML front matter */
content: string;
/** A JSON object with the YAML front matter */
data: Record<string, unknown>;
/** The original filePath */
filePath: string;
/**
* A hash of the file contents (including the front matter)
*/
Expand All @@ -22,19 +24,19 @@ interface ReadDocMetadata {
/**
* Returns the content, matter and slug of the specified Markdown or HTML file
*
* @param {String} filepath path to the HTML/Markdown file
* @param {String} filePath path to the HTML/Markdown file
* (file extension must end in `.html`, `.md`., or `.markdown`)
*/
export default function readDoc(filepath: string): ReadDocMetadata {
debug(`reading file ${filepath}`);
const rawFileContents = fs.readFileSync(filepath, 'utf8');
export default function readDoc(filePath: string): ReadDocMetadata {
debug(`reading file ${filePath}`);
const rawFileContents = fs.readFileSync(filePath, 'utf8');
const matter = grayMatter(rawFileContents);
const { content, data } = matter;
debug(`front matter for ${filepath}: ${JSON.stringify(matter)}`);
debug(`front matter for ${filePath}: ${JSON.stringify(matter)}`);

// Stripping the subdirectories and markdown extension from the filename and lowercasing to get the default slug.
const slug = matter.data.slug || path.basename(filepath).replace(path.extname(filepath), '').toLowerCase();
const slug = matter.data.slug || path.basename(filePath).replace(path.extname(filePath), '').toLowerCase();

const hash = crypto.createHash('sha1').update(rawFileContents).digest('hex');
return { content, data, hash, slug };
return { content, data, filePath, hash, slug };
}
71 changes: 27 additions & 44 deletions src/lib/syncDocsPath.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import fsSync from 'node:fs';
import type { ReadDocMetadata } from './readDoc.js';

import fs from 'node:fs/promises';
import path from 'node:path';

import chalk from 'chalk';
import frontMatter from 'gray-matter';
import { Headers } from 'node-fetch';
import toposort from 'toposort';

Expand All @@ -30,10 +30,10 @@ async function pushDoc(
key: string,
selectedVersion: string | undefined,
dryRun: boolean,
filePath: string,
fileData: ReadDocMetadata,
type: CommandCategories,
) {
const { content, data, hash, slug } = readDoc(filePath);
const { content, data, filePath, hash, slug } = fileData;

// TODO: ideally we should offer a zero-configuration approach that doesn't
// require YAML front matter, but that will have to be a breaking change
Expand Down Expand Up @@ -130,36 +130,24 @@ async function pushDoc(
});
}

function sortFiles(files: string[], { allowedFileExtensions }: { allowedFileExtensions: string[] }): string[] {
const extensionRegexp = new RegExp(`(${allowedFileExtensions.join('|')})$`);

const filesBySlug = files
.map(filePath => {
const doc = frontMatter(fsSync.readFileSync(filePath, 'utf8'));
const slug = path.basename(filePath).replace(extensionRegexp, '');
const parentDocSlug = doc.data.parentDocSlug;

return { filePath, slug, parentDocSlug };
})
.reduce<Record<string, { filePath: string; parentDocSlug: string; slug: string }>>(
(bySlug, obj) => {
// eslint-disable-next-line no-param-reassign
bySlug[obj.slug] = obj;
return bySlug;
},
{},
);

const dependencies = Object.values(filesBySlug).reduce<[string, string][]>(
(edges, obj) => {
if (obj.parentDocSlug) {
edges.push([filesBySlug[obj.parentDocSlug].filePath, filesBySlug[obj.slug].filePath]);
}
const byParentDoc = (left: ReadDocMetadata, right: ReadDocMetadata) => {
return (right.data.parentDoc ? 1 : 0) - (left.data.parentDoc ? 1 : 0);
};

function sortFiles(filePaths: string[]): ReadDocMetadata[] {
const files = filePaths.map(readDoc).sort(byParentDoc);
const filesBySlug = files.reduce<Record<string, ReadDocMetadata>>((bySlug, obj) => {
// eslint-disable-next-line no-param-reassign
bySlug[obj.slug] = obj;
return bySlug;
}, {});
const dependencies = Object.values(filesBySlug).reduce<[ReadDocMetadata, ReadDocMetadata][]>((edges, obj) => {
if (obj.data.parentDocSlug && filesBySlug[obj.data.parentDocSlug as string]) {
edges.push([filesBySlug[obj.data.parentDocSlug as string], filesBySlug[obj.slug]]);
}

return edges;
},
[],
);
return edges;
}, []);

return toposort.array(files, dependencies);
}
Expand Down Expand Up @@ -218,22 +206,15 @@ export default async function syncDocsPath(
let sortedFiles;

try {
sortedFiles = sortFiles(files, { allowedFileExtensions });
sortedFiles = sortFiles(files);
} catch (e) {
if (e.message.match(/Cyclic dependency/)) {
const filePath = e.message.replace(/^.*"(.*)"/, '$1');
return Promise.reject(
new Error(`Unable to resolve docs parent hierarchy. A cycle was found with: ${filePath}`),
);
}

return Promise.reject(e);
}

output = (
await Promise.all(
sortedFiles.map(async filename => {
return pushDoc(key, selectedVersion, dryRun, filename, cmdType);
sortedFiles.map(async fileData => {
return pushDoc(key, selectedVersion, dryRun, fileData, cmdType);
}),
)
).join('\n');
Expand All @@ -248,7 +229,9 @@ export default async function syncDocsPath(
),
);
}
output = await pushDoc(key, selectedVersion, dryRun, pathInput, cmdType);

const fileData = readDoc(pathInput);
output = await pushDoc(key, selectedVersion, dryRun, fileData, cmdType);
}
return Promise.resolve(chalk.green(output));
}

0 comments on commit afe367d

Please sign in to comment.