@redocly/cli 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/commands/build-docs/index.js +2 -4
  3. package/lib/commands/build-docs/utils.d.ts +1 -1
  4. package/lib/commands/build-docs/utils.js +3 -3
  5. package/package.json +2 -2
  6. package/src/__mocks__/@redocly/openapi-core.ts +80 -0
  7. package/src/__mocks__/documents.ts +63 -0
  8. package/src/__mocks__/fs.ts +6 -0
  9. package/src/__mocks__/perf_hooks.ts +3 -0
  10. package/src/__mocks__/redoc.ts +2 -0
  11. package/src/__mocks__/utils.ts +19 -0
  12. package/src/__tests__/commands/build-docs.test.ts +62 -0
  13. package/src/__tests__/commands/bundle.test.ts +150 -0
  14. package/src/__tests__/commands/join.test.ts +122 -0
  15. package/src/__tests__/commands/lint.test.ts +190 -0
  16. package/src/__tests__/commands/push-region.test.ts +58 -0
  17. package/src/__tests__/commands/push.test.ts +492 -0
  18. package/src/__tests__/fetch-with-timeout.test.ts +35 -0
  19. package/src/__tests__/fixtures/config.ts +21 -0
  20. package/src/__tests__/fixtures/openapi.json +0 -0
  21. package/src/__tests__/fixtures/openapi.yaml +0 -0
  22. package/src/__tests__/fixtures/redocly.yaml +0 -0
  23. package/src/__tests__/utils.test.ts +564 -0
  24. package/src/__tests__/wrapper.test.ts +57 -0
  25. package/src/assert-node-version.ts +8 -0
  26. package/src/commands/build-docs/index.ts +50 -0
  27. package/src/commands/build-docs/template.hbs +23 -0
  28. package/src/commands/build-docs/types.ts +24 -0
  29. package/src/commands/build-docs/utils.ts +110 -0
  30. package/src/commands/bundle.ts +177 -0
  31. package/src/commands/join.ts +811 -0
  32. package/src/commands/lint.ts +151 -0
  33. package/src/commands/login.ts +27 -0
  34. package/src/commands/preview-docs/index.ts +190 -0
  35. package/src/commands/preview-docs/preview-server/default.hbs +24 -0
  36. package/src/commands/preview-docs/preview-server/hot.js +42 -0
  37. package/src/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
  38. package/src/commands/preview-docs/preview-server/preview-server.ts +156 -0
  39. package/src/commands/preview-docs/preview-server/server.ts +91 -0
  40. package/src/commands/push.ts +441 -0
  41. package/src/commands/split/__tests__/fixtures/samples.json +61 -0
  42. package/src/commands/split/__tests__/fixtures/spec.json +70 -0
  43. package/src/commands/split/__tests__/fixtures/webhooks.json +85 -0
  44. package/src/commands/split/__tests__/index.test.ts +137 -0
  45. package/src/commands/split/index.ts +385 -0
  46. package/src/commands/split/types.ts +85 -0
  47. package/src/commands/stats.ts +119 -0
  48. package/src/custom.d.ts +1 -0
  49. package/src/fetch-with-timeout.ts +21 -0
  50. package/src/index.ts +484 -0
  51. package/src/js-utils.ts +17 -0
  52. package/src/types.ts +40 -0
  53. package/src/update-version-notifier.ts +106 -0
  54. package/src/utils.ts +590 -0
  55. package/src/wrapper.ts +42 -0
  56. package/tsconfig.json +9 -0
  57. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,492 @@
1
+ import * as fs from 'fs';
2
+ import { Config, getMergedConfig } from '@redocly/openapi-core';
3
+ import { exitWithError } from '../../utils';
4
+ import { getApiRoot, getDestinationProps, handlePush, transformPush } from '../../commands/push';
5
+ import { ConfigFixture } from '../fixtures/config';
6
+ import { yellow } from 'colorette';
7
+
8
+ jest.mock('fs');
9
+ jest.mock('node-fetch', () => ({
10
+ default: jest.fn(() => ({
11
+ ok: true,
12
+ json: jest.fn().mockResolvedValue({}),
13
+ })),
14
+ }));
15
+ jest.mock('@redocly/openapi-core');
16
+ jest.mock('../../utils');
17
+
18
+ (getMergedConfig as jest.Mock).mockImplementation((config) => config);
19
+
20
+ describe('push', () => {
21
+ const redoclyClient = require('@redocly/openapi-core').__redoclyClient;
22
+
23
+ beforeEach(() => {
24
+ jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
25
+ });
26
+
27
+ it('pushes definition', async () => {
28
+ await handlePush(
29
+ {
30
+ upsert: true,
31
+ api: 'spec.json',
32
+ destination: '@org/my-api@1.0.0',
33
+ branchName: 'test',
34
+ public: true,
35
+ 'job-id': '123',
36
+ 'batch-size': 2,
37
+ },
38
+ ConfigFixture as any
39
+ );
40
+
41
+ expect(redoclyClient.registryApi.prepareFileUpload).toBeCalledTimes(1);
42
+ expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
43
+ expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({
44
+ branch: 'test',
45
+ filePaths: ['filePath'],
46
+ isUpsert: true,
47
+ isPublic: true,
48
+ name: 'my-api',
49
+ organizationId: 'org',
50
+ rootFilePath: 'filePath',
51
+ version: '1.0.0',
52
+ batchId: '123',
53
+ batchSize: 2,
54
+ });
55
+ });
56
+
57
+ it('fails if jobId value is an empty string', async () => {
58
+ await handlePush(
59
+ {
60
+ upsert: true,
61
+ api: 'spec.json',
62
+ destination: '@org/my-api@1.0.0',
63
+ branchName: 'test',
64
+ public: true,
65
+ 'job-id': ' ',
66
+ 'batch-size': 2,
67
+ },
68
+ ConfigFixture as any
69
+ );
70
+
71
+ expect(exitWithError).toBeCalledTimes(1);
72
+ });
73
+
74
+ it('fails if batchSize value is less than 2', async () => {
75
+ await handlePush(
76
+ {
77
+ upsert: true,
78
+ api: 'spec.json',
79
+ destination: '@org/my-api@1.0.0',
80
+ branchName: 'test',
81
+ public: true,
82
+ 'job-id': '123',
83
+ 'batch-size': 1,
84
+ },
85
+ ConfigFixture as any
86
+ );
87
+
88
+ expect(exitWithError).toBeCalledTimes(1);
89
+ });
90
+
91
+ it('push with --files', async () => {
92
+ const mockConfig = { ...ConfigFixture, files: ['./resouces/1.md', './resouces/2.md'] } as any;
93
+
94
+ (fs.statSync as jest.Mock).mockImplementation(() => {
95
+ return { isDirectory: () => false, size: 10 };
96
+ });
97
+
98
+ await handlePush(
99
+ {
100
+ upsert: true,
101
+ api: 'spec.json',
102
+ destination: '@org/my-api@1.0.0',
103
+ public: true,
104
+ files: ['./resouces/1.md', './resouces/2.md'],
105
+ },
106
+ mockConfig
107
+ );
108
+
109
+ expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({
110
+ filePaths: ['filePath', 'filePath', 'filePath'],
111
+ isUpsert: true,
112
+ isPublic: true,
113
+ name: 'my-api',
114
+ organizationId: 'org',
115
+ rootFilePath: 'filePath',
116
+ version: '1.0.0',
117
+ });
118
+ expect(redoclyClient.registryApi.prepareFileUpload).toBeCalledTimes(3);
119
+ });
120
+
121
+ it('push should fail if organization not provided', async () => {
122
+ await handlePush(
123
+ {
124
+ upsert: true,
125
+ api: 'spec.json',
126
+ destination: 'test@v1',
127
+ branchName: 'test',
128
+ public: true,
129
+ 'job-id': '123',
130
+ 'batch-size': 2,
131
+ },
132
+ ConfigFixture as any
133
+ );
134
+
135
+ expect(exitWithError).toBeCalledTimes(1);
136
+ expect(exitWithError).toBeCalledWith(
137
+ `No organization provided, please use --organization option or specify the 'organization' field in the config file.`
138
+ );
139
+ });
140
+
141
+ it('push should work with organization in config', async () => {
142
+ const mockConfig = { ...ConfigFixture, organization: 'test_org' } as any;
143
+ await handlePush(
144
+ {
145
+ upsert: true,
146
+ api: 'spec.json',
147
+ destination: 'my-api@1.0.0',
148
+ branchName: 'test',
149
+ public: true,
150
+ 'job-id': '123',
151
+ 'batch-size': 2,
152
+ },
153
+ mockConfig
154
+ );
155
+
156
+ expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
157
+ expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({
158
+ branch: 'test',
159
+ filePaths: ['filePath'],
160
+ isUpsert: true,
161
+ isPublic: true,
162
+ name: 'my-api',
163
+ organizationId: 'test_org',
164
+ rootFilePath: 'filePath',
165
+ version: '1.0.0',
166
+ batchId: '123',
167
+ batchSize: 2,
168
+ });
169
+ });
170
+
171
+ it('push should work if destination not provided but api in config is provided', async () => {
172
+ const mockConfig = {
173
+ ...ConfigFixture,
174
+ organization: 'test_org',
175
+ apis: { 'my-api@1.0.0': { root: 'path' } },
176
+ } as any;
177
+
178
+ await handlePush(
179
+ {
180
+ upsert: true,
181
+ branchName: 'test',
182
+ public: true,
183
+ 'job-id': '123',
184
+ 'batch-size': 2,
185
+ },
186
+ mockConfig
187
+ );
188
+
189
+ expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
190
+ });
191
+
192
+ it('push should fail if apis not provided', async () => {
193
+ const mockConfig = { organization: 'test_org', apis: {} } as any;
194
+
195
+ await handlePush(
196
+ {
197
+ upsert: true,
198
+ branchName: 'test',
199
+ public: true,
200
+ 'job-id': '123',
201
+ 'batch-size': 2,
202
+ },
203
+ mockConfig
204
+ );
205
+
206
+ expect(exitWithError).toBeCalledTimes(1);
207
+ expect(exitWithError).toHaveBeenLastCalledWith(
208
+ 'Api not found. Please make sure you have provided the correct data in the config file.'
209
+ );
210
+ });
211
+
212
+ it('push should fail if destination not provided', async () => {
213
+ const mockConfig = { organization: 'test_org', apis: {} } as any;
214
+
215
+ await handlePush(
216
+ {
217
+ upsert: true,
218
+ api: 'api.yaml',
219
+ branchName: 'test',
220
+ public: true,
221
+ 'job-id': '123',
222
+ 'batch-size': 2,
223
+ },
224
+ mockConfig
225
+ );
226
+
227
+ expect(exitWithError).toBeCalledTimes(1);
228
+ expect(exitWithError).toHaveBeenLastCalledWith(
229
+ 'No destination provided, please use --destination option to provide destination.'
230
+ );
231
+ });
232
+
233
+ it('push should fail if destination format is not valid', async () => {
234
+ const mockConfig = { organization: 'test_org', apis: {} } as any;
235
+
236
+ await handlePush(
237
+ {
238
+ upsert: true,
239
+ destination: 'name/v1',
240
+ branchName: 'test',
241
+ public: true,
242
+ 'job-id': '123',
243
+ 'batch-size': 2,
244
+ },
245
+ mockConfig
246
+ );
247
+
248
+ expect(exitWithError).toHaveBeenCalledWith(
249
+ `Destination argument value is not valid, please use the right format: ${yellow(
250
+ '<api-name@api-version>'
251
+ )}`
252
+ );
253
+ });
254
+
255
+ it('push should work and encode name with spaces', async () => {
256
+ const encodeURIComponentSpy = jest.spyOn(global, 'encodeURIComponent');
257
+
258
+ const mockConfig = {
259
+ ...ConfigFixture,
260
+ organization: 'test_org',
261
+ apis: { 'my test api@v1': { root: 'path' } },
262
+ } as any;
263
+
264
+ await handlePush(
265
+ {
266
+ upsert: true,
267
+ destination: 'my test api@v1',
268
+ branchName: 'test',
269
+ public: true,
270
+ 'job-id': '123',
271
+ 'batch-size': 2,
272
+ },
273
+ mockConfig
274
+ );
275
+
276
+ expect(encodeURIComponentSpy).toHaveReturnedWith('my%20test%20api');
277
+ expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
278
+ });
279
+ });
280
+
281
+ describe('transformPush', () => {
282
+ it('should adapt the existing syntax', () => {
283
+ const cb = jest.fn();
284
+ transformPush(cb)(
285
+ {
286
+ api: 'openapi.yaml',
287
+ maybeDestination: '@testing_org/main@v1',
288
+ },
289
+ {} as any
290
+ );
291
+ expect(cb).toBeCalledWith(
292
+ {
293
+ api: 'openapi.yaml',
294
+ destination: '@testing_org/main@v1',
295
+ },
296
+ {}
297
+ );
298
+ });
299
+ it('should adapt the existing syntax (including branchName)', () => {
300
+ const cb = jest.fn();
301
+ transformPush(cb)(
302
+ {
303
+ api: 'openapi.yaml',
304
+ maybeDestination: '@testing_org/main@v1',
305
+ maybeBranchName: 'other',
306
+ },
307
+ {} as any
308
+ );
309
+ expect(cb).toBeCalledWith(
310
+ {
311
+ api: 'openapi.yaml',
312
+ destination: '@testing_org/main@v1',
313
+ branchName: 'other',
314
+ },
315
+ {}
316
+ );
317
+ });
318
+ it('should use --branch option firstly', () => {
319
+ const cb = jest.fn();
320
+ transformPush(cb)(
321
+ {
322
+ api: 'openapi.yaml',
323
+ maybeDestination: '@testing_org/main@v1',
324
+ maybeBranchName: 'other',
325
+ branch: 'priority-branch',
326
+ },
327
+ {} as any
328
+ );
329
+ expect(cb).toBeCalledWith(
330
+ {
331
+ api: 'openapi.yaml',
332
+ destination: '@testing_org/main@v1',
333
+ branchName: 'priority-branch',
334
+ },
335
+ {}
336
+ );
337
+ });
338
+ it('should work for a destination only', () => {
339
+ const cb = jest.fn();
340
+ transformPush(cb)(
341
+ {
342
+ api: '@testing_org/main@v1',
343
+ },
344
+ {} as any
345
+ );
346
+ expect(cb).toBeCalledWith(
347
+ {
348
+ destination: '@testing_org/main@v1',
349
+ },
350
+ {}
351
+ );
352
+ });
353
+ it('should work for a api only', () => {
354
+ const cb = jest.fn();
355
+ transformPush(cb)(
356
+ {
357
+ api: 'test.yaml',
358
+ },
359
+ {} as any
360
+ );
361
+ expect(cb).toBeCalledWith(
362
+ {
363
+ api: 'test.yaml',
364
+ },
365
+ {}
366
+ );
367
+ });
368
+ it('should accept aliases for the old syntax', () => {
369
+ const cb = jest.fn();
370
+ transformPush(cb)(
371
+ {
372
+ api: 'alias',
373
+ maybeDestination: '@testing_org/main@v1',
374
+ },
375
+ {} as any
376
+ );
377
+ expect(cb).toBeCalledWith(
378
+ {
379
+ destination: '@testing_org/main@v1',
380
+ api: 'alias',
381
+ },
382
+ {}
383
+ );
384
+ });
385
+ it('should use --job-id option firstly', () => {
386
+ const cb = jest.fn();
387
+ transformPush(cb)(
388
+ {
389
+ 'batch-id': 'b-123',
390
+ 'job-id': 'j-123',
391
+ api: 'test',
392
+ maybeDestination: 'main@v1',
393
+ branch: 'test',
394
+ destination: 'main@v1',
395
+ },
396
+ {} as any
397
+ );
398
+ expect(cb).toBeCalledWith(
399
+ {
400
+ 'job-id': 'j-123',
401
+ api: 'test',
402
+ branchName: 'test',
403
+ destination: 'main@v1',
404
+ },
405
+ {}
406
+ );
407
+ });
408
+ it('should accept no arguments at all', () => {
409
+ const cb = jest.fn();
410
+ transformPush(cb)({}, {} as any);
411
+ expect(cb).toBeCalledWith({}, {});
412
+ });
413
+ });
414
+
415
+ describe('getDestinationProps', () => {
416
+ it('should get valid destination props for the full destination syntax', () => {
417
+ expect(getDestinationProps('@testing_org/main@v1', 'org-from-config')).toEqual({
418
+ organizationId: 'testing_org',
419
+ name: 'main',
420
+ version: 'v1',
421
+ });
422
+ });
423
+ it('should fallback the organizationId from a config for the short destination syntax', () => {
424
+ expect(getDestinationProps('main@v1', 'org-from-config')).toEqual({
425
+ organizationId: 'org-from-config',
426
+ name: 'main',
427
+ version: 'v1',
428
+ });
429
+ });
430
+ it('should fallback the organizationId from a config if no destination provided', () => {
431
+ expect(getDestinationProps(undefined, 'org-from-config')).toEqual({
432
+ organizationId: 'org-from-config',
433
+ });
434
+ });
435
+ it('should return empty organizationId if there is no one found', () => {
436
+ expect(getDestinationProps('main@v1', undefined)).toEqual({
437
+ organizationId: undefined,
438
+ name: 'main',
439
+ version: 'v1',
440
+ });
441
+ });
442
+ it('should return organizationId from destination string', () => {
443
+ expect(getDestinationProps('@test-org/main@main-v1', undefined)).toEqual({
444
+ organizationId: 'test-org',
445
+ name: 'main',
446
+ version: 'main-v1',
447
+ });
448
+ });
449
+
450
+ it('should return organizationId, version and empty name from destination string', () => {
451
+ expect(getDestinationProps('@test_org/@main_v1', undefined)).toEqual({
452
+ organizationId: 'test_org',
453
+ name: '',
454
+ version: 'main_v1',
455
+ });
456
+ });
457
+
458
+ it('should validate organizationId with space and version with dot', () => {
459
+ expect(getDestinationProps('@test org/simple_name@main.v1', undefined)).toEqual({
460
+ organizationId: 'test org',
461
+ name: 'simple_name',
462
+ version: 'main.v1',
463
+ });
464
+ });
465
+
466
+ it('should not work with "@" in destination name', () => {
467
+ expect(getDestinationProps('@test org/simple@name@main.v1', undefined)).toEqual({
468
+ organizationId: undefined,
469
+ name: undefined,
470
+ version: undefined,
471
+ });
472
+ });
473
+ });
474
+
475
+ describe('getApiRoot', () => {
476
+ let config: Config = {
477
+ apis: {
478
+ 'main@v1': {
479
+ root: 'openapi.yaml',
480
+ },
481
+ main: {
482
+ root: 'latest.yaml',
483
+ },
484
+ },
485
+ } as unknown as Config;
486
+ it('should resolve the correct api for a valid name & version', () => {
487
+ expect(getApiRoot({ name: 'main', version: 'v1', config })).toEqual('openapi.yaml');
488
+ });
489
+ it('should resolve the latest version of api if there is no matching version', () => {
490
+ expect(getApiRoot({ name: 'main', version: 'latest', config })).toEqual('latest.yaml');
491
+ });
492
+ });
@@ -0,0 +1,35 @@
1
+ import fetchWithTimeout from '../fetch-with-timeout';
2
+ import nodeFetch from 'node-fetch';
3
+
4
+ jest.mock('node-fetch');
5
+
6
+ describe('fetchWithTimeout', () => {
7
+ afterEach(() => {
8
+ jest.clearAllMocks();
9
+ });
10
+
11
+ it('should use bare node-fetch if AbortController is not available', async () => {
12
+ // @ts-ignore
13
+ global.AbortController = undefined;
14
+ // @ts-ignore
15
+ global.setTimeout = jest.fn();
16
+ await fetchWithTimeout('url', { method: 'GET' });
17
+
18
+ expect(nodeFetch).toHaveBeenCalledWith('url', { method: 'GET' });
19
+
20
+ expect(global.setTimeout).toHaveBeenCalledTimes(0);
21
+ });
22
+
23
+ it('should call node-fetch with signal if AbortController is available', async () => {
24
+ global.AbortController = jest.fn().mockImplementation(() => ({ signal: 'something' }));
25
+ // @ts-ignore
26
+ global.setTimeout = jest.fn();
27
+
28
+ global.clearTimeout = jest.fn();
29
+ await fetchWithTimeout('url');
30
+
31
+ expect(global.setTimeout).toHaveBeenCalledTimes(1);
32
+ expect(nodeFetch).toHaveBeenCalledWith('url', { signal: 'something' });
33
+ expect(global.clearTimeout).toHaveBeenCalledTimes(1);
34
+ });
35
+ });
@@ -0,0 +1,21 @@
1
+ export const ConfigFixture = {
2
+ configFile: null,
3
+ styleguide: {
4
+ addIgnore: jest.fn(),
5
+ skipRules: jest.fn(),
6
+ skipPreprocessors: jest.fn(),
7
+ saveIgnore: jest.fn(),
8
+ skipDecorators: jest.fn(),
9
+ ignore: null,
10
+ decorators: {
11
+ oas2: {},
12
+ oas3_0: {},
13
+ oas3_1: {},
14
+ },
15
+ preprocessors: {
16
+ oas2: {},
17
+ oas3_0: {},
18
+ oas3_1: {},
19
+ },
20
+ },
21
+ };
File without changes
File without changes
File without changes