@mintlify/cli
Version:
The Mintlify CLI
222 lines (191 loc) • 7.24 kB
text/typescript
import { getConfigObj, getConfigPath } from '@mintlify/prebuild';
import * as previewing from '@mintlify/previewing';
import { validateDocsConfig } from '@mintlify/validation';
import fs from 'fs';
import { outputFile } from 'fs-extra';
import { migrateMdx } from '../src/migrateMdx.js';
vi.mock('../src/constants.js', () => ({
HOME_DIR: '/home/test-user',
CMD_EXEC_PATH: '/project',
}));
vi.mock('@mintlify/prebuild', async () => {
const original = await vi.importActual<typeof import('@mintlify/prebuild')>('@mintlify/prebuild');
return {
...original,
getConfigPath: vi.fn(),
getConfigObj: vi.fn(),
};
});
vi.mock('@mintlify/validation', async () => {
const original =
await vi.importActual<typeof import('@mintlify/validation')>('@mintlify/validation');
return {
...original,
validateDocsConfig: vi.fn(),
};
});
vi.mock('fs-extra', () => ({
outputFile: vi.fn(),
}));
const addLogSpy = vi.spyOn(previewing, 'addLog');
describe('migrateMdx', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const mkDirent = (name: string, isDir: boolean) =>
({ name, isDirectory: () => isDir, isFile: () => !isDir }) as unknown as fs.Dirent;
const readdirSpy = vi.spyOn(fs.promises, 'readdir');
readdirSpy.mockImplementation(async (p: Parameters<typeof fs.promises.readdir>[0]) => {
const value = typeof p === 'string' ? p : (p as unknown as { toString(): string }).toString();
if (value === '/project') {
return [
mkDirent('api', true),
mkDirent('webhooks', true),
mkDirent('openapi.json', false),
mkDirent('openapi1.json', false),
];
}
if (value === '/project/api') {
return [mkDirent('pets.mdx', false)];
}
if (value === '/project/webhooks') {
return [mkDirent('newPet.mdx', false)];
}
return [] as unknown as fs.Dirent[];
});
it('logs and exits when docs.json is not found', async () => {
vi.mocked(getConfigPath).mockResolvedValueOnce(undefined as unknown as string);
await migrateMdx();
expect(addLogSpy).toHaveBeenCalledWith(
expect.objectContaining({ props: { message: 'docs.json not found in current directory' } })
);
});
it('migrates a path operation MDX page to x-mint and updates docs.json and spec', async () => {
vi.mocked(getConfigPath).mockResolvedValueOnce('/project/docs.json');
vi.mocked(getConfigObj).mockResolvedValueOnce({
navigation: {
pages: ['api/pets'],
},
} as unknown as object);
vi.mocked(validateDocsConfig).mockResolvedValueOnce({
success: true,
data: {
navigation: {
pages: ['api/pets'],
},
},
} as unknown as ReturnType<typeof validateDocsConfig>);
const existsSyncSpy = vi.spyOn(fs, 'existsSync');
existsSyncSpy.mockImplementation((p: fs.PathLike) => {
const value = String(p);
return value === '/project/api/pets.mdx' || value === 'openapi.json';
});
const readFileSpy = vi.spyOn(fs.promises, 'readFile');
readFileSpy.mockImplementation(async (p: Parameters<typeof fs.promises.readFile>[0]) => {
const value = typeof p === 'string' ? p : (p as unknown as { toString(): string }).toString();
if (value === '/project/api/pets.mdx') {
return `---\nopenapi: openapi.json GET /pets\n---\n\n# Title\nContent`;
}
if (value === '/project/openapi.json') {
return JSON.stringify({
openapi: '3.0.0',
info: { title: 'Test', version: '1.0.0' },
paths: {
'/pets': {
get: {
summary: 'List pets',
responses: { '200': { description: 'ok' } },
},
},
},
});
}
throw new Error('Unexpected readFile path: ' + value);
});
const unlinkSpy = vi.spyOn(fs.promises, 'unlink').mockResolvedValueOnce();
await migrateMdx();
expect(addLogSpy).toHaveBeenCalledWith(
expect.objectContaining({ props: { message: 'docs.json updated' } })
);
expect(outputFile).toHaveBeenCalledWith(
'/project/docs.json',
expect.stringContaining('openapi.json get /pets')
);
const specWriteCall = vi
.mocked(outputFile)
.mock.calls.find(
(c) => typeof c[0] === 'string' && (c[0] as string).includes('openapi.json')
);
expect(specWriteCall).toBeTruthy();
const writtenSpec = JSON.parse(specWriteCall?.[1] as string);
expect(writtenSpec.paths['/pets'].get['x-mint']).toEqual(
expect.objectContaining({ href: '/api/pets' })
);
expect(unlinkSpy).toHaveBeenCalledWith('/project/api/pets.mdx');
expect(addLogSpy).toHaveBeenCalledWith(
expect.objectContaining({ props: { message: 'migration complete' } })
);
});
it('migrates a webhook MDX page to x-mint on the webhook operation', async () => {
vi.mocked(getConfigPath).mockResolvedValueOnce('/project/docs.json');
vi.mocked(getConfigObj).mockResolvedValueOnce({
navigation: {
pages: ['webhooks/newPet'],
},
} as unknown as object);
vi.mocked(validateDocsConfig).mockResolvedValueOnce({
success: true,
data: {
navigation: {
pages: ['webhooks/newPet'],
},
},
} as unknown as ReturnType<typeof validateDocsConfig>);
const existsSyncSpy = vi.spyOn(fs, 'existsSync');
existsSyncSpy.mockImplementation((p: fs.PathLike) => {
const value = String(p);
return value === '/project/webhooks/newPet.mdx' || value === 'openapi1.json';
});
const readFileSpy = vi.spyOn(fs.promises, 'readFile');
readFileSpy.mockImplementation(async (p: Parameters<typeof fs.promises.readFile>[0]) => {
const value = typeof p === 'string' ? p : (p as unknown as { toString(): string }).toString();
if (value === '/project/webhooks/newPet.mdx') {
return `---\nopenapi: openapi1.json webhook newPet\n---\n\n# Webhook\nBody`;
}
if (value === '/project/openapi1.json') {
return JSON.stringify({
openapi: '3.1.0',
info: { title: 'Test', version: '1.0.0' },
webhooks: {
newPet: {
post: {
summary: 'Webhook - new pet',
requestBody: {},
responses: { '200': { description: 'ok' } },
},
},
},
});
}
throw new Error('Unexpected readFile path: ' + value);
});
vi.spyOn(fs.promises, 'unlink').mockResolvedValueOnce();
await migrateMdx();
expect(addLogSpy).toHaveBeenCalledWith(
expect.objectContaining({ props: { message: 'docs.json updated' } })
);
const specWriteCall = vi
.mocked(outputFile)
.mock.calls.find(
(c) => typeof c[0] === 'string' && (c[0] as string).includes('openapi1.json')
);
expect(specWriteCall).toBeTruthy();
const writtenSpec = JSON.parse(specWriteCall?.[1] as string);
expect(writtenSpec.webhooks.newPet.post['x-mint']).toEqual(
expect.objectContaining({ href: '/webhooks/newPet' })
);
expect(addLogSpy).toHaveBeenCalledWith(
expect.objectContaining({ props: { message: 'migration complete' } })
);
});
});