chore: aggregate data regarding API types and versions usage (#1580)

This commit is contained in:
Andrew Tatomyr
2024-08-01 12:07:03 +03:00
committed by GitHub
parent 6b2b79bd7d
commit 2c776bad87
27 changed files with 499 additions and 336 deletions

View File

@@ -16,6 +16,8 @@ When a command is run, the following data is collected:
- values from `REDOCLY_ENVIRONMENT` - values from `REDOCLY_ENVIRONMENT`
- CLI version - CLI version
- Node.js and NPM versions - Node.js and NPM versions
- whether the `redocly.yaml` configuration file exists
- API specification version
Values such as file names, organization IDs, and URLs are removed, replaced by just "URL" or "file", etc. Values such as file names, organization IDs, and URLs are removed, replaced by just "URL" or "file", etc.

View File

@@ -39,8 +39,8 @@ describe('build-docs', () => {
it('should work correctly when calling handlerBuildCommand', async () => { it('should work correctly when calling handlerBuildCommand', async () => {
const processExitMock = jest.spyOn(process, 'exit').mockImplementation(); const processExitMock = jest.spyOn(process, 'exit').mockImplementation();
await handlerBuildCommand( await handlerBuildCommand({
{ argv: {
o: '', o: '',
title: 'test', title: 'test',
disableGoogleFont: false, disableGoogleFont: false,
@@ -49,8 +49,9 @@ describe('build-docs', () => {
theme: { openapi: {} }, theme: { openapi: {} },
api: '../some-path/openapi.yaml', api: '../some-path/openapi.yaml',
} as BuildDocsArgv, } as BuildDocsArgv,
{} as any config: {} as any,
); version: 'cli-version',
});
expect(loadAndBundleSpec).toBeCalledTimes(1); expect(loadAndBundleSpec).toBeCalledTimes(1);
expect(getFallbackApisOrExit).toBeCalledTimes(1); expect(getFallbackApisOrExit).toBeCalledTimes(1);
expect(processExitMock).toBeCalledTimes(0); expect(processExitMock).toBeCalledTimes(0);

View File

@@ -14,21 +14,21 @@ describe('handleJoin', () => {
colloreteYellowMock.mockImplementation((string: string) => string); colloreteYellowMock.mockImplementation((string: string) => string);
it('should call exitWithError because only one entrypoint', async () => { it('should call exitWithError because only one entrypoint', async () => {
await handleJoin({ apis: ['first.yaml'] }, {} as any, 'cli-version'); await handleJoin({ argv: { apis: ['first.yaml'] }, config: {} as any, version: 'cli-version' });
expect(exitWithError).toHaveBeenCalledWith(`At least 2 apis should be provided.`); expect(exitWithError).toHaveBeenCalledWith(`At least 2 apis should be provided.`);
}); });
it('should call exitWithError because passed all 3 options for tags', async () => { it('should call exitWithError because passed all 3 options for tags', async () => {
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.yaml', 'second.yaml'], apis: ['first.yaml', 'second.yaml'],
'prefix-tags-with-info-prop': 'something', 'prefix-tags-with-info-prop': 'something',
'without-x-tag-groups': true, 'without-x-tag-groups': true,
'prefix-tags-with-filename': true, 'prefix-tags-with-filename': true,
}, },
{} as any, config: {} as any,
'cli-version' version: 'cli-version',
); });
expect(exitWithError).toHaveBeenCalledWith( expect(exitWithError).toHaveBeenCalledWith(
`You use prefix-tags-with-filename, prefix-tags-with-info-prop, without-x-tag-groups together.\nPlease choose only one!` `You use prefix-tags-with-filename, prefix-tags-with-info-prop, without-x-tag-groups together.\nPlease choose only one!`
@@ -36,15 +36,15 @@ describe('handleJoin', () => {
}); });
it('should call exitWithError because passed all 2 options for tags', async () => { it('should call exitWithError because passed all 2 options for tags', async () => {
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.yaml', 'second.yaml'], apis: ['first.yaml', 'second.yaml'],
'without-x-tag-groups': true, 'without-x-tag-groups': true,
'prefix-tags-with-filename': true, 'prefix-tags-with-filename': true,
}, },
{} as any, config: {} as any,
'cli-version' version: 'cli-version',
); });
expect(exitWithError).toHaveBeenCalledWith( expect(exitWithError).toHaveBeenCalledWith(
`You use prefix-tags-with-filename, without-x-tag-groups together.\nPlease choose only one!` `You use prefix-tags-with-filename, without-x-tag-groups together.\nPlease choose only one!`
@@ -52,13 +52,13 @@ describe('handleJoin', () => {
}); });
it('should call exitWithError because Only OpenAPI 3.0 and OpenAPI 3.1 are supported', async () => { it('should call exitWithError because Only OpenAPI 3.0 and OpenAPI 3.1 are supported', async () => {
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.yaml', 'second.yaml'], apis: ['first.yaml', 'second.yaml'],
}, },
ConfigFixture as any, config: ConfigFixture as any,
'cli-version' version: 'cli-version',
); });
expect(exitWithError).toHaveBeenCalledWith( expect(exitWithError).toHaveBeenCalledWith(
'Only OpenAPI 3.0 and OpenAPI 3.1 are supported: undefined.' 'Only OpenAPI 3.0 and OpenAPI 3.1 are supported: undefined.'
); );
@@ -68,13 +68,13 @@ describe('handleJoin', () => {
(detectSpec as jest.Mock) (detectSpec as jest.Mock)
.mockImplementationOnce(() => 'oas3_0') .mockImplementationOnce(() => 'oas3_0')
.mockImplementationOnce(() => 'oas3_1'); .mockImplementationOnce(() => 'oas3_1');
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.yaml', 'second.yaml'], apis: ['first.yaml', 'second.yaml'],
}, },
ConfigFixture as any, config: ConfigFixture as any,
'cli-version' version: 'cli-version',
); });
expect(exitWithError).toHaveBeenCalledWith( expect(exitWithError).toHaveBeenCalledWith(
'All APIs must use the same OpenAPI version: undefined.' 'All APIs must use the same OpenAPI version: undefined.'
@@ -83,13 +83,13 @@ describe('handleJoin', () => {
it('should call writeToFileByExtension function', async () => { it('should call writeToFileByExtension function', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0'); (detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.yaml', 'second.yaml'], apis: ['first.yaml', 'second.yaml'],
}, },
ConfigFixture as any, config: ConfigFixture as any,
'cli-version' version: 'cli-version',
); });
expect(writeToFileByExtension).toHaveBeenCalledWith( expect(writeToFileByExtension).toHaveBeenCalledWith(
expect.any(Object), expect.any(Object),
@@ -100,13 +100,13 @@ describe('handleJoin', () => {
it('should call writeToFileByExtension function for OpenAPI 3.1', async () => { it('should call writeToFileByExtension function for OpenAPI 3.1', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_1'); (detectSpec as jest.Mock).mockReturnValue('oas3_1');
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.yaml', 'second.yaml'], apis: ['first.yaml', 'second.yaml'],
}, },
ConfigFixture as any, config: ConfigFixture as any,
'cli-version' version: 'cli-version',
); });
expect(writeToFileByExtension).toHaveBeenCalledWith( expect(writeToFileByExtension).toHaveBeenCalledWith(
expect.any(Object), expect.any(Object),
@@ -117,14 +117,14 @@ describe('handleJoin', () => {
it('should call writeToFileByExtension function with custom output file', async () => { it('should call writeToFileByExtension function with custom output file', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0'); (detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.yaml', 'second.yaml'], apis: ['first.yaml', 'second.yaml'],
output: 'output.yml', output: 'output.yml',
}, },
ConfigFixture as any, config: ConfigFixture as any,
'cli-version' version: 'cli-version',
); });
expect(writeToFileByExtension).toHaveBeenCalledWith( expect(writeToFileByExtension).toHaveBeenCalledWith(
expect.any(Object), expect.any(Object),
@@ -135,13 +135,13 @@ describe('handleJoin', () => {
it('should call writeToFileByExtension function with json file extension', async () => { it('should call writeToFileByExtension function with json file extension', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0'); (detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.json', 'second.yaml'], apis: ['first.json', 'second.yaml'],
}, },
ConfigFixture as any, config: ConfigFixture as any,
'cli-version' version: 'cli-version',
); });
expect(writeToFileByExtension).toHaveBeenCalledWith( expect(writeToFileByExtension).toHaveBeenCalledWith(
expect.any(Object), expect.any(Object),
@@ -152,13 +152,13 @@ describe('handleJoin', () => {
it('should call skipDecorators and skipPreprocessors', async () => { it('should call skipDecorators and skipPreprocessors', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0'); (detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.yaml', 'second.yaml'], apis: ['first.yaml', 'second.yaml'],
}, },
ConfigFixture as any, config: ConfigFixture as any,
'cli-version' version: 'cli-version',
); });
const config = loadConfig(); const config = loadConfig();
expect(config.styleguide.skipDecorators).toHaveBeenCalled(); expect(config.styleguide.skipDecorators).toHaveBeenCalled();
@@ -168,15 +168,15 @@ describe('handleJoin', () => {
it('should handle join with prefix-components-with-info-prop and null values', async () => { it('should handle join with prefix-components-with-info-prop and null values', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0'); (detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin( await handleJoin({
{ argv: {
apis: ['first.yaml', 'second.yaml', 'third.yaml'], apis: ['first.yaml', 'second.yaml', 'third.yaml'],
'prefix-components-with-info-prop': 'title', 'prefix-components-with-info-prop': 'title',
output: 'join-result.yaml', output: 'join-result.yaml',
}, },
ConfigFixture as any, config: ConfigFixture as any,
'cli-version' version: 'cli-version',
); });
expect(writeToFileByExtension).toHaveBeenCalledWith( expect(writeToFileByExtension).toHaveBeenCalledWith(
{ {

View File

@@ -28,30 +28,32 @@ describe('push-with-region', () => {
it('should call login with default domain when region is US', async () => { it('should call login with default domain when region is US', async () => {
redoclyClient.domain = 'redoc.ly'; redoclyClient.domain = 'redoc.ly';
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
api: 'spec.json', api: 'spec.json',
destination: '@org/my-api@1.0.0', destination: '@org/my-api@1.0.0',
branchName: 'test', branchName: 'test',
}, },
ConfigFixture as any config: ConfigFixture as any,
); version: 'cli-version',
});
expect(mockPromptClientToken).toBeCalledTimes(1); expect(mockPromptClientToken).toBeCalledTimes(1);
expect(mockPromptClientToken).toHaveBeenCalledWith(redoclyClient.domain); expect(mockPromptClientToken).toHaveBeenCalledWith(redoclyClient.domain);
}); });
it('should call login with EU domain when region is EU', async () => { it('should call login with EU domain when region is EU', async () => {
redoclyClient.domain = 'eu.redocly.com'; redoclyClient.domain = 'eu.redocly.com';
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
api: 'spec.json', api: 'spec.json',
destination: '@org/my-api@1.0.0', destination: '@org/my-api@1.0.0',
branchName: 'test', branchName: 'test',
}, },
ConfigFixture as any config: ConfigFixture as any,
); version: 'cli-version',
});
expect(mockPromptClientToken).toBeCalledTimes(1); expect(mockPromptClientToken).toBeCalledTimes(1);
expect(mockPromptClientToken).toHaveBeenCalledWith(redoclyClient.domain); expect(mockPromptClientToken).toHaveBeenCalledWith(redoclyClient.domain);
}); });

View File

@@ -25,8 +25,8 @@ describe('push', () => {
}); });
it('pushes definition', async () => { it('pushes definition', async () => {
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
api: 'spec.json', api: 'spec.json',
destination: '@org/my-api@1.0.0', destination: '@org/my-api@1.0.0',
@@ -35,8 +35,9 @@ describe('push', () => {
'job-id': '123', 'job-id': '123',
'batch-size': 2, 'batch-size': 2,
}, },
ConfigFixture as any config: ConfigFixture as any,
); version: 'cli-version',
});
expect(redoclyClient.registryApi.prepareFileUpload).toBeCalledTimes(1); expect(redoclyClient.registryApi.prepareFileUpload).toBeCalledTimes(1);
expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1); expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
@@ -55,8 +56,8 @@ describe('push', () => {
}); });
it('fails if jobId value is an empty string', async () => { it('fails if jobId value is an empty string', async () => {
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
api: 'spec.json', api: 'spec.json',
destination: '@org/my-api@1.0.0', destination: '@org/my-api@1.0.0',
@@ -65,15 +66,16 @@ describe('push', () => {
'job-id': ' ', 'job-id': ' ',
'batch-size': 2, 'batch-size': 2,
}, },
ConfigFixture as any config: ConfigFixture as any,
); version: 'cli-version',
});
expect(exitWithError).toBeCalledTimes(1); expect(exitWithError).toBeCalledTimes(1);
}); });
it('fails if batchSize value is less than 2', async () => { it('fails if batchSize value is less than 2', async () => {
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
api: 'spec.json', api: 'spec.json',
destination: '@org/my-api@1.0.0', destination: '@org/my-api@1.0.0',
@@ -82,8 +84,9 @@ describe('push', () => {
'job-id': '123', 'job-id': '123',
'batch-size': 1, 'batch-size': 1,
}, },
ConfigFixture as any config: ConfigFixture as any,
); version: 'cli-version',
});
expect(exitWithError).toBeCalledTimes(1); expect(exitWithError).toBeCalledTimes(1);
}); });
@@ -95,16 +98,17 @@ describe('push', () => {
return { isDirectory: () => false, size: 10 }; return { isDirectory: () => false, size: 10 };
}); });
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
api: 'spec.json', api: 'spec.json',
destination: '@org/my-api@1.0.0', destination: '@org/my-api@1.0.0',
public: true, public: true,
files: ['./resouces/1.md', './resouces/2.md'], files: ['./resouces/1.md', './resouces/2.md'],
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({ expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({
filePaths: ['filePath', 'filePath', 'filePath'], filePaths: ['filePath', 'filePath', 'filePath'],
@@ -119,8 +123,8 @@ describe('push', () => {
}); });
it('push should fail if organization not provided', async () => { it('push should fail if organization not provided', async () => {
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
api: 'spec.json', api: 'spec.json',
destination: 'test@v1', destination: 'test@v1',
@@ -129,8 +133,9 @@ describe('push', () => {
'job-id': '123', 'job-id': '123',
'batch-size': 2, 'batch-size': 2,
}, },
ConfigFixture as any config: ConfigFixture as any,
); version: 'cli-version',
});
expect(exitWithError).toBeCalledTimes(1); expect(exitWithError).toBeCalledTimes(1);
expect(exitWithError).toBeCalledWith( expect(exitWithError).toBeCalledWith(
@@ -140,8 +145,8 @@ describe('push', () => {
it('push should work with organization in config', async () => { it('push should work with organization in config', async () => {
const mockConfig = { ...ConfigFixture, organization: 'test_org' } as any; const mockConfig = { ...ConfigFixture, organization: 'test_org' } as any;
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
api: 'spec.json', api: 'spec.json',
destination: 'my-api@1.0.0', destination: 'my-api@1.0.0',
@@ -150,8 +155,9 @@ describe('push', () => {
'job-id': '123', 'job-id': '123',
'batch-size': 2, 'batch-size': 2,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1); expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({ expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({
@@ -175,16 +181,17 @@ describe('push', () => {
apis: { 'my-api@1.0.0': { root: 'path' } }, apis: { 'my-api@1.0.0': { root: 'path' } },
} as any; } as any;
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
branchName: 'test', branchName: 'test',
public: true, public: true,
'job-id': '123', 'job-id': '123',
'batch-size': 2, 'batch-size': 2,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1); expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
}); });
@@ -192,16 +199,17 @@ describe('push', () => {
it('push should fail if apis not provided', async () => { it('push should fail if apis not provided', async () => {
const mockConfig = { organization: 'test_org', apis: {} } as any; const mockConfig = { organization: 'test_org', apis: {} } as any;
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
branchName: 'test', branchName: 'test',
public: true, public: true,
'job-id': '123', 'job-id': '123',
'batch-size': 2, 'batch-size': 2,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(exitWithError).toBeCalledTimes(1); expect(exitWithError).toBeCalledTimes(1);
expect(exitWithError).toHaveBeenLastCalledWith( expect(exitWithError).toHaveBeenLastCalledWith(
@@ -212,8 +220,8 @@ describe('push', () => {
it('push should fail if destination not provided', async () => { it('push should fail if destination not provided', async () => {
const mockConfig = { organization: 'test_org', apis: {} } as any; const mockConfig = { organization: 'test_org', apis: {} } as any;
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
api: 'api.yaml', api: 'api.yaml',
branchName: 'test', branchName: 'test',
@@ -221,8 +229,9 @@ describe('push', () => {
'job-id': '123', 'job-id': '123',
'batch-size': 2, 'batch-size': 2,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(exitWithError).toBeCalledTimes(1); expect(exitWithError).toBeCalledTimes(1);
expect(exitWithError).toHaveBeenLastCalledWith( expect(exitWithError).toHaveBeenLastCalledWith(
@@ -233,8 +242,8 @@ describe('push', () => {
it('push should fail if destination format is not valid', async () => { it('push should fail if destination format is not valid', async () => {
const mockConfig = { organization: 'test_org', apis: {} } as any; const mockConfig = { organization: 'test_org', apis: {} } as any;
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
destination: 'name/v1', destination: 'name/v1',
branchName: 'test', branchName: 'test',
@@ -242,8 +251,9 @@ describe('push', () => {
'job-id': '123', 'job-id': '123',
'batch-size': 2, 'batch-size': 2,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(exitWithError).toHaveBeenCalledWith( expect(exitWithError).toHaveBeenCalledWith(
`Destination argument value is not valid, please use the right format: ${yellow( `Destination argument value is not valid, please use the right format: ${yellow(
@@ -261,8 +271,8 @@ describe('push', () => {
apis: { 'my test api@v1': { root: 'path' } }, apis: { 'my test api@v1': { root: 'path' } },
} as any; } as any;
await handlePush( await handlePush({
{ argv: {
upsert: true, upsert: true,
destination: 'my test api@v1', destination: 'my test api@v1',
branchName: 'test', branchName: 'test',
@@ -270,8 +280,9 @@ describe('push', () => {
'job-id': '123', 'job-id': '123',
'batch-size': 2, 'batch-size': 2,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(encodeURIComponentSpy).toHaveReturnedWith('my%20test%20api'); expect(encodeURIComponentSpy).toHaveReturnedWith('my%20test%20api');
expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1); expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
@@ -281,130 +292,144 @@ describe('push', () => {
describe('transformPush', () => { describe('transformPush', () => {
it('should adapt the existing syntax', () => { it('should adapt the existing syntax', () => {
const cb = jest.fn(); const cb = jest.fn();
transformPush(cb)( transformPush(cb)({
{ argv: {
apis: ['openapi.yaml', '@testing_org/main@v1'], apis: ['openapi.yaml', '@testing_org/main@v1'],
}, },
{} as any config: {} as any,
); version: 'cli-version',
expect(cb).toBeCalledWith( });
{ expect(cb).toBeCalledWith({
argv: {
api: 'openapi.yaml', api: 'openapi.yaml',
destination: '@testing_org/main@v1', destination: '@testing_org/main@v1',
}, },
{} config: {},
); version: 'cli-version',
});
}); });
it('should adapt the existing syntax (including branchName)', () => { it('should adapt the existing syntax (including branchName)', () => {
const cb = jest.fn(); const cb = jest.fn();
transformPush(cb)( transformPush(cb)({
{ argv: {
apis: ['openapi.yaml', '@testing_org/main@v1', 'other'], apis: ['openapi.yaml', '@testing_org/main@v1', 'other'],
}, },
{} as any config: {} as any,
); version: 'cli-version',
expect(cb).toBeCalledWith( });
{ expect(cb).toBeCalledWith({
argv: {
api: 'openapi.yaml', api: 'openapi.yaml',
destination: '@testing_org/main@v1', destination: '@testing_org/main@v1',
branchName: 'other', branchName: 'other',
}, },
{} config: {},
); version: 'cli-version',
});
}); });
it('should use --branch option firstly', () => { it('should use --branch option firstly', () => {
const cb = jest.fn(); const cb = jest.fn();
transformPush(cb)( transformPush(cb)({
{ argv: {
apis: ['openapi.yaml', '@testing_org/main@v1', 'other'], apis: ['openapi.yaml', '@testing_org/main@v1', 'other'],
branch: 'priority-branch', branch: 'priority-branch',
}, },
{} as any config: {} as any,
); version: 'cli-version',
expect(cb).toBeCalledWith( });
{ expect(cb).toBeCalledWith({
argv: {
api: 'openapi.yaml', api: 'openapi.yaml',
destination: '@testing_org/main@v1', destination: '@testing_org/main@v1',
branchName: 'priority-branch', branchName: 'priority-branch',
}, },
{} config: {},
); version: 'cli-version',
});
}); });
it('should work for a destination only', () => { it('should work for a destination only', () => {
const cb = jest.fn(); const cb = jest.fn();
transformPush(cb)( transformPush(cb)({
{ argv: {
apis: ['main@v1'], apis: ['main@v1'],
}, },
{} as any config: {} as any,
); version: 'cli-version',
expect(cb).toBeCalledWith( });
{ expect(cb).toBeCalledWith({
argv: {
destination: 'main@v1', destination: 'main@v1',
}, },
{} config: {},
); version: 'cli-version',
});
}); });
it('should work for a api only', () => { it('should work for a api only', () => {
const cb = jest.fn(); const cb = jest.fn();
transformPush(cb)( transformPush(cb)({
{ argv: {
apis: ['test.yaml'], apis: ['test.yaml'],
}, },
{} as any config: {} as any,
); version: 'cli-version',
expect(cb).toBeCalledWith( });
{ expect(cb).toBeCalledWith({
argv: {
api: 'test.yaml', api: 'test.yaml',
}, },
{} config: {},
); version: 'cli-version',
});
}); });
it('should use destination from option', () => { it('should use destination from option', () => {
const cb = jest.fn(); const cb = jest.fn();
transformPush(cb)( transformPush(cb)({
{ argv: {
apis: ['test.yaml', 'test@v1'], apis: ['test.yaml', 'test@v1'],
destination: 'main@v1', destination: 'main@v1',
}, },
{} as any config: {} as any,
); version: 'cli-version',
expect(cb).toBeCalledWith( });
{ expect(cb).toBeCalledWith({
argv: {
destination: 'main@v1', destination: 'main@v1',
api: 'test.yaml', api: 'test.yaml',
}, },
{} config: {},
); version: 'cli-version',
});
}); });
it('should use --job-id option firstly', () => { it('should use --job-id option firstly', () => {
const cb = jest.fn(); const cb = jest.fn();
transformPush(cb)( transformPush(cb)({
{ argv: {
'batch-id': 'b-123', 'batch-id': 'b-123',
'job-id': 'j-123', 'job-id': 'j-123',
apis: ['test'], apis: ['test'],
branch: 'test', branch: 'test',
destination: 'main@v1', destination: 'main@v1',
}, },
{} as any config: {} as any,
); version: 'cli-version',
expect(cb).toBeCalledWith( });
{ expect(cb).toBeCalledWith({
argv: {
'job-id': 'j-123', 'job-id': 'j-123',
api: 'test', api: 'test',
branchName: 'test', branchName: 'test',
destination: 'main@v1', destination: 'main@v1',
}, },
{} config: {},
); version: 'cli-version',
});
}); });
it('should accept no arguments at all', () => { it('should accept no arguments at all', () => {
const cb = jest.fn(); const cb = jest.fn();
transformPush(cb)({}, {} as any); transformPush(cb)({ argv: {}, config: {} as any, version: 'cli-version' });
expect(cb).toBeCalledWith({}, {}); expect(cb).toBeCalledWith({ argv: {}, config: {}, version: 'cli-version' });
}); });
}); });

View File

@@ -4,6 +4,7 @@ import { commandWrapper } from '../wrapper';
import { handleLint } from '../commands/lint'; import { handleLint } from '../commands/lint';
import { Arguments } from 'yargs'; import { Arguments } from 'yargs';
import { handlePush, PushOptions } from '../commands/push'; import { handlePush, PushOptions } from '../commands/push';
import { detectSpec } from '@redocly/openapi-core';
jest.mock('node-fetch'); jest.mock('node-fetch');
jest.mock('../utils/miscellaneous', () => ({ jest.mock('../utils/miscellaneous', () => ({
@@ -11,7 +12,9 @@ jest.mock('../utils/miscellaneous', () => ({
loadConfigAndHandleErrors: jest.fn(), loadConfigAndHandleErrors: jest.fn(),
})); }));
jest.mock('../commands/lint', () => ({ jest.mock('../commands/lint', () => ({
handleLint: jest.fn(), handleLint: jest.fn().mockImplementation(({ collectSpecData }) => {
collectSpecData({ openapi: '3.1.0' });
}),
lintConfigCallback: jest.fn(), lintConfigCallback: jest.fn(),
})); }));
@@ -20,13 +23,32 @@ describe('commandWrapper', () => {
(loadConfigAndHandleErrors as jest.Mock).mockImplementation(() => { (loadConfigAndHandleErrors as jest.Mock).mockImplementation(() => {
return { telemetry: 'on', styleguide: { recommendedFallback: true } }; return { telemetry: 'on', styleguide: { recommendedFallback: true } };
}); });
(detectSpec as jest.Mock).mockImplementationOnce(() => {
return 'oas3_1';
});
process.env.REDOCLY_TELEMETRY = 'on'; process.env.REDOCLY_TELEMETRY = 'on';
const wrappedHandler = commandWrapper(handleLint); const wrappedHandler = commandWrapper(handleLint);
await wrappedHandler({} as any); await wrappedHandler({} as any);
expect(handleLint).toHaveBeenCalledTimes(1); expect(handleLint).toHaveBeenCalledTimes(1);
expect(sendTelemetry).toHaveBeenCalledTimes(1); expect(sendTelemetry).toHaveBeenCalledTimes(1);
expect(sendTelemetry).toHaveBeenCalledWith({}, 0, false); expect(sendTelemetry).toHaveBeenCalledWith({}, 0, false, 'oas3_1', 'openapi', '3.1.0');
});
it('should not collect spec version if the file is not parsed to json', async () => {
(loadConfigAndHandleErrors as jest.Mock).mockImplementation(() => {
return { telemetry: 'on', styleguide: { recommendedFallback: true } };
});
(handleLint as jest.Mock).mockImplementation(({ collectSpecData }) => {
collectSpecData();
});
process.env.REDOCLY_TELEMETRY = 'on';
const wrappedHandler = commandWrapper(handleLint);
await wrappedHandler({} as any);
expect(handleLint).toHaveBeenCalledTimes(1);
expect(sendTelemetry).toHaveBeenCalledTimes(1);
expect(sendTelemetry).toHaveBeenCalledWith({}, 0, false, undefined, undefined, undefined);
}); });
it('should NOT send telemetry if there is "telemetry: off" in the config', async () => { it('should NOT send telemetry if there is "telemetry: off" in the config', async () => {

View File

@@ -85,15 +85,16 @@ describe('handlePushStatus()', () => {
it('should throw error if organization not provided', async () => { it('should throw error if organization not provided', async () => {
await expect( await expect(
handlePushStatus( handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: '', organization: '',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
}, },
mockConfig config: mockConfig,
) version: 'cli-version',
})
).rejects.toThrowErrorMatchingInlineSnapshot( ).rejects.toThrowErrorMatchingInlineSnapshot(
`"No organization provided, please use --organization option or specify the 'organization' field in the config file."` `"No organization provided, please use --organization option or specify the 'organization' field in the config file."`
); );
@@ -108,15 +109,16 @@ describe('handlePushStatus()', () => {
process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
remotes.getPush.mockResolvedValueOnce(pushResponseStub); remotes.getPush.mockResolvedValueOnce(pushResponseStub);
await handlePushStatus( await handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(process.stdout.write).toHaveBeenCalledTimes(1); expect(process.stdout.write).toHaveBeenCalledTimes(1);
expect(process.stdout.write).toHaveBeenCalledWith( expect(process.stdout.write).toHaveBeenCalledWith(
'🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n' '🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n'
@@ -127,15 +129,16 @@ describe('handlePushStatus()', () => {
process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true }); remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true });
await handlePushStatus( await handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(process.stdout.write).toHaveBeenCalledTimes(2); expect(process.stdout.write).toHaveBeenCalledTimes(2);
expect(process.stdout.write).toHaveBeenCalledWith( expect(process.stdout.write).toHaveBeenCalledWith(
'🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n' '🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n'
@@ -157,15 +160,16 @@ describe('handlePushStatus()', () => {
}); });
await expect( await expect(
handlePushStatus( handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
}, },
mockConfig config: mockConfig,
) version: 'cli-version',
})
).rejects.toThrowErrorMatchingInlineSnapshot(` ).rejects.toThrowErrorMatchingInlineSnapshot(`
"❌ Preview deploy fail. "❌ Preview deploy fail.
Preview URL: https://preview-test-url" Preview URL: https://preview-test-url"
@@ -197,15 +201,16 @@ describe('handlePushStatus()', () => {
}, },
}); });
await handlePushStatus( await handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(process.stdout.write).toHaveBeenCalledTimes(4); expect(process.stdout.write).toHaveBeenCalledTimes(4);
expect(process.stdout.write).toHaveBeenCalledWith( expect(process.stdout.write).toHaveBeenCalledWith(
'🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n' '🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n'
@@ -232,16 +237,17 @@ describe('handlePushStatus()', () => {
}, },
}); });
await handlePushStatus( await handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
wait: true, wait: true,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(process.stderr.write).toHaveBeenCalledWith( expect(process.stderr.write).toHaveBeenCalledWith(
'Files not added to your project. Reason: no changes.\n' 'Files not added to your project. Reason: no changes.\n'
@@ -253,15 +259,16 @@ describe('handlePushStatus()', () => {
process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: false }); remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: false });
const result = await handlePushStatus( const result = await handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(result).toEqual({ expect(result).toEqual({
preview: { preview: {
@@ -280,15 +287,16 @@ describe('handlePushStatus()', () => {
process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true }); remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true });
const result = await handlePushStatus( const result = await handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(result).toEqual({ expect(result).toEqual({
preview: { preview: {
@@ -344,8 +352,8 @@ describe('handlePushStatus()', () => {
}, },
}); });
const result = await handlePushStatus( const result = await handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
@@ -353,8 +361,9 @@ describe('handlePushStatus()', () => {
'retry-interval': 0.5, // 500 ms 'retry-interval': 0.5, // 500 ms
wait: true, wait: true,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(result).toEqual({ expect(result).toEqual({
preview: { preview: {
@@ -417,8 +426,8 @@ describe('handlePushStatus()', () => {
}, },
}); });
const result = await handlePushStatus( const result = await handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
@@ -426,8 +435,9 @@ describe('handlePushStatus()', () => {
'retry-interval': 0.5, // 500 ms 'retry-interval': 0.5, // 500 ms
wait: true, wait: true,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(result).toEqual({ expect(result).toEqual({
preview: { preview: {
@@ -458,16 +468,17 @@ describe('handlePushStatus()', () => {
}); });
await expect( await expect(
handlePushStatus( handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
'continue-on-deploy-failures': false, 'continue-on-deploy-failures': false,
}, },
mockConfig config: mockConfig,
) version: 'cli-version',
})
).rejects.toThrowErrorMatchingInlineSnapshot(` ).rejects.toThrowErrorMatchingInlineSnapshot(`
"❌ Preview deploy fail. "❌ Preview deploy fail.
Preview URL: https://preview-test-url" Preview URL: https://preview-test-url"
@@ -488,16 +499,17 @@ describe('handlePushStatus()', () => {
}); });
await expect( await expect(
handlePushStatus( handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
pushId: 'test-push-id', pushId: 'test-push-id',
'continue-on-deploy-failures': true, 'continue-on-deploy-failures': true,
}, },
mockConfig config: mockConfig,
) version: 'cli-version',
})
).resolves.toStrictEqual({ ).resolves.toStrictEqual({
preview: { preview: {
deploy: { status: 'failed', url: 'https://preview-test-url' }, deploy: { status: 'failed', url: 'https://preview-test-url' },
@@ -545,8 +557,8 @@ describe('handlePushStatus()', () => {
const onRetrySpy = jest.fn(); const onRetrySpy = jest.fn();
const result = await handlePushStatus( const result = await handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
@@ -555,8 +567,9 @@ describe('handlePushStatus()', () => {
'retry-interval': 0.5, // 500 ms 'retry-interval': 0.5, // 500 ms
onRetry: onRetrySpy, onRetry: onRetrySpy,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(onRetrySpy).toBeCalledTimes(2); expect(onRetrySpy).toBeCalledTimes(2);
@@ -617,8 +630,8 @@ describe('handlePushStatus()', () => {
}); });
await expect( await expect(
handlePushStatus( handlePushStatus({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
organization: 'test-org', organization: 'test-org',
project: 'test-project', project: 'test-project',
@@ -627,8 +640,9 @@ describe('handlePushStatus()', () => {
'max-execution-time': 1, // seconds 'max-execution-time': 1, // seconds
wait: true, wait: true,
}, },
mockConfig config: mockConfig,
) version: 'cli-version',
})
).rejects.toThrowErrorMatchingInlineSnapshot(` ).rejects.toThrowErrorMatchingInlineSnapshot(`
"✗ Failed to get push status. Reason: Timeout exceeded "✗ Failed to get push status. Reason: Timeout exceeded
" "

View File

@@ -63,8 +63,8 @@ describe('handlePush()', () => {
pathRelativeSpy.mockImplementationOnce((_, p) => p); pathRelativeSpy.mockImplementationOnce((_, p) => p);
pathDirnameSpy.mockImplementation((_: string) => '.'); pathDirnameSpy.mockImplementation((_: string) => '.');
await handlePush( await handlePush({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
'mount-path': 'test-mount-path', 'mount-path': 'test-mount-path',
organization: 'test-org', organization: 'test-org',
@@ -81,8 +81,9 @@ describe('handlePush()', () => {
files: ['test-file'], files: ['test-file'],
'max-execution-time': 10, 'max-execution-time': 10,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(remotes.getDefaultBranch).toHaveBeenCalledWith('test-org', 'test-project'); expect(remotes.getDefaultBranch).toHaveBeenCalledWith('test-org', 'test-project');
expect(remotes.upsert).toHaveBeenCalledWith('test-org', 'test-project', { expect(remotes.upsert).toHaveBeenCalledWith('test-org', 'test-project', {
@@ -132,8 +133,8 @@ describe('handlePush()', () => {
pathRelativeSpy.mockImplementationOnce((_, p) => p); pathRelativeSpy.mockImplementationOnce((_, p) => p);
pathDirnameSpy.mockImplementation((_: string) => '.'); pathDirnameSpy.mockImplementation((_: string) => '.');
const result = await handlePush( const result = await handlePush({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
'mount-path': 'test-mount-path', 'mount-path': 'test-mount-path',
organization: 'test-org', organization: 'test-org',
@@ -150,8 +151,9 @@ describe('handlePush()', () => {
files: ['test-file'], files: ['test-file'],
'max-execution-time': 10, 'max-execution-time': 10,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(result).toEqual({ pushId: 'test-id' }); expect(result).toEqual({ pushId: 'test-id' });
}); });
@@ -188,8 +190,8 @@ describe('handlePush()', () => {
throw new Error('Not a directory'); throw new Error('Not a directory');
}); });
await handlePush( await handlePush({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
'mount-path': 'test-mount-path', 'mount-path': 'test-mount-path',
organization: 'test-org', organization: 'test-org',
@@ -201,8 +203,9 @@ describe('handlePush()', () => {
files: ['test-folder'], files: ['test-folder'],
'max-execution-time': 10, 'max-execution-time': 10,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(remotes.push).toHaveBeenCalledWith( expect(remotes.push).toHaveBeenCalledWith(
expect.anything(), expect.anything(),
@@ -230,8 +233,8 @@ describe('handlePush()', () => {
const mockConfig = { apis: {} } as any; const mockConfig = { apis: {} } as any;
process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
await handlePush( await handlePush({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
'mount-path': 'test-mount-path', 'mount-path': 'test-mount-path',
organization: 'test-org', organization: 'test-org',
@@ -243,8 +246,9 @@ describe('handlePush()', () => {
files: [], files: [],
'max-execution-time': 10, 'max-execution-time': 10,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(remotes.getDefaultBranch).not.toHaveBeenCalled(); expect(remotes.getDefaultBranch).not.toHaveBeenCalled();
expect(remotes.upsert).not.toHaveBeenCalled(); expect(remotes.upsert).not.toHaveBeenCalled();
@@ -265,8 +269,8 @@ describe('handlePush()', () => {
pathRelativeSpy.mockImplementationOnce((_, p) => p); pathRelativeSpy.mockImplementationOnce((_, p) => p);
pathDirnameSpy.mockImplementation((_: string) => '.'); pathDirnameSpy.mockImplementation((_: string) => '.');
await handlePush( await handlePush({
{ argv: {
domain: 'test-domain', domain: 'test-domain',
'mount-path': 'test-mount-path', 'mount-path': 'test-mount-path',
project: 'test-project', project: 'test-project',
@@ -277,8 +281,9 @@ describe('handlePush()', () => {
'default-branch': 'main', 'default-branch': 'main',
'max-execution-time': 10, 'max-execution-time': 10,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(remotes.getDefaultBranch).toHaveBeenCalledWith( expect(remotes.getDefaultBranch).toHaveBeenCalledWith(
'test-org-from-config', 'test-org-from-config',
@@ -312,8 +317,8 @@ describe('handlePush()', () => {
pathRelativeSpy.mockImplementationOnce((_, p) => p); pathRelativeSpy.mockImplementationOnce((_, p) => p);
pathDirnameSpy.mockImplementation((_: string) => '.'); pathDirnameSpy.mockImplementation((_: string) => '.');
await handlePush( await handlePush({
{ argv: {
'mount-path': 'test-mount-path', 'mount-path': 'test-mount-path',
project: 'test-project', project: 'test-project',
branch: 'test-branch', branch: 'test-branch',
@@ -323,8 +328,9 @@ describe('handlePush()', () => {
files: ['test-file'], files: ['test-file'],
'max-execution-time': 10, 'max-execution-time': 10,
}, },
mockConfig config: mockConfig,
); version: 'cli-version',
});
expect(ReuniteApiClient).toBeCalledWith('test-domain-from-env', 'test-api-key'); expect(ReuniteApiClient).toBeCalledWith('test-domain-from-env', 'test-api-key');
}); });

View File

@@ -1,18 +1,19 @@
import * as colors from 'colorette'; import * as colors from 'colorette';
import type { Config, OutputFormat } from '@redocly/openapi-core';
import { exitWithError, printExecutionTime } from '../../utils/miscellaneous'; import { exitWithError, printExecutionTime } from '../../utils/miscellaneous';
import { Spinner } from '../../utils/spinner'; import { Spinner } from '../../utils/spinner';
import { DeploymentError } from '../utils'; import { DeploymentError } from '../utils';
import { ReuniteApiClient, getApiKeys, getDomain } from '../api'; import { ReuniteApiClient, getApiKeys, getDomain } from '../api';
import { capitalize } from '../../utils/js-utils'; import { capitalize } from '../../utils/js-utils';
import { retryUntilConditionMet } from './utils';
import type { OutputFormat } from '@redocly/openapi-core';
import type { CommandArgs } from '../../wrapper';
import type { import type {
DeploymentStatus, DeploymentStatus,
DeploymentStatusResponse, DeploymentStatusResponse,
PushResponse, PushResponse,
ScorecardItem, ScorecardItem,
} from '../api/types'; } from '../api/types';
import { retryUntilConditionMet } from './utils';
const RETRY_INTERVAL_MS = 5000; // 5 sec const RETRY_INTERVAL_MS = 5000; // 5 sec
@@ -37,10 +38,10 @@ export interface PushStatusSummary {
commit: PushResponse['commit']; commit: PushResponse['commit'];
} }
export async function handlePushStatus( export async function handlePushStatus({
argv: PushStatusOptions, argv,
config: Config config,
): Promise<PushStatusSummary | undefined> { }: CommandArgs<PushStatusOptions>): Promise<PushStatusSummary | undefined> {
const startedAt = performance.now(); const startedAt = performance.now();
const spinner = new Spinner(); const spinner = new Spinner();

View File

@@ -11,7 +11,8 @@ import {
import { handlePushStatus } from './push-status'; import { handlePushStatus } from './push-status';
import { ReuniteApiClient, getDomain, getApiKeys } from '../api'; import { ReuniteApiClient, getDomain, getApiKeys } from '../api';
import type { OutputFormat, Config } from '@redocly/openapi-core'; import type { OutputFormat } from '@redocly/openapi-core';
import type { CommandArgs } from '../../wrapper';
export type PushOptions = { export type PushOptions = {
apis?: string[]; apis?: string[];
@@ -42,10 +43,11 @@ export type PushOptions = {
type FileToUpload = { name: string; path: string }; type FileToUpload = { name: string; path: string };
export async function handlePush( export async function handlePush({
argv: PushOptions, argv,
config: Config config,
): Promise<{ pushId: string } | void> { version,
}: CommandArgs<PushOptions>): Promise<{ pushId: string } | void> {
const startedAt = performance.now(); // for printing execution time const startedAt = performance.now(); // for printing execution time
const startTime = Date.now(); // for push-status command const startTime = Date.now(); // for push-status command
@@ -131,8 +133,8 @@ export async function handlePush(
if (waitForDeployment) { if (waitForDeployment) {
process.stdout.write('\n'); process.stdout.write('\n');
await handlePushStatus( await handlePushStatus({
{ argv: {
organization: orgId, organization: orgId,
project: projectId, project: projectId,
pushId: id, pushId: id,
@@ -142,8 +144,9 @@ export async function handlePush(
'start-time': startTime, 'start-time': startTime,
'continue-on-deploy-failures': argv['continue-on-deploy-failures'], 'continue-on-deploy-failures': argv['continue-on-deploy-failures'],
}, },
config config,
); version,
});
} }
verbose && verbose &&
printExecutionTime( printExecutionTime(

View File

@@ -2,17 +2,21 @@ import { loadAndBundleSpec } from 'redoc';
import { dirname, resolve } from 'path'; import { dirname, resolve } from 'path';
import { writeFileSync, mkdirSync } from 'fs'; import { writeFileSync, mkdirSync } from 'fs';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { getMergedConfig, isAbsoluteUrl } from '@redocly/openapi-core';
import { getObjectOrJSON, getPageHTML } from './utils'; import { getObjectOrJSON, getPageHTML } from './utils';
import type { BuildDocsArgv } from './types';
import { Config, getMergedConfig, isAbsoluteUrl } from '@redocly/openapi-core';
import { exitWithError, getExecutionTime, getFallbackApisOrExit } from '../../utils/miscellaneous'; import { exitWithError, getExecutionTime, getFallbackApisOrExit } from '../../utils/miscellaneous';
export const handlerBuildCommand = async (argv: BuildDocsArgv, configFromFile: Config) => { import type { BuildDocsArgv } from './types';
import type { CommandArgs } from '../../wrapper';
export const handlerBuildCommand = async ({
argv,
config: configFromFile,
collectSpecData,
}: CommandArgs<BuildDocsArgv>) => {
const startedAt = performance.now(); const startedAt = performance.now();
const config = getMergedConfig(configFromFile, argv.api); const config = getMergedConfig(configFromFile, argv.api);
const apis = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config); const apis = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config);
const { path: pathToApi } = apis[0]; const { path: pathToApi } = apis[0];
@@ -31,6 +35,7 @@ export const handlerBuildCommand = async (argv: BuildDocsArgv, configFromFile: C
const elapsed = getExecutionTime(startedAt); const elapsed = getExecutionTime(startedAt);
const api = await loadAndBundleSpec(isAbsoluteUrl(pathToApi) ? pathToApi : resolve(pathToApi)); const api = await loadAndBundleSpec(isAbsoluteUrl(pathToApi) ? pathToApi : resolve(pathToApi));
collectSpecData?.(api);
const pageHTML = await getPageHTML( const pageHTML = await getPageHTML(
api, api,
pathToApi, pathToApi,

View File

@@ -1,4 +1,7 @@
import { formatProblems, getTotals, getMergedConfig, bundle, Config } from '@redocly/openapi-core'; import { performance } from 'perf_hooks';
import { blue, gray, green, yellow } from 'colorette';
import { writeFileSync } from 'fs';
import { formatProblems, getTotals, getMergedConfig, bundle } from '@redocly/openapi-core';
import { import {
dumpBundle, dumpBundle,
getExecutionTime, getExecutionTime,
@@ -8,12 +11,11 @@ import {
printUnusedWarnings, printUnusedWarnings,
saveBundle, saveBundle,
sortTopLevelKeysForOas, sortTopLevelKeysForOas,
checkForDeprecatedOptions,
} from '../utils/miscellaneous'; } from '../utils/miscellaneous';
import type { OutputExtensions, Skips, Totals } from '../types'; import type { OutputExtensions, Skips, Totals } from '../types';
import { performance } from 'perf_hooks'; import type { CommandArgs } from '../wrapper';
import { blue, gray, green, yellow } from 'colorette';
import { writeFileSync } from 'fs';
import { checkForDeprecatedOptions } from '../utils/miscellaneous';
export type BundleOptions = { export type BundleOptions = {
apis?: string[]; apis?: string[];
@@ -28,7 +30,12 @@ export type BundleOptions = {
'keep-url-references'?: boolean; 'keep-url-references'?: boolean;
} & Skips; } & Skips;
export async function handleBundle(argv: BundleOptions, config: Config, version: string) { export async function handleBundle({
argv,
config,
version,
collectSpecData,
}: CommandArgs<BundleOptions>) {
const removeUnusedComponents = const removeUnusedComponents =
argv['remove-unused-components'] || argv['remove-unused-components'] ||
config.rawConfig?.styleguide?.decorators?.hasOwnProperty('remove-unused-components'); config.rawConfig?.styleguide?.decorators?.hasOwnProperty('remove-unused-components');
@@ -59,6 +66,7 @@ export async function handleBundle(argv: BundleOptions, config: Config, version:
dereference: argv.dereferenced, dereference: argv.dereferenced,
removeUnusedComponents, removeUnusedComponents,
keepUrlRefs: argv['keep-url-references'], keepUrlRefs: argv['keep-url-references'],
collectSpecData,
}); });
const fileTotals = getTotals(problems); const fileTotals = getTotals(problems);

View File

@@ -2,7 +2,6 @@ import * as path from 'path';
import { red, blue, yellow, green } from 'colorette'; import { red, blue, yellow, green } from 'colorette';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { import {
Config,
SpecVersion, SpecVersion,
BaseResolver, BaseResolver,
formatProblems, formatProblems,
@@ -38,6 +37,7 @@ import type {
Oas3Server, Oas3Server,
Oas3_1Definition, Oas3_1Definition,
} from '@redocly/openapi-core/lib/typings/openapi'; } from '@redocly/openapi-core/lib/typings/openapi';
import type { CommandArgs } from '../wrapper';
const Tags = 'tags'; const Tags = 'tags';
const xTagGroups = 'x-tagGroups'; const xTagGroups = 'x-tagGroups';
@@ -64,7 +64,11 @@ export type JoinOptions = {
'lint-config'?: RuleSeverity; 'lint-config'?: RuleSeverity;
}; };
export async function handleJoin(argv: JoinOptions, config: Config, packageVersion: string) { export async function handleJoin({
argv,
config,
version: packageVersion,
}: CommandArgs<JoinOptions>) {
const startedAt = performance.now(); const startedAt = performance.now();
if (argv.apis.length < 2) { if (argv.apis.length < 2) {

View File

@@ -1,7 +1,6 @@
import { blue, gray } from 'colorette'; import { blue, gray } from 'colorette';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { import {
Config,
formatProblems, formatProblems,
getMergedConfig, getMergedConfig,
getTotals, getTotals,
@@ -23,10 +22,11 @@ import {
} from '../utils/miscellaneous'; } from '../utils/miscellaneous';
import { getCommandNameFromArgs } from '../utils/getCommandNameFromArgs'; import { getCommandNameFromArgs } from '../utils/getCommandNameFromArgs';
import type { Arguments } from 'yargs';
import type { OutputFormat, ProblemSeverity, RuleSeverity } from '@redocly/openapi-core'; import type { OutputFormat, ProblemSeverity, RuleSeverity } from '@redocly/openapi-core';
import type { RawConfigProcessor } from '@redocly/openapi-core/lib/config'; import type { RawConfigProcessor } from '@redocly/openapi-core/lib/config';
import type { CommandOptions, Skips, Totals } from '../types'; import type { CommandOptions, Skips, Totals } from '../types';
import type { Arguments } from 'yargs'; import type { CommandArgs } from '../wrapper';
export type LintOptions = { export type LintOptions = {
apis?: string[]; apis?: string[];
@@ -38,7 +38,12 @@ export type LintOptions = {
'lint-config'?: RuleSeverity; 'lint-config'?: RuleSeverity;
} & Omit<Skips, 'skip-decorator'>; } & Omit<Skips, 'skip-decorator'>;
export async function handleLint(argv: LintOptions, config: Config, version: string) { export async function handleLint({
argv,
config,
version,
collectSpecData,
}: CommandArgs<LintOptions>) {
const apis = await getFallbackApisOrExit(argv.apis, config); const apis = await getFallbackApisOrExit(argv.apis, config);
if (!apis.length) { if (!apis.length) {
@@ -74,6 +79,7 @@ export async function handleLint(argv: LintOptions, config: Config, version: str
const results = await lint({ const results = await lint({
ref: path, ref: path,
config: resolvedConfig, config: resolvedConfig,
collectSpecData,
}); });
const fileTotals = getTotals(results); const fileTotals = getTotals(results);

View File

@@ -1,7 +1,9 @@
import { Region, RedoclyClient, Config } from '@redocly/openapi-core'; import { Region, RedoclyClient } from '@redocly/openapi-core';
import { blue, green, gray } from 'colorette'; import { blue, green, gray } from 'colorette';
import { promptUser } from '../utils/miscellaneous'; import { promptUser } from '../utils/miscellaneous';
import type { CommandArgs } from '../wrapper';
export function promptClientToken(domain: string) { export function promptClientToken(domain: string) {
return promptUser( return promptUser(
green( green(
@@ -17,7 +19,7 @@ export type LoginOptions = {
config?: string; config?: string;
}; };
export async function handleLogin(argv: LoginOptions, config: Config) { export async function handleLogin({ argv, config }: CommandArgs<LoginOptions>) {
const region = argv.region || config.region; const region = argv.region || config.region;
const client = new RedoclyClient(region); const client = new RedoclyClient(region);
const clientToken = await promptClientToken(client.domain); const clientToken = await promptClientToken(client.domain);

View File

@@ -7,7 +7,9 @@ import {
loadConfigAndHandleErrors, loadConfigAndHandleErrors,
} from '../../utils/miscellaneous'; } from '../../utils/miscellaneous';
import startPreviewServer from './preview-server/preview-server'; import startPreviewServer from './preview-server/preview-server';
import type { Skips } from '../../types'; import type { Skips } from '../../types';
import type { CommandArgs } from '../../wrapper';
export type PreviewDocsOptions = { export type PreviewDocsOptions = {
port: number; port: number;
@@ -18,7 +20,10 @@ export type PreviewDocsOptions = {
force?: boolean; force?: boolean;
} & Omit<Skips, 'skip-rule'>; } & Omit<Skips, 'skip-rule'>;
export async function previewDocs(argv: PreviewDocsOptions, configFromFile: Config) { export async function previewDocs({
argv,
config: configFromFile,
}: CommandArgs<PreviewDocsOptions>) {
let isAuthorizedWithRedocly = false; let isAuthorizedWithRedocly = false;
let redocOptions: any = {}; let redocOptions: any = {};
let config = await reloadConfig(configFromFile); let config = await reloadConfig(configFromFile);

View File

@@ -4,12 +4,13 @@ import { spawn } from 'child_process';
import { PRODUCT_NAMES, PRODUCT_PACKAGES } from './constants'; import { PRODUCT_NAMES, PRODUCT_PACKAGES } from './constants';
import type { PreviewProjectOptions, Product } from './types'; import type { PreviewProjectOptions, Product } from './types';
import type { CommandArgs } from '../../wrapper';
export const previewProject = async (args: PreviewProjectOptions) => { export const previewProject = async ({ argv }: CommandArgs<PreviewProjectOptions>) => {
const { plan, port } = args; const { plan, port } = argv;
const projectDir = args['source-dir']; const projectDir = argv['source-dir'];
const product = args.product || tryGetProductFromPackageJson(projectDir); const product = argv.product || tryGetProductFromPackageJson(projectDir);
if (!isValidProduct(product)) { if (!isValidProduct(product)) {
process.stderr.write(`Invalid product ${product}`); process.stderr.write(`Invalid product ${product}`);

View File

@@ -26,6 +26,8 @@ import {
import { promptClientToken } from './login'; import { promptClientToken } from './login';
import { handlePush as handleCMSPush } from '../cms/commands/push'; import { handlePush as handleCMSPush } from '../cms/commands/push';
import type { CommandArgs } from '../wrapper';
const DEFAULT_VERSION = 'latest'; const DEFAULT_VERSION = 'latest';
export const DESTINATION_REGEX = export const DESTINATION_REGEX =
@@ -59,7 +61,7 @@ export function commonPushHandler({
return transformPush(handlePush); return transformPush(handlePush);
} }
export async function handlePush(argv: PushOptions, config: Config): Promise<void> { export async function handlePush({ argv, config }: CommandArgs<PushOptions>): Promise<void> {
const client = new RedoclyClient(config.region); const client = new RedoclyClient(config.region);
const isAuthorized = await client.isAuthorizedWithRedoclyByRegion(); const isAuthorized = await client.isAuthorizedWithRedoclyByRegion();
if (!isAuthorized) { if (!isAuthorized) {
@@ -366,16 +368,11 @@ type BarePushArgs = Omit<PushOptions, 'destination' | 'branchName'> & {
export const transformPush = export const transformPush =
(callback: typeof handlePush) => (callback: typeof handlePush) =>
( ({
{ argv: { apis, branch, 'batch-id': batchId, 'job-id': jobId, ...rest },
apis, config,
branch, version,
'batch-id': batchId, }: CommandArgs<BarePushArgs & { 'batch-id'?: string }>) => {
'job-id': jobId,
...rest
}: BarePushArgs & { 'batch-id'?: string },
config: Config
) => {
const [maybeApiOrDestination, maybeDestination, maybeBranchName] = apis || []; const [maybeApiOrDestination, maybeDestination, maybeBranchName] = apis || [];
if (batchId) { if (batchId) {
@@ -414,16 +411,17 @@ export const transformPush =
apiFile = maybeApiOrDestination; apiFile = maybeApiOrDestination;
} }
return callback( return callback({
{ argv: {
...rest, ...rest,
destination: rest.destination ?? destination, destination: rest.destination ?? destination,
api: apiFile, api: apiFile,
branchName: branch ?? maybeBranchName, branchName: branch ?? maybeBranchName,
'job-id': jobId || batchId, 'job-id': jobId || batchId,
}, },
config config,
); version,
});
}; };
export function getApiRoot({ export function getApiRoot({

View File

@@ -3,6 +3,9 @@ import * as path from 'path';
import * as openapiCore from '@redocly/openapi-core'; import * as openapiCore from '@redocly/openapi-core';
import { ComponentsFiles } from '../types'; import { ComponentsFiles } from '../types';
import { blue, green } from 'colorette'; import { blue, green } from 'colorette';
import { loadConfigAndHandleErrors } from '../../../utils/__mocks__/miscellaneous';
import type { Config } from '@redocly/openapi-core';
const utils = require('../../../utils/miscellaneous'); const utils = require('../../../utils/miscellaneous');
@@ -25,9 +28,13 @@ describe('#split', () => {
jest.spyOn(process.stderr, 'write').mockImplementation(() => true); jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
await handleSplit({ await handleSplit({
api: filePath, argv: {
outDir: openapiDir, api: filePath,
separator: '_', outDir: openapiDir,
separator: '_',
},
config: loadConfigAndHandleErrors() as any as Config,
version: 'cli-version',
}); });
expect(process.stderr.write).toBeCalledTimes(2); expect(process.stderr.write).toBeCalledTimes(2);
@@ -46,9 +53,13 @@ describe('#split', () => {
jest.spyOn(utils, 'pathToFilename').mockImplementation(() => 'newFilePath'); jest.spyOn(utils, 'pathToFilename').mockImplementation(() => 'newFilePath');
await handleSplit({ await handleSplit({
api: filePath, argv: {
outDir: openapiDir, api: filePath,
separator: '_', outDir: openapiDir,
separator: '_',
},
config: loadConfigAndHandleErrors() as any as Config,
version: 'cli-version',
}); });
expect(utils.pathToFilename).toBeCalledWith(expect.anything(), '_'); expect(utils.pathToFilename).toBeCalledWith(expect.anything(), '_');

View File

@@ -36,6 +36,7 @@ import type {
Oas3PathItem, Oas3PathItem,
Referenced, Referenced,
} from './types'; } from './types';
import type { CommandArgs } from '../../wrapper';
export type SplitOptions = { export type SplitOptions = {
api: string; api: string;
@@ -44,12 +45,13 @@ export type SplitOptions = {
config?: string; config?: string;
}; };
export async function handleSplit(argv: SplitOptions) { export async function handleSplit({ argv, collectSpecData }: CommandArgs<SplitOptions>) {
const startedAt = performance.now(); const startedAt = performance.now();
const { api, outDir, separator } = argv; const { api, outDir, separator } = argv;
validateDefinitionFileName(api!); validateDefinitionFileName(api!);
const ext = getAndValidateFileExtension(api); const ext = getAndValidateFileExtension(api);
const openapi = readYaml(api!) as Oas3Definition | Oas3_1Definition; const openapi = readYaml(api!) as Oas3Definition | Oas3_1Definition;
collectSpecData?.(openapi);
splitDefinition(openapi, outDir, separator, ext); splitDefinition(openapi, outDir, separator, ext);
process.stderr.write( process.stderr.write(
`🪓 Document: ${blue(api!)} ${green('is successfully split')} `🪓 Document: ${blue(api!)} ${green('is successfully split')}
@@ -292,7 +294,7 @@ function iteratePathItems(
for (const pathName of Object.keys(pathItems)) { for (const pathName of Object.keys(pathItems)) {
const pathFile = `${path.join(outDir, pathToFilename(pathName, pathSeparator))}.${ext}`; const pathFile = `${path.join(outDir, pathToFilename(pathName, pathSeparator))}.${ext}`;
const pathData = pathItems[pathName] as Oas3PathItem; const pathData = pathItems[pathName];
if (isRef(pathData)) continue; if (isRef(pathData)) continue;

View File

@@ -1,7 +1,6 @@
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import * as colors from 'colorette'; import * as colors from 'colorette';
import { import {
Config,
StyleguideConfig, StyleguideConfig,
normalizeTypes, normalizeTypes,
BaseResolver, BaseResolver,
@@ -15,7 +14,9 @@ import {
} from '@redocly/openapi-core'; } from '@redocly/openapi-core';
import { getFallbackApisOrExit } from '../utils/miscellaneous'; import { getFallbackApisOrExit } from '../utils/miscellaneous';
import { printExecutionTime } from '../utils/miscellaneous'; import { printExecutionTime } from '../utils/miscellaneous';
import type { StatsAccumulator, StatsName, WalkContext, OutputFormat } from '@redocly/openapi-core'; import type { StatsAccumulator, StatsName, WalkContext, OutputFormat } from '@redocly/openapi-core';
import type { CommandArgs } from '../wrapper';
const statsAccumulator: StatsAccumulator = { const statsAccumulator: StatsAccumulator = {
refs: { metric: '🚗 References', total: 0, color: 'red', items: new Set() }, refs: { metric: '🚗 References', total: 0, color: 'red', items: new Set() },
@@ -86,10 +87,11 @@ export type StatsOptions = {
config?: string; config?: string;
}; };
export async function handleStats(argv: StatsOptions, config: Config) { export async function handleStats({ argv, config, collectSpecData }: CommandArgs<StatsOptions>) {
const [{ path }] = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config); const [{ path }] = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config);
const externalRefResolver = new BaseResolver(config.resolve); const externalRefResolver = new BaseResolver(config.resolve);
const { bundle: document } = await bundle({ config, ref: path }); const { bundle: document } = await bundle({ config, ref: path });
collectSpecData?.(document.parsed);
const lintConfig: StyleguideConfig = config.styleguide; const lintConfig: StyleguideConfig = config.styleguide;
const specVersion = detectSpec(document.parsed); const specVersion = detectSpec(document.parsed);
const types = normalizeTypes( const types = normalizeTypes(

View File

@@ -541,7 +541,10 @@ export function cleanColors(input: string): string {
export async function sendTelemetry( export async function sendTelemetry(
argv: Arguments | undefined, argv: Arguments | undefined,
exit_code: ExitCode, exit_code: ExitCode,
has_config: boolean | undefined has_config: boolean | undefined,
spec_version: string | undefined,
spec_keyword: string | undefined,
spec_full_version: string | undefined
): Promise<void> { ): Promise<void> {
try { try {
if (!argv) { if (!argv) {
@@ -569,6 +572,9 @@ export async function sendTelemetry(
environment_ci: process.env.CI, environment_ci: process.env.CI,
raw_input: cleanRawInput(process.argv.slice(2)), raw_input: cleanRawInput(process.argv.slice(2)),
has_config, has_config,
spec_version,
spec_keyword,
spec_full_version,
}; };
await fetch(`https://api.redocly.com/registry/telemetry/cli`, { await fetch(`https://api.redocly.com/registry/telemetry/cli`, {
method: 'POST', method: 'POST',
@@ -598,6 +604,9 @@ export type Analytics = {
environment_ci?: string; environment_ci?: string;
raw_input: string; raw_input: string;
has_config?: boolean; has_config?: boolean;
spec_version?: string;
spec_keyword?: string;
spec_full_version?: string;
}; };
function isFile(value: string) { function isFile(value: string) {

View File

@@ -1,22 +1,48 @@
import { Config, Region, doesYamlFileExist } from '@redocly/openapi-core'; import { detectSpec, doesYamlFileExist } from '@redocly/openapi-core';
import type { Arguments } from 'yargs'; import { isPlainObject } from '@redocly/openapi-core/lib/utils';
import { version } from './utils/update-version-notifier'; import { version } from './utils/update-version-notifier';
import { import { exitWithError, loadConfigAndHandleErrors, sendTelemetry } from './utils/miscellaneous';
ExitCode,
exitWithError,
loadConfigAndHandleErrors,
sendTelemetry,
} from './utils/miscellaneous';
import { lintConfigCallback } from './commands/lint'; import { lintConfigCallback } from './commands/lint';
import type { Arguments } from 'yargs';
import type { Config, Region } from '@redocly/openapi-core';
import type { CollectFn } from '@redocly/openapi-core/lib/utils';
import type { ExitCode } from './utils/miscellaneous';
import type { CommandOptions } from './types'; import type { CommandOptions } from './types';
export type CommandArgs<T extends CommandOptions> = {
argv: T;
config: Config;
version: string;
collectSpecData?: CollectFn;
};
export function commandWrapper<T extends CommandOptions>( export function commandWrapper<T extends CommandOptions>(
commandHandler?: (argv: T, config: Config, version: string) => Promise<unknown> commandHandler?: (wrapperArgs: CommandArgs<T>) => Promise<unknown>
) { ) {
return async (argv: Arguments<T>) => { return async (argv: Arguments<T>) => {
let code: ExitCode = 2; let code: ExitCode = 2;
let hasConfig; let hasConfig;
let telemetry; let telemetry;
let specVersion: string | undefined;
let specKeyword: string | undefined;
let specFullVersion: string | undefined;
const collectSpecData: CollectFn = (document) => {
specVersion = detectSpec(document);
if (!isPlainObject(document)) return;
specKeyword = document?.openapi
? 'openapi'
: document?.swagger
? 'swagger'
: document?.asyncapi
? 'asyncapi'
: document?.arazzo
? 'arazzo'
: undefined;
if (specKeyword) {
specFullVersion = document[specKeyword] as string;
}
};
try { try {
if (argv.config && !doesYamlFileExist(argv.config)) { if (argv.config && !doesYamlFileExist(argv.config)) {
exitWithError('Please provide a valid path to the configuration file.'); exitWithError('Please provide a valid path to the configuration file.');
@@ -32,14 +58,14 @@ export function commandWrapper<T extends CommandOptions>(
hasConfig = !config.styleguide.recommendedFallback; hasConfig = !config.styleguide.recommendedFallback;
code = 1; code = 1;
if (typeof commandHandler === 'function') { if (typeof commandHandler === 'function') {
await commandHandler(argv, config, version); await commandHandler({ argv, config, version, collectSpecData });
} }
code = 0; code = 0;
} catch (err) { } catch (err) {
// Do nothing // Do nothing
} finally { } finally {
if (process.env.REDOCLY_TELEMETRY !== 'off' && telemetry !== 'off') { if (process.env.REDOCLY_TELEMETRY !== 'off' && telemetry !== 'off') {
await sendTelemetry(argv, code, hasConfig); await sendTelemetry(argv, code, hasConfig, specVersion, specKeyword, specFullVersion);
} }
process.once('beforeExit', () => { process.once('beforeExit', () => {
process.exit(code); process.exit(code);

View File

@@ -24,6 +24,7 @@ import type { WalkContext, UserContext, ResolveResult, NormalizedProblem } from
import type { Config, StyleguideConfig } from './config'; import type { Config, StyleguideConfig } from './config';
import type { OasRef } from './typings/openapi'; import type { OasRef } from './typings/openapi';
import type { Document, ResolvedRefMap } from './resolve'; import type { Document, ResolvedRefMap } from './resolve';
import type { CollectFn } from './utils';
export enum OasVersion { export enum OasVersion {
Version2 = 'oas2', Version2 = 'oas2',
@@ -82,6 +83,7 @@ export async function bundle(
opts: { opts: {
ref?: string; ref?: string;
doc?: Document; doc?: Document;
collectSpecData?: CollectFn;
} & BundleOptions } & BundleOptions
) { ) {
const { const {
@@ -100,6 +102,7 @@ export async function bundle(
if (document instanceof Error) { if (document instanceof Error) {
throw document; throw document;
} }
opts.collectSpecData?.(document.parsed);
return bundleDocument({ return bundleDocument({
document, document,

View File

@@ -22,14 +22,17 @@ import type {
Oas3Visitor, Oas3Visitor,
RuleInstanceConfig, RuleInstanceConfig,
} from './visitors'; } from './visitors';
import type { CollectFn } from './utils';
export async function lint(opts: { export async function lint(opts: {
ref: string; ref: string;
config: Config; config: Config;
externalRefResolver?: BaseResolver; externalRefResolver?: BaseResolver;
collectSpecData?: CollectFn;
}) { }) {
const { ref, externalRefResolver = new BaseResolver(opts.config.resolve) } = opts; const { ref, externalRefResolver = new BaseResolver(opts.config.resolve) } = opts;
const document = (await externalRefResolver.resolveDocument(null, ref, true)) as Document; const document = (await externalRefResolver.resolveDocument(null, ref, true)) as Document;
opts.collectSpecData?.(document.parsed);
return lintDocument({ return lintDocument({
document, document,

View File

@@ -1,4 +1,4 @@
import { import type {
Oas3Rule, Oas3Rule,
Oas3Preprocessor, Oas3Preprocessor,
Oas2Rule, Oas2Rule,
@@ -16,7 +16,7 @@ import { Oas3_1Types } from './types/oas3_1';
import { AsyncApi2Types } from './types/asyncapi2'; import { AsyncApi2Types } from './types/asyncapi2';
import { AsyncApi3Types } from './types/asyncapi3'; import { AsyncApi3Types } from './types/asyncapi3';
import { ArazzoTypes } from './types/arazzo'; import { ArazzoTypes } from './types/arazzo';
import { import type {
BuiltInAsync2RuleId, BuiltInAsync2RuleId,
BuiltInAsync3RuleId, BuiltInAsync3RuleId,
BuiltInCommonOASRuleId, BuiltInCommonOASRuleId,
@@ -24,6 +24,7 @@ import {
BuiltInOAS2RuleId, BuiltInOAS2RuleId,
BuiltInOAS3RuleId, BuiltInOAS3RuleId,
} from './types/redocly-yaml'; } from './types/redocly-yaml';
import { isPlainObject } from './utils';
export enum SpecVersion { export enum SpecVersion {
OAS2 = 'oas2', OAS2 = 'oas2',
@@ -93,8 +94,8 @@ export type Async2DecoratorsSet = Record<string, Async2Preprocessor>;
export type Async3DecoratorsSet = Record<string, Async3Preprocessor>; export type Async3DecoratorsSet = Record<string, Async3Preprocessor>;
export type ArazzoDecoratorsSet = Record<string, ArazzoPreprocessor>; export type ArazzoDecoratorsSet = Record<string, ArazzoPreprocessor>;
export function detectSpec(root: any): SpecVersion { export function detectSpec(root: unknown): SpecVersion {
if (typeof root !== 'object') { if (!isPlainObject(root)) {
throw new Error(`Document must be JSON object, got ${typeof root}`); throw new Error(`Document must be JSON object, got ${typeof root}`);
} }
@@ -102,11 +103,11 @@ export function detectSpec(root: any): SpecVersion {
throw new Error(`Invalid OpenAPI version: should be a string but got "${typeof root.openapi}"`); throw new Error(`Invalid OpenAPI version: should be a string but got "${typeof root.openapi}"`);
} }
if (root.openapi && root.openapi.startsWith('3.0')) { if (typeof root.openapi === 'string' && root.openapi.startsWith('3.0')) {
return SpecVersion.OAS3_0; return SpecVersion.OAS3_0;
} }
if (root.openapi && root.openapi.startsWith('3.1')) { if (typeof root.openapi === 'string' && root.openapi.startsWith('3.1')) {
return SpecVersion.OAS3_1; return SpecVersion.OAS3_1;
} }
@@ -114,16 +115,15 @@ export function detectSpec(root: any): SpecVersion {
return SpecVersion.OAS2; return SpecVersion.OAS2;
} }
// if not detected yet
if (root.openapi || root.swagger) { if (root.openapi || root.swagger) {
throw new Error(`Unsupported OpenAPI version: ${root.openapi || root.swagger}`); throw new Error(`Unsupported OpenAPI version: ${root.openapi || root.swagger}`);
} }
if (root.asyncapi && root.asyncapi.startsWith('2.')) { if (typeof root.asyncapi === 'string' && root.asyncapi.startsWith('2.')) {
return SpecVersion.Async2; return SpecVersion.Async2;
} }
if (root.asyncapi && root.asyncapi.startsWith('3.')) { if (typeof root.asyncapi === 'string' && root.asyncapi.startsWith('3.')) {
return SpecVersion.Async3; return SpecVersion.Async3;
} }
@@ -131,7 +131,7 @@ export function detectSpec(root: any): SpecVersion {
throw new Error(`Unsupported AsyncAPI version: ${root.asyncapi}`); throw new Error(`Unsupported AsyncAPI version: ${root.asyncapi}`);
} }
if (root.arazzo && root.arazzo.startsWith('1.')) { if (typeof root.arazzo === 'string' && root.arazzo.startsWith('1.')) {
return SpecVersion.Arazzo; return SpecVersion.Arazzo;
} }

View File

@@ -38,15 +38,15 @@ export function isDefined<T>(x: T | undefined): x is T {
return x !== undefined; return x !== undefined;
} }
export function isPlainObject(value: any): value is Record<string, unknown> { export function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value); return value !== null && typeof value === 'object' && !Array.isArray(value);
} }
export function isEmptyObject(value: any): value is Record<string, unknown> { export function isEmptyObject(value: unknown): value is Record<string, unknown> {
return isPlainObject(value) && Object.keys(value).length === 0; return isPlainObject(value) && Object.keys(value).length === 0;
} }
export function isEmptyArray(value: any) { export function isEmptyArray(value: unknown) {
return Array.isArray(value) && value.length === 0; return Array.isArray(value) && value.length === 0;
} }
@@ -317,3 +317,5 @@ export function dequal(foo: any, bar: any): boolean {
return foo !== foo && bar !== bar; return foo !== foo && bar !== bar;
} }
export type CollectFn = (value: unknown) => void;