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`
- CLI version
- 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.

View File

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

View File

@@ -14,21 +14,21 @@ describe('handleJoin', () => {
colloreteYellowMock.mockImplementation((string: string) => string);
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.`);
});
it('should call exitWithError because passed all 3 options for tags', async () => {
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.yaml', 'second.yaml'],
'prefix-tags-with-info-prop': 'something',
'without-x-tag-groups': true,
'prefix-tags-with-filename': true,
},
{} as any,
'cli-version'
);
config: {} as any,
version: 'cli-version',
});
expect(exitWithError).toHaveBeenCalledWith(
`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 () => {
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.yaml', 'second.yaml'],
'without-x-tag-groups': true,
'prefix-tags-with-filename': true,
},
{} as any,
'cli-version'
);
config: {} as any,
version: 'cli-version',
});
expect(exitWithError).toHaveBeenCalledWith(
`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 () => {
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.yaml', 'second.yaml'],
},
ConfigFixture as any,
'cli-version'
);
config: ConfigFixture as any,
version: 'cli-version',
});
expect(exitWithError).toHaveBeenCalledWith(
'Only OpenAPI 3.0 and OpenAPI 3.1 are supported: undefined.'
);
@@ -68,13 +68,13 @@ describe('handleJoin', () => {
(detectSpec as jest.Mock)
.mockImplementationOnce(() => 'oas3_0')
.mockImplementationOnce(() => 'oas3_1');
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.yaml', 'second.yaml'],
},
ConfigFixture as any,
'cli-version'
);
config: ConfigFixture as any,
version: 'cli-version',
});
expect(exitWithError).toHaveBeenCalledWith(
'All APIs must use the same OpenAPI version: undefined.'
@@ -83,13 +83,13 @@ describe('handleJoin', () => {
it('should call writeToFileByExtension function', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.yaml', 'second.yaml'],
},
ConfigFixture as any,
'cli-version'
);
config: ConfigFixture as any,
version: 'cli-version',
});
expect(writeToFileByExtension).toHaveBeenCalledWith(
expect.any(Object),
@@ -100,13 +100,13 @@ describe('handleJoin', () => {
it('should call writeToFileByExtension function for OpenAPI 3.1', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_1');
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.yaml', 'second.yaml'],
},
ConfigFixture as any,
'cli-version'
);
config: ConfigFixture as any,
version: 'cli-version',
});
expect(writeToFileByExtension).toHaveBeenCalledWith(
expect.any(Object),
@@ -117,14 +117,14 @@ describe('handleJoin', () => {
it('should call writeToFileByExtension function with custom output file', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.yaml', 'second.yaml'],
output: 'output.yml',
},
ConfigFixture as any,
'cli-version'
);
config: ConfigFixture as any,
version: 'cli-version',
});
expect(writeToFileByExtension).toHaveBeenCalledWith(
expect.any(Object),
@@ -135,13 +135,13 @@ describe('handleJoin', () => {
it('should call writeToFileByExtension function with json file extension', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.json', 'second.yaml'],
},
ConfigFixture as any,
'cli-version'
);
config: ConfigFixture as any,
version: 'cli-version',
});
expect(writeToFileByExtension).toHaveBeenCalledWith(
expect.any(Object),
@@ -152,13 +152,13 @@ describe('handleJoin', () => {
it('should call skipDecorators and skipPreprocessors', async () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.yaml', 'second.yaml'],
},
ConfigFixture as any,
'cli-version'
);
config: ConfigFixture as any,
version: 'cli-version',
});
const config = loadConfig();
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 () => {
(detectSpec as jest.Mock).mockReturnValue('oas3_0');
await handleJoin(
{
await handleJoin({
argv: {
apis: ['first.yaml', 'second.yaml', 'third.yaml'],
'prefix-components-with-info-prop': 'title',
output: 'join-result.yaml',
},
ConfigFixture as any,
'cli-version'
);
config: ConfigFixture as any,
version: 'cli-version',
});
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 () => {
redoclyClient.domain = 'redoc.ly';
await handlePush(
{
await handlePush({
argv: {
upsert: true,
api: 'spec.json',
destination: '@org/my-api@1.0.0',
branchName: 'test',
},
ConfigFixture as any
);
config: ConfigFixture as any,
version: 'cli-version',
});
expect(mockPromptClientToken).toBeCalledTimes(1);
expect(mockPromptClientToken).toHaveBeenCalledWith(redoclyClient.domain);
});
it('should call login with EU domain when region is EU', async () => {
redoclyClient.domain = 'eu.redocly.com';
await handlePush(
{
await handlePush({
argv: {
upsert: true,
api: 'spec.json',
destination: '@org/my-api@1.0.0',
branchName: 'test',
},
ConfigFixture as any
);
config: ConfigFixture as any,
version: 'cli-version',
});
expect(mockPromptClientToken).toBeCalledTimes(1);
expect(mockPromptClientToken).toHaveBeenCalledWith(redoclyClient.domain);
});

View File

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

View File

@@ -4,6 +4,7 @@ import { commandWrapper } from '../wrapper';
import { handleLint } from '../commands/lint';
import { Arguments } from 'yargs';
import { handlePush, PushOptions } from '../commands/push';
import { detectSpec } from '@redocly/openapi-core';
jest.mock('node-fetch');
jest.mock('../utils/miscellaneous', () => ({
@@ -11,7 +12,9 @@ jest.mock('../utils/miscellaneous', () => ({
loadConfigAndHandleErrors: jest.fn(),
}));
jest.mock('../commands/lint', () => ({
handleLint: jest.fn(),
handleLint: jest.fn().mockImplementation(({ collectSpecData }) => {
collectSpecData({ openapi: '3.1.0' });
}),
lintConfigCallback: jest.fn(),
}));
@@ -20,13 +23,32 @@ describe('commandWrapper', () => {
(loadConfigAndHandleErrors as jest.Mock).mockImplementation(() => {
return { telemetry: 'on', styleguide: { recommendedFallback: true } };
});
(detectSpec as jest.Mock).mockImplementationOnce(() => {
return 'oas3_1';
});
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);
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 () => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,17 +2,21 @@ import { loadAndBundleSpec } from 'redoc';
import { dirname, resolve } from 'path';
import { writeFileSync, mkdirSync } from 'fs';
import { performance } from 'perf_hooks';
import { getMergedConfig, isAbsoluteUrl } from '@redocly/openapi-core';
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';
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 config = getMergedConfig(configFromFile, argv.api);
const apis = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config);
const { path: pathToApi } = apis[0];
@@ -31,6 +35,7 @@ export const handlerBuildCommand = async (argv: BuildDocsArgv, configFromFile: C
const elapsed = getExecutionTime(startedAt);
const api = await loadAndBundleSpec(isAbsoluteUrl(pathToApi) ? pathToApi : resolve(pathToApi));
collectSpecData?.(api);
const pageHTML = await getPageHTML(
api,
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 {
dumpBundle,
getExecutionTime,
@@ -8,12 +11,11 @@ import {
printUnusedWarnings,
saveBundle,
sortTopLevelKeysForOas,
checkForDeprecatedOptions,
} from '../utils/miscellaneous';
import type { OutputExtensions, Skips, Totals } from '../types';
import { performance } from 'perf_hooks';
import { blue, gray, green, yellow } from 'colorette';
import { writeFileSync } from 'fs';
import { checkForDeprecatedOptions } from '../utils/miscellaneous';
import type { CommandArgs } from '../wrapper';
export type BundleOptions = {
apis?: string[];
@@ -28,7 +30,12 @@ export type BundleOptions = {
'keep-url-references'?: boolean;
} & Skips;
export async function handleBundle(argv: BundleOptions, config: Config, version: string) {
export async function handleBundle({
argv,
config,
version,
collectSpecData,
}: CommandArgs<BundleOptions>) {
const removeUnusedComponents =
argv['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,
removeUnusedComponents,
keepUrlRefs: argv['keep-url-references'],
collectSpecData,
});
const fileTotals = getTotals(problems);

View File

@@ -2,7 +2,6 @@ import * as path from 'path';
import { red, blue, yellow, green } from 'colorette';
import { performance } from 'perf_hooks';
import {
Config,
SpecVersion,
BaseResolver,
formatProblems,
@@ -38,6 +37,7 @@ import type {
Oas3Server,
Oas3_1Definition,
} from '@redocly/openapi-core/lib/typings/openapi';
import type { CommandArgs } from '../wrapper';
const Tags = 'tags';
const xTagGroups = 'x-tagGroups';
@@ -64,7 +64,11 @@ export type JoinOptions = {
'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();
if (argv.apis.length < 2) {

View File

@@ -1,7 +1,6 @@
import { blue, gray } from 'colorette';
import { performance } from 'perf_hooks';
import {
Config,
formatProblems,
getMergedConfig,
getTotals,
@@ -23,10 +22,11 @@ import {
} from '../utils/miscellaneous';
import { getCommandNameFromArgs } from '../utils/getCommandNameFromArgs';
import type { Arguments } from 'yargs';
import type { OutputFormat, ProblemSeverity, RuleSeverity } from '@redocly/openapi-core';
import type { RawConfigProcessor } from '@redocly/openapi-core/lib/config';
import type { CommandOptions, Skips, Totals } from '../types';
import type { Arguments } from 'yargs';
import type { CommandArgs } from '../wrapper';
export type LintOptions = {
apis?: string[];
@@ -38,7 +38,12 @@ export type LintOptions = {
'lint-config'?: RuleSeverity;
} & 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);
if (!apis.length) {
@@ -74,6 +79,7 @@ export async function handleLint(argv: LintOptions, config: Config, version: str
const results = await lint({
ref: path,
config: resolvedConfig,
collectSpecData,
});
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 { promptUser } from '../utils/miscellaneous';
import type { CommandArgs } from '../wrapper';
export function promptClientToken(domain: string) {
return promptUser(
green(
@@ -17,7 +19,7 @@ export type LoginOptions = {
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 client = new RedoclyClient(region);
const clientToken = await promptClientToken(client.domain);

View File

@@ -7,7 +7,9 @@ import {
loadConfigAndHandleErrors,
} from '../../utils/miscellaneous';
import startPreviewServer from './preview-server/preview-server';
import type { Skips } from '../../types';
import type { CommandArgs } from '../../wrapper';
export type PreviewDocsOptions = {
port: number;
@@ -18,7 +20,10 @@ export type PreviewDocsOptions = {
force?: boolean;
} & 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 redocOptions: any = {};
let config = await reloadConfig(configFromFile);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,48 @@
import { Config, Region, doesYamlFileExist } from '@redocly/openapi-core';
import type { Arguments } from 'yargs';
import { detectSpec, doesYamlFileExist } from '@redocly/openapi-core';
import { isPlainObject } from '@redocly/openapi-core/lib/utils';
import { version } from './utils/update-version-notifier';
import {
ExitCode,
exitWithError,
loadConfigAndHandleErrors,
sendTelemetry,
} from './utils/miscellaneous';
import { exitWithError, loadConfigAndHandleErrors, sendTelemetry } from './utils/miscellaneous';
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';
export type CommandArgs<T extends CommandOptions> = {
argv: T;
config: Config;
version: string;
collectSpecData?: CollectFn;
};
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>) => {
let code: ExitCode = 2;
let hasConfig;
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 {
if (argv.config && !doesYamlFileExist(argv.config)) {
exitWithError('Please provide a valid path to the configuration file.');
@@ -32,14 +58,14 @@ export function commandWrapper<T extends CommandOptions>(
hasConfig = !config.styleguide.recommendedFallback;
code = 1;
if (typeof commandHandler === 'function') {
await commandHandler(argv, config, version);
await commandHandler({ argv, config, version, collectSpecData });
}
code = 0;
} catch (err) {
// Do nothing
} finally {
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.exit(code);

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import {
import type {
Oas3Rule,
Oas3Preprocessor,
Oas2Rule,
@@ -16,7 +16,7 @@ import { Oas3_1Types } from './types/oas3_1';
import { AsyncApi2Types } from './types/asyncapi2';
import { AsyncApi3Types } from './types/asyncapi3';
import { ArazzoTypes } from './types/arazzo';
import {
import type {
BuiltInAsync2RuleId,
BuiltInAsync3RuleId,
BuiltInCommonOASRuleId,
@@ -24,6 +24,7 @@ import {
BuiltInOAS2RuleId,
BuiltInOAS3RuleId,
} from './types/redocly-yaml';
import { isPlainObject } from './utils';
export enum SpecVersion {
OAS2 = 'oas2',
@@ -93,8 +94,8 @@ export type Async2DecoratorsSet = Record<string, Async2Preprocessor>;
export type Async3DecoratorsSet = Record<string, Async3Preprocessor>;
export type ArazzoDecoratorsSet = Record<string, ArazzoPreprocessor>;
export function detectSpec(root: any): SpecVersion {
if (typeof root !== 'object') {
export function detectSpec(root: unknown): SpecVersion {
if (!isPlainObject(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}"`);
}
if (root.openapi && root.openapi.startsWith('3.0')) {
if (typeof root.openapi === 'string' && root.openapi.startsWith('3.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;
}
@@ -114,16 +115,15 @@ export function detectSpec(root: any): SpecVersion {
return SpecVersion.OAS2;
}
// if not detected yet
if (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;
}
if (root.asyncapi && root.asyncapi.startsWith('3.')) {
if (typeof root.asyncapi === 'string' && root.asyncapi.startsWith('3.')) {
return SpecVersion.Async3;
}
@@ -131,7 +131,7 @@ export function detectSpec(root: any): SpecVersion {
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;
}

View File

@@ -38,15 +38,15 @@ export function isDefined<T>(x: T | undefined): x is T {
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);
}
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;
}
export function isEmptyArray(value: any) {
export function isEmptyArray(value: unknown) {
return Array.isArray(value) && value.length === 0;
}
@@ -317,3 +317,5 @@ export function dequal(foo: any, bar: any): boolean {
return foo !== foo && bar !== bar;
}
export type CollectFn = (value: unknown) => void;