Skip to content

Commit 8b4e889

Browse files
committed
refactor(cli): split up push/pull/link` funcs
Move the implementation of `push`/`pull`/`link` into their own functions, so that we can reuse them to add the feature for sync Mermaid diagrams from within markdown files.
1 parent bafd02c commit 8b4e889

File tree

3 files changed

+162
-84
lines changed

3 files changed

+162
-84
lines changed

packages/cli/src/commander.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ describe('pull', () => {
230230
program.parseAsync(['--config', CONFIG_AUTHED, 'pull', 'test/fixtures/unsynced.mmd'], {
231231
from: 'user',
232232
}),
233-
).rejects.toThrowError('Diagram has no id');
233+
).rejects.toThrowError('Diagram at test/fixtures/unsynced.mmd has no id');
234234
});
235235

236236
it('should fail if MermaidChart document has no code', async () => {
@@ -240,7 +240,7 @@ describe('pull', () => {
240240

241241
await expect(
242242
program.parseAsync(['--config', CONFIG_AUTHED, 'pull', diagram], { from: 'user' }),
243-
).rejects.toThrowError('Diagram has no code');
243+
).rejects.toThrowError(`Diagram at ${diagram} has no code`);
244244
});
245245

246246
it('should pull document and add a `id:` field to frontmatter', async () => {
@@ -280,7 +280,7 @@ describe('push', () => {
280280
program.parseAsync(['--config', CONFIG_AUTHED, 'push', 'test/fixtures/unsynced.mmd'], {
281281
from: 'user',
282282
}),
283-
).rejects.toThrowError('Diagram has no id');
283+
).rejects.toThrowError('Diagram at test/fixtures/unsynced.mmd has no id');
284284
});
285285

286286
it('should push document and remove the `id:` field front frontmatter', async () => {

packages/cli/src/commander.ts

Lines changed: 33 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import {
55
InvalidArgumentError,
66
} from '@commander-js/extra-typings';
77
import { readFile, writeFile } from 'fs/promises';
8-
import { extractFrontMatter, removeFrontMatterKeys, injectFrontMatter } from './frontmatter.js';
98
import { MermaidChart } from '@mermaidchart/sdk';
109

1110
import input from '@inquirer/input';
1211
import select, { Separator } from '@inquirer/select';
1312
import { type Config, defaultConfigPath, readConfig, writeConfig } from './config.js';
13+
import { link, pull, push } from './methods.js';
1414

1515
/**
1616
* Global configuration option for the root Commander Command.
@@ -156,58 +156,45 @@ function logout() {
156156
});
157157
}
158158

159-
function link() {
159+
function linkCmd() {
160160
return createCommand('link')
161161
.description('Link the given Mermaid diagram to Mermaid Chart')
162162
.addArgument(new Argument('<path>', 'The path of the file to link.'))
163163
.action(async (path, _options, command) => {
164164
const optsWithGlobals = command.optsWithGlobals<CommonOptions>();
165165
const client = await createClient(optsWithGlobals);
166+
const linkCache = {};
167+
166168
const existingFile = await readFile(path, { encoding: 'utf8' });
167-
const frontmatter = extractFrontMatter(existingFile);
168-
169-
if (frontmatter.metadata.id) {
170-
throw new CommanderError(
171-
/*exitCode=*/ 1,
172-
'EALREADY_LINKED',
173-
'This document already has an `id` field',
174-
);
175-
}
176169

177-
const projects = await client.getProjects();
178-
179-
const projectId = await select({
180-
message: 'Select a project to upload your document to',
181-
choices: [
182-
...projects.map((project) => {
183-
return {
184-
name: project.title,
185-
value: project.id,
186-
};
187-
}),
188-
new Separator(
189-
`Or go to ${new URL('/app/projects', client.baseURL)} to create a new project`,
190-
),
191-
],
170+
const linkedDiagram = await link(existingFile, client, {
171+
cache: linkCache,
172+
title: path,
173+
async getProjectId(cache) {
174+
cache.projects = cache.projects ?? client.getProjects();
175+
const projectId = await select({
176+
message: `Select a project to upload ${path} to`,
177+
choices: [
178+
...(await cache.projects).map((project) => {
179+
return {
180+
name: project.title,
181+
value: project.id,
182+
};
183+
}),
184+
new Separator(
185+
`Or go to ${new URL('/app/projects', client.baseURL)} to create a new project`,
186+
),
187+
],
188+
});
189+
return projectId;
190+
},
192191
});
193192

194-
const createdDocument = await client.createDocument(projectId);
195-
196-
const code = injectFrontMatter(existingFile, { id: createdDocument.documentID });
197-
198-
await Promise.all([
199-
writeFile(path, code, { encoding: 'utf8' }),
200-
client.setDocument({
201-
projectID: createdDocument.projectID,
202-
documentID: createdDocument.documentID,
203-
title: path,
204-
code: existingFile,
205-
}),
206-
]);
193+
await writeFile(path, linkedDiagram, { encoding: 'utf8' });
207194
});
208195
}
209196

210-
function pull() {
197+
function pullCmd() {
211198
return createCommand('pull')
212199
.description('Pulls a document from from Mermaid Chart')
213200
.addArgument(new Argument('<path>', 'The path of the file to pull.'))
@@ -216,21 +203,8 @@ function pull() {
216203
const optsWithGlobals = command.optsWithGlobals<CommonOptions>();
217204
const client = await createClient(optsWithGlobals);
218205
const text = await readFile(path, { encoding: 'utf8' });
219-
const frontmatter = extractFrontMatter(text);
220206

221-
if (frontmatter.metadata.id === undefined) {
222-
throw new Error('Diagram has no id, have you run `link` yet?');
223-
}
224-
225-
const uploadedFile = await client.getDocument({
226-
documentID: frontmatter.metadata.id,
227-
});
228-
229-
if (uploadedFile.code === undefined) {
230-
throw new Error('Diagram has no code, please use push first');
231-
}
232-
233-
const newFile = injectFrontMatter(uploadedFile.code, { id: frontmatter.metadata.id });
207+
const newFile = await pull(text, client, { title: path });
234208

235209
if (text === newFile) {
236210
console.log(`✅ - ${path} is up to date`);
@@ -246,38 +220,16 @@ function pull() {
246220
});
247221
}
248222

249-
function push() {
223+
function pushCmd() {
250224
return createCommand('push')
251225
.description('Push a local diagram to Mermaid Chart')
252226
.addArgument(new Argument('<path>', 'The path of the file to push.'))
253227
.action(async (path, _options, command) => {
254228
const optsWithGlobals = command.optsWithGlobals<CommonOptions>();
255229
const client = await createClient(optsWithGlobals);
256230
const text = await readFile(path, { encoding: 'utf8' });
257-
const frontmatter = extractFrontMatter(text);
258-
259-
if (frontmatter.metadata.id === undefined) {
260-
throw new Error('Diagram has no id, have you run `link` yet?');
261-
}
262-
263-
// TODO: check if file has changed since last push and print a warning
264-
const existingDiagram = await client.getDocument({
265-
documentID: frontmatter.metadata.id,
266-
});
267231

268-
// due to MC-1056, try to remove YAML frontmatter if we can
269-
const diagramToUpload = removeFrontMatterKeys(text, new Set(['id']));
270-
271-
if (existingDiagram.code === diagramToUpload) {
272-
console.log(`✅ - ${path} is up to date`);
273-
} else {
274-
await client.setDocument({
275-
projectID: existingDiagram.projectID,
276-
documentID: existingDiagram.documentID,
277-
code: diagramToUpload,
278-
});
279-
console.log(`✅ - ${path} was pushed`);
280-
}
232+
await push(text, client, { title: path });
281233
});
282234
}
283235

@@ -297,7 +249,7 @@ export function createCommanderCommand() {
297249
.addCommand(whoami())
298250
.addCommand(login())
299251
.addCommand(logout())
300-
.addCommand(link())
301-
.addCommand(pull())
302-
.addCommand(push());
252+
.addCommand(linkCmd())
253+
.addCommand(pullCmd())
254+
.addCommand(pushCmd());
303255
}

packages/cli/src/methods.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type { MermaidChart } from '@mermaidchart/sdk';
2+
3+
import { CommanderError } from '@commander-js/extra-typings';
4+
import { extractFrontMatter, injectFrontMatter, removeFrontMatterKeys } from './frontmatter.js';
5+
6+
/**
7+
* Cached data to use when pulling/pushing/linking multiple files at once.
8+
*/
9+
interface Cache {
10+
/**
11+
* If set, the user has said to use the projectId to create all documents
12+
* in.
13+
*/
14+
selectedProjectId?: string;
15+
/**
16+
* Cached response from {@link MermaidChart.getProjects}.
17+
*/
18+
projects?: ReturnType<MermaidChart['getProjects']>;
19+
}
20+
21+
interface CommonOptions {
22+
/** Description of diagram to use when sending messages to the user */
23+
title: string;
24+
}
25+
26+
interface LinkOptions extends CommonOptions {
27+
/** Function that asks the user which project id they want to upload a diagram to */
28+
getProjectId: (cache: LinkOptions['cache']) => Promise<string>;
29+
// cache to be shared between link calls
30+
cache: Cache;
31+
}
32+
33+
/**
34+
* Creates a new diagram on MermaidChart.com for the given local diagram.
35+
*
36+
* @returns The diagram with an added `id: xxxx` field.
37+
*/
38+
export async function link(diagram: string, client: MermaidChart, options: LinkOptions) {
39+
const frontmatter = extractFrontMatter(diagram);
40+
41+
if (frontmatter.metadata.id) {
42+
throw new CommanderError(
43+
/*exitCode=*/ 1,
44+
'EALREADY_LINKED',
45+
'This document already has an `id` field',
46+
);
47+
}
48+
49+
const { title, getProjectId, cache } = options;
50+
51+
const projectId = cache.selectedProjectId ?? (await getProjectId(cache));
52+
53+
const createdDocument = await client.createDocument(projectId);
54+
55+
await client.setDocument({
56+
projectID: createdDocument.projectID,
57+
documentID: createdDocument.documentID,
58+
title,
59+
code: diagram,
60+
});
61+
62+
const diagramWithId = injectFrontMatter(diagram, { id: createdDocument.documentID });
63+
64+
return diagramWithId;
65+
}
66+
67+
interface PullOptions extends CommonOptions {}
68+
69+
/**
70+
* Pulls down a diagram from MermaidChart.com
71+
*
72+
* @param diagram - The local diagram. This should have an `id: ` field in the YAML frontmatter.
73+
*
74+
* @returns The updated diagram. This may equal `diagram` if there were no changes.
75+
*/
76+
export async function pull(diagram: string, client: MermaidChart, { title }: PullOptions) {
77+
const frontmatter = extractFrontMatter(diagram);
78+
79+
if (frontmatter.metadata.id === undefined) {
80+
throw new Error(`Diagram at ${title} has no id, have you run \`link\` yet?`);
81+
}
82+
83+
const uploadedFile = await client.getDocument({
84+
documentID: frontmatter.metadata.id,
85+
});
86+
87+
if (uploadedFile.code === undefined) {
88+
throw new Error(`Diagram at ${title} has no code, please use \`push\` first.`);
89+
}
90+
91+
const newFile = injectFrontMatter(uploadedFile.code, { id: frontmatter.metadata.id });
92+
93+
return newFile;
94+
}
95+
96+
interface PushOptions extends CommonOptions {}
97+
98+
/**
99+
* Push the given diagram to MermaidChart.com
100+
*/
101+
export async function push(diagram: string, client: MermaidChart, { title }: PushOptions) {
102+
const frontmatter = extractFrontMatter(diagram);
103+
104+
if (frontmatter.metadata.id === undefined) {
105+
throw new Error(`Diagram at ${title} has no id, have you run \`link\` yet?`);
106+
}
107+
108+
// TODO: check if file has changed since last push and print a warning
109+
const existingDiagram = await client.getDocument({
110+
documentID: frontmatter.metadata.id,
111+
});
112+
113+
// due to MC-1056, try to remove YAML frontmatter if we can
114+
const diagramToUpload = removeFrontMatterKeys(diagram, new Set(['id']));
115+
116+
if (existingDiagram.code === diagramToUpload) {
117+
console.log(`✅ - ${title} is up to date`);
118+
} else {
119+
await client.setDocument({
120+
projectID: existingDiagram.projectID,
121+
documentID: existingDiagram.documentID,
122+
code: diagramToUpload,
123+
});
124+
console.log(`✅ - ${title} was pushed`);
125+
}
126+
}

0 commit comments

Comments
 (0)