Skip to content
This repository was archived by the owner on Aug 6, 2025. It is now read-only.

Commit 61df145

Browse files
authored
DOP-3414: Add back button support for OAS Page Builder (#725)
1 parent 9b3e7a0 commit 61df145

File tree

9 files changed

+128
-50
lines changed

9 files changed

+128
-50
lines changed

modules/oas-page-builder/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,26 @@ program
1212
.requiredOption('-b, --bundle <path>', 'path to parsed bundle zip')
1313
.requiredOption('-o, --output <path>', 'path to the directory to output generated files')
1414
.requiredOption('--redoc <path>', 'path to the Redoc CLI program to run. Must be a JS file')
15-
.requiredOption('--repo <path>', 'path to repo being built');
15+
.requiredOption('--repo <path>', 'path to repo being built')
16+
.requiredOption('--site-url <url>, url to landing page of specific docs site');
1617

1718
program.parse();
1819
const options = program.opts<ModuleOptions>();
1920

2021
const app = async (options: ModuleOptions) => {
2122
const { bundle: bundlePath } = options;
22-
const oasMetadata = getOASMetadata(bundlePath);
23-
if (!oasMetadata) {
23+
const metadata = getOASMetadata(bundlePath);
24+
if (!metadata) {
2425
console.log('No OpenAPI content pages found.');
2526
return;
2627
}
2728

28-
const oasMetadataEntries = Object.entries(oasMetadata);
29+
const { siteTitle, openapiPages } = metadata;
30+
const oasMetadataEntries = Object.entries(openapiPages);
2931
const numOASPages = oasMetadataEntries.length;
3032
console.log(`OpenAPI content pages found: ${numOASPages}.`);
3133

32-
await buildOpenAPIPages(oasMetadataEntries, options);
34+
await buildOpenAPIPages(oasMetadataEntries, { ...options, siteTitle });
3335
};
3436

3537
app(options)

modules/oas-page-builder/src/services/buildMetadata.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import AdmZip from 'adm-zip';
22
import { deserialize } from 'bson';
3-
import { OASPagesMetadata } from './types';
3+
import { BuildMetadata, OASPagesMetadata } from './types';
44

5-
export const getOASMetadata = (bundlePath: string): OASPagesMetadata | null => {
5+
export const getOASMetadata = (bundlePath: string): BuildMetadata | null => {
66
const zip = new AdmZip(bundlePath);
77
const zipEntries = zip.getEntries();
88

99
for (const entry of zipEntries) {
1010
if (entry.entryName === 'site.bson') {
1111
const buildMetadata = deserialize(entry.getData());
12-
const oasMetadata: OASPagesMetadata = buildMetadata['openapi_pages'];
13-
if (!!oasMetadata) return oasMetadata;
12+
const siteTitle: string = buildMetadata['title'];
13+
const oasMetadata: OASPagesMetadata | undefined = buildMetadata['openapi_pages'];
14+
if (!!oasMetadata && siteTitle) {
15+
return {
16+
siteTitle,
17+
openapiPages: oasMetadata,
18+
};
19+
}
1420
}
1521
}
1622

modules/oas-page-builder/src/services/commandExecutor.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

modules/oas-page-builder/src/services/pageBuilder.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import fetch from 'node-fetch';
2-
import { ModuleOptions } from '../types';
32
import { normalizePath } from '../utils/normalizePath';
4-
import { execRedoc } from './commandExecutor';
3+
import { RedocExecutor } from './redocExecutor';
54
import { findLastSavedGitHash } from './database';
6-
import { OASPageMetadata } from './types';
5+
import { OASPageMetadata, PageBuilderOptions } from './types';
76

87
const OAS_FILE_SERVER = 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/';
98

@@ -53,8 +52,10 @@ const getAtlasSpecUrl = async (apiKeyword: string) => {
5352

5453
export const buildOpenAPIPages = async (
5554
entries: [string, OASPageMetadata][],
56-
{ output, redoc: redocPath, repo: repoPath }: ModuleOptions
55+
{ output, redoc: redocPath, repo: repoPath, siteUrl, siteTitle }: PageBuilderOptions
5756
) => {
57+
const redocExecutor = new RedocExecutor(redocPath, siteUrl, siteTitle);
58+
5859
for (const [pageSlug, data] of entries) {
5960
const { source_type: sourceType, source } = data;
6061

@@ -73,7 +74,7 @@ export const buildOpenAPIPages = async (
7374
}
7475

7576
const finalFilename = normalizePath(`${output}/${pageSlug}/index.html`);
76-
await execRedoc(spec, finalFilename, redocPath);
77+
await redocExecutor.execute(spec, finalFilename);
7778
} catch (e) {
7879
console.error(e);
7980
// Continue to try to build other pages since it's possible that mut will
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { exec } from 'child_process';
2+
import { promisify } from 'util';
3+
4+
const execCommand = promisify(exec);
5+
6+
export class RedocExecutor {
7+
redocPath: string;
8+
// Stringified options object for Redoc command. Redoc also accepts individual options or a JSON file
9+
private _optionsString: string;
10+
11+
constructor(redocPath: string, siteUrl: string, siteTitle: string) {
12+
this.redocPath = redocPath;
13+
14+
// Custom options DOP defines in the Redoc fork
15+
const customOptions = {
16+
backNavigationPath: siteUrl,
17+
siteTitle,
18+
};
19+
20+
// May contain both native Redoc options and custom DOP options in the future
21+
this._optionsString = JSON.stringify({ customOptions });
22+
}
23+
24+
// Calls Redoc CLI to build spec at given output path
25+
async execute(specSource: string, outputPath: string) {
26+
const outputArg = `--output ${outputPath}`;
27+
const optionsArg = `--options '${this._optionsString}'`;
28+
const command = `node ${this.redocPath} build ${specSource} ${outputArg} ${optionsArg}`;
29+
30+
const { stdout, stderr } = await execCommand(command);
31+
console.log(stdout);
32+
33+
if (stderr) {
34+
console.error(`Error trying to build page ${outputPath} with Redoc.`);
35+
throw stderr;
36+
}
37+
}
38+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
import { ModuleOptions } from '../types';
2+
13
export interface OASPageMetadata {
24
source_type: string;
35
source: string;
46
}
57

68
export type OASPagesMetadata = Record<string, OASPageMetadata>;
9+
10+
export interface BuildMetadata {
11+
siteTitle: string;
12+
openapiPages: OASPagesMetadata;
13+
}
14+
15+
export interface PageBuilderOptions extends Omit<ModuleOptions, 'bundle'> {
16+
siteTitle: string;
17+
}

modules/oas-page-builder/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export interface ModuleOptions {
33
output: string;
44
redoc: string;
55
repo: string;
6+
siteUrl: string;
67
}

modules/oas-page-builder/tests/unit/services/pageBuilder.test.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import fetch from 'node-fetch';
2-
import { execRedoc } from '../../../src/services/commandExecutor';
2+
import { RedocExecutor } from '../../../src/services/redocExecutor';
33
import { findLastSavedGitHash } from '../../../src/services/database';
44
import { buildOpenAPIPages } from '../../../src/services/pageBuilder';
5-
import { OASPageMetadata } from '../../../src/services/types';
5+
import { OASPageMetadata, PageBuilderOptions } from '../../../src/services/types';
66
import { ModuleOptions } from '../../../src/types';
77

88
const MOCKED_GIT_HASH = '1234';
99
const LAST_SAVED_GIT_HASH = '4321';
1010

11+
const mockExecute = jest.fn();
1112
// Mock execRedoc since we only want to ensure pageBuilder properly calls the function
12-
jest.mock('../../../src/services/commandExecutor', () => ({
13-
execRedoc: jest.fn(),
13+
jest.mock('../../../src/services/redocExecutor', () => ({
14+
RedocExecutor: jest.fn().mockImplementation(() => ({
15+
execute: mockExecute,
16+
})),
1417
}));
1518

1619
// Mock database since implementation relies on database instance. Returned values
@@ -33,17 +36,17 @@ const mockFetchImplementation = (ok: boolean) => {
3336
};
3437

3538
describe('pageBuilder', () => {
36-
const testOptions: ModuleOptions = {
37-
bundle: '/path/to/bundle.zip',
39+
const testOptions: PageBuilderOptions = {
3840
output: '/path/to/destination',
3941
redoc: '/path/to/redoc/cli/index.js',
4042
repo: '/path/to/repo',
43+
siteUrl: 'https://mongodb.com/docs',
44+
siteTitle: 'Test Docs',
4145
};
4246

4347
beforeEach(() => {
4448
// Reset mock to reset call count
45-
// @ts-ignore
46-
execRedoc.mockReset();
49+
mockExecute.mockReset();
4750
});
4851

4952
it('builds OpenAPI pages', async () => {
@@ -62,24 +65,23 @@ describe('pageBuilder', () => {
6265
];
6366

6467
await buildOpenAPIPages(testEntries, testOptions);
65-
expect(execRedoc).toBeCalledTimes(testEntries.length);
68+
// const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
69+
// const mockRedocExecutorInstance = RedocExecutor.mock.instances[0];
70+
expect(mockExecute).toBeCalledTimes(testEntries.length);
6671
// Local
67-
expect(execRedoc).toBeCalledWith(
72+
expect(mockExecute).toBeCalledWith(
6873
`${testOptions.repo}/source${testEntries[0][1].source}`,
69-
`${testOptions.output}/${testEntries[0][0]}/index.html`,
70-
testOptions.redoc
74+
`${testOptions.output}/${testEntries[0][0]}/index.html`
7175
);
7276
// Url
73-
expect(execRedoc).toBeCalledWith(
77+
expect(mockExecute).toBeCalledWith(
7478
`${testEntries[1][1].source}`,
75-
getExpectedOutputPath(testOptions.output, testEntries[1][0]),
76-
testOptions.redoc
79+
getExpectedOutputPath(testOptions.output, testEntries[1][0])
7780
);
7881
// Atlas
79-
expect(execRedoc).toBeCalledWith(
82+
expect(mockExecute).toBeCalledWith(
8083
`https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/${MOCKED_GIT_HASH}.json`,
81-
getExpectedOutputPath(testOptions.output, testEntries[2][0]),
82-
testOptions.redoc
84+
getExpectedOutputPath(testOptions.output, testEntries[2][0])
8385
);
8486
});
8587

@@ -91,10 +93,9 @@ describe('pageBuilder', () => {
9193
const testEntries: [string, OASPageMetadata][] = [['path/to/page/1', { source_type: 'atlas', source: 'cloud' }]];
9294

9395
await buildOpenAPIPages(testEntries, testOptions);
94-
expect(execRedoc).toBeCalledWith(
96+
expect(mockExecute).toBeCalledWith(
9597
`https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/${LAST_SAVED_GIT_HASH}.json`,
96-
getExpectedOutputPath(testOptions.output, testEntries[0][0]),
97-
testOptions.redoc
98+
getExpectedOutputPath(testOptions.output, testEntries[0][0])
9899
);
99100
});
100101

@@ -106,6 +107,6 @@ describe('pageBuilder', () => {
106107
const testEntries: [string, OASPageMetadata][] = [['path/to/page/1', { source_type: 'atlas', source: 'cloud' }]];
107108

108109
await buildOpenAPIPages(testEntries, testOptions);
109-
expect(execRedoc).toBeCalledTimes(0);
110+
expect(mockExecute).toBeCalledTimes(0);
110111
});
111112
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { RedocExecutor } from '../../../src/services/redocExecutor';
2+
import cp from 'child_process';
3+
4+
jest.mock('child_process');
5+
// @ts-ignore
6+
cp.exec.mockImplementation((command: string, callback: any) => {
7+
callback(null, command);
8+
});
9+
10+
describe('RedocExecutor', () => {
11+
it('calls the Redoc build command with expected parameters', async () => {
12+
const testRedocPath = '/path/to/redoc/cli/index.js';
13+
const testSiteUrl = 'https://mongodb.com/docs';
14+
const testSiteTitle = 'Test Docs';
15+
const redocExecutor = new RedocExecutor(testRedocPath, testSiteUrl, testSiteTitle);
16+
17+
const testSpecSource = '/path/to/spec.json';
18+
const testOutputPath = '/path/to/output/index.html';
19+
await redocExecutor.execute(testSpecSource, testOutputPath);
20+
21+
const expectedOptions = {
22+
customOptions: {
23+
backNavigationPath: testSiteUrl,
24+
siteTitle: testSiteTitle,
25+
},
26+
};
27+
const expectedCommand = `node ${testRedocPath} build ${testSpecSource} --output ${testOutputPath} --options '${JSON.stringify(
28+
expectedOptions
29+
)}'`;
30+
expect(cp.exec).toBeCalledWith(expectedCommand, expect.anything());
31+
});
32+
});

0 commit comments

Comments
 (0)