@redocly/cli 1.22.1 → 1.23.0

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 (51) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/__tests__/commands/bundle.test.js +110 -1
  3. package/lib/__tests__/fetch-with-timeout.test.js +29 -5
  4. package/lib/__tests__/utils.test.js +54 -32
  5. package/lib/cms/api/__tests__/api.client.test.js +17 -9
  6. package/lib/cms/api/api-client.d.ts +26 -7
  7. package/lib/cms/api/api-client.js +103 -72
  8. package/lib/cms/commands/__tests__/push-status.test.js +1 -1
  9. package/lib/cms/commands/__tests__/push.test.js +41 -1
  10. package/lib/cms/commands/__tests__/utils.test.js +1 -1
  11. package/lib/cms/commands/push-status.d.ts +1 -1
  12. package/lib/cms/commands/push-status.js +3 -7
  13. package/lib/cms/commands/push.js +4 -4
  14. package/lib/cms/commands/utils.d.ts +3 -0
  15. package/lib/cms/commands/utils.js +8 -1
  16. package/lib/commands/bundle.d.ts +1 -1
  17. package/lib/commands/bundle.js +9 -9
  18. package/lib/commands/eject.d.ts +1 -1
  19. package/lib/commands/eject.js +1 -1
  20. package/lib/commands/preview-project/index.js +1 -1
  21. package/lib/index.js +1 -2
  22. package/lib/types.d.ts +1 -0
  23. package/lib/utils/__mocks__/miscellaneous.d.ts +1 -0
  24. package/lib/utils/__mocks__/miscellaneous.js +2 -1
  25. package/lib/utils/fetch-with-timeout.d.ts +6 -1
  26. package/lib/utils/fetch-with-timeout.js +16 -14
  27. package/lib/utils/miscellaneous.d.ts +4 -1
  28. package/lib/utils/miscellaneous.js +24 -29
  29. package/lib/utils/update-version-notifier.js +8 -4
  30. package/package.json +2 -2
  31. package/src/__tests__/commands/bundle.test.ts +131 -4
  32. package/src/__tests__/fetch-with-timeout.test.ts +36 -6
  33. package/src/__tests__/utils.test.ts +58 -33
  34. package/src/cms/api/__tests__/api.client.test.ts +20 -11
  35. package/src/cms/api/api-client.ts +158 -91
  36. package/src/cms/commands/__tests__/push-status.test.ts +1 -1
  37. package/src/cms/commands/__tests__/push.test.ts +49 -2
  38. package/src/cms/commands/__tests__/utils.test.ts +1 -1
  39. package/src/cms/commands/push-status.ts +5 -9
  40. package/src/cms/commands/push.ts +5 -6
  41. package/src/cms/commands/utils.ts +15 -1
  42. package/src/commands/bundle.ts +14 -12
  43. package/src/commands/eject.ts +2 -2
  44. package/src/commands/preview-project/index.ts +1 -1
  45. package/src/index.ts +1 -2
  46. package/src/types.ts +1 -0
  47. package/src/utils/__mocks__/miscellaneous.ts +1 -0
  48. package/src/utils/fetch-with-timeout.ts +23 -14
  49. package/src/utils/miscellaneous.ts +32 -37
  50. package/src/utils/update-version-notifier.ts +11 -5
  51. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,8 @@
1
- import fetch from 'node-fetch';
2
1
  import * as FormData from 'form-data';
3
- import { getProxyAgent } from '@redocly/openapi-core';
4
- import fetchWithTimeout from '../../utils/fetch-with-timeout';
2
+ import fetchWithTimeout, {
3
+ type FetchWithTimeoutOptions,
4
+ DEFAULT_FETCH_TIMEOUT,
5
+ } from '../../utils/fetch-with-timeout';
5
6
 
6
7
  import type { Response } from 'node-fetch';
7
8
  import type { ReadStream } from 'fs';
@@ -12,41 +13,76 @@ import type {
12
13
  UpsertRemoteResponse,
13
14
  } from './types';
14
15
 
15
- class RemotesApiClient {
16
- constructor(private readonly domain: string, private readonly apiKey: string) {}
16
+ export class ReuniteApiError extends Error {
17
+ constructor(message: string, public status: number) {
18
+ super(message);
19
+ }
20
+ }
17
21
 
18
- private async getParsedResponse<T>(response: Response): Promise<T> {
22
+ class ReuniteBaseApiClient {
23
+ constructor(protected version: string, protected command: string) {}
24
+
25
+ protected async getParsedResponse<T>(response: Response): Promise<T> {
19
26
  const responseBody = await response.json();
20
27
 
21
28
  if (response.ok) {
22
29
  return responseBody as T;
23
30
  }
24
31
 
25
- throw new Error(responseBody.title || response.statusText);
32
+ throw new ReuniteApiError(
33
+ `${responseBody.title || response.statusText || 'Unknown error'}.`,
34
+ response.status
35
+ );
26
36
  }
27
37
 
28
- async getDefaultBranch(organizationId: string, projectId: string) {
29
- const response = await fetchWithTimeout(
30
- `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/source`,
31
- {
32
- method: 'GET',
33
- headers: {
34
- 'Content-Type': 'application/json',
35
- Authorization: `Bearer ${this.apiKey}`,
36
- },
37
- }
38
- );
38
+ protected request(url: string, options: FetchWithTimeoutOptions) {
39
+ const headers = {
40
+ ...options.headers,
41
+ 'user-agent': `redocly-cli/${this.version.trim()} ${this.command}`,
42
+ };
39
43
 
40
- if (!response) {
41
- throw new Error(`Failed to get default branch.`);
42
- }
44
+ return fetchWithTimeout(url, {
45
+ ...options,
46
+ headers,
47
+ });
48
+ }
49
+ }
43
50
 
51
+ class RemotesApiClient extends ReuniteBaseApiClient {
52
+ constructor(
53
+ private readonly domain: string,
54
+ private readonly apiKey: string,
55
+ version: string,
56
+ command: string
57
+ ) {
58
+ super(version, command);
59
+ }
60
+
61
+ async getDefaultBranch(organizationId: string, projectId: string) {
44
62
  try {
63
+ const response = await this.request(
64
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/source`,
65
+ {
66
+ timeout: DEFAULT_FETCH_TIMEOUT,
67
+ method: 'GET',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ Authorization: `Bearer ${this.apiKey}`,
71
+ },
72
+ }
73
+ );
74
+
45
75
  const source = await this.getParsedResponse<ProjectSourceResponse>(response);
46
76
 
47
77
  return source.branchName;
48
78
  } catch (err) {
49
- throw new Error(`Failed to fetch default branch: ${err.message || 'Unknown error'}`);
79
+ const message = `Failed to fetch default branch. ${err.message}`;
80
+
81
+ if (err instanceof ReuniteApiError) {
82
+ throw new ReuniteApiError(message, err.status);
83
+ }
84
+
85
+ throw new Error(message);
50
86
  }
51
87
  }
52
88
 
@@ -58,31 +94,34 @@ class RemotesApiClient {
58
94
  mountBranchName: string;
59
95
  }
60
96
  ): Promise<UpsertRemoteResponse> {
61
- const response = await fetchWithTimeout(
62
- `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes`,
63
- {
64
- method: 'POST',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- Authorization: `Bearer ${this.apiKey}`,
68
- },
69
- body: JSON.stringify({
70
- mountPath: remote.mountPath,
71
- mountBranchName: remote.mountBranchName,
72
- type: 'CICD',
73
- autoMerge: true,
74
- }),
75
- }
76
- );
77
-
78
- if (!response) {
79
- throw new Error(`Failed to upsert.`);
80
- }
81
-
82
97
  try {
98
+ const response = await this.request(
99
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes`,
100
+ {
101
+ timeout: DEFAULT_FETCH_TIMEOUT,
102
+ method: 'POST',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ Authorization: `Bearer ${this.apiKey}`,
106
+ },
107
+ body: JSON.stringify({
108
+ mountPath: remote.mountPath,
109
+ mountBranchName: remote.mountBranchName,
110
+ type: 'CICD',
111
+ autoMerge: true,
112
+ }),
113
+ }
114
+ );
115
+
83
116
  return await this.getParsedResponse<UpsertRemoteResponse>(response);
84
117
  } catch (err) {
85
- throw new Error(`Failed to upsert remote: ${err.message || 'Unknown error'}`);
118
+ const message = `Failed to upsert remote. ${err.message}`;
119
+
120
+ if (err instanceof ReuniteApiError) {
121
+ throw new ReuniteApiError(message, err.status);
122
+ }
123
+
124
+ throw new Error(message);
86
125
  }
87
126
  }
88
127
 
@@ -110,46 +149,61 @@ class RemotesApiClient {
110
149
  }
111
150
 
112
151
  payload.isMainBranch && formData.append('isMainBranch', 'true');
113
-
114
- const response = await fetch(
115
- `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes`,
116
- {
117
- method: 'POST',
118
- headers: {
119
- Authorization: `Bearer ${this.apiKey}`,
120
- },
121
- body: formData,
122
- agent: getProxyAgent(),
123
- }
124
- );
125
-
126
152
  try {
153
+ const response = await this.request(
154
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes`,
155
+ {
156
+ method: 'POST',
157
+ headers: {
158
+ Authorization: `Bearer ${this.apiKey}`,
159
+ },
160
+ body: formData,
161
+ }
162
+ );
163
+
127
164
  return await this.getParsedResponse<PushResponse>(response);
128
165
  } catch (err) {
129
- throw new Error(`Failed to push: ${err.message || 'Unknown error'}`);
130
- }
131
- }
166
+ const message = `Failed to push. ${err.message}`;
132
167
 
133
- async getRemotesList(organizationId: string, projectId: string, mountPath: string) {
134
- const response = await fetchWithTimeout(
135
- `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes?filter=mountPath:/${mountPath}/`,
136
- {
137
- method: 'GET',
138
- headers: {
139
- 'Content-Type': 'application/json',
140
- Authorization: `Bearer ${this.apiKey}`,
141
- },
168
+ if (err instanceof ReuniteApiError) {
169
+ throw new ReuniteApiError(message, err.status);
142
170
  }
143
- );
144
171
 
145
- if (!response) {
146
- throw new Error(`Failed to get remotes list.`);
172
+ throw new Error(message);
147
173
  }
174
+ }
148
175
 
176
+ async getRemotesList({
177
+ organizationId,
178
+ projectId,
179
+ mountPath,
180
+ }: {
181
+ organizationId: string;
182
+ projectId: string;
183
+ mountPath: string;
184
+ }) {
149
185
  try {
186
+ const response = await this.request(
187
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/remotes?filter=mountPath:/${mountPath}/`,
188
+ {
189
+ timeout: DEFAULT_FETCH_TIMEOUT,
190
+ method: 'GET',
191
+ headers: {
192
+ 'Content-Type': 'application/json',
193
+ Authorization: `Bearer ${this.apiKey}`,
194
+ },
195
+ }
196
+ );
197
+
150
198
  return await this.getParsedResponse<ListRemotesResponse>(response);
151
199
  } catch (err) {
152
- throw new Error(`Failed to get remote list: ${err.message || 'Unknown error'}`);
200
+ const message = `Failed to get remote list. ${err.message}`;
201
+
202
+ if (err instanceof ReuniteApiError) {
203
+ throw new ReuniteApiError(message, err.status);
204
+ }
205
+
206
+ throw new Error(message);
153
207
  }
154
208
  }
155
209
 
@@ -162,25 +216,28 @@ class RemotesApiClient {
162
216
  projectId: string;
163
217
  pushId: string;
164
218
  }) {
165
- const response = await fetchWithTimeout(
166
- `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes/${pushId}`,
167
- {
168
- method: 'GET',
169
- headers: {
170
- 'Content-Type': 'application/json',
171
- Authorization: `Bearer ${this.apiKey}`,
172
- },
173
- }
174
- );
175
-
176
- if (!response) {
177
- throw new Error(`Failed to get push status.`);
178
- }
179
-
180
219
  try {
220
+ const response = await this.request(
221
+ `${this.domain}/api/orgs/${organizationId}/projects/${projectId}/pushes/${pushId}`,
222
+ {
223
+ timeout: DEFAULT_FETCH_TIMEOUT,
224
+ method: 'GET',
225
+ headers: {
226
+ 'Content-Type': 'application/json',
227
+ Authorization: `Bearer ${this.apiKey}`,
228
+ },
229
+ }
230
+ );
231
+
181
232
  return await this.getParsedResponse<PushResponse>(response);
182
233
  } catch (err) {
183
- throw new Error(`Failed to get push status: ${err.message || 'Unknown error'}`);
234
+ const message = `Failed to get push status. ${err.message}`;
235
+
236
+ if (err instanceof ReuniteApiError) {
237
+ throw new ReuniteApiError(message, err.status);
238
+ }
239
+
240
+ throw new Error(message);
184
241
  }
185
242
  }
186
243
  }
@@ -188,8 +245,18 @@ class RemotesApiClient {
188
245
  export class ReuniteApiClient {
189
246
  remotes: RemotesApiClient;
190
247
 
191
- constructor(public domain: string, private readonly apiKey: string) {
192
- this.remotes = new RemotesApiClient(this.domain, this.apiKey);
248
+ constructor({
249
+ domain,
250
+ apiKey,
251
+ version,
252
+ command,
253
+ }: {
254
+ domain: string;
255
+ apiKey: string;
256
+ version: string;
257
+ command: 'push' | 'push-status';
258
+ }) {
259
+ this.remotes = new RemotesApiClient(domain, apiKey, version, command);
193
260
  }
194
261
  }
195
262
 
@@ -644,7 +644,7 @@ describe('handlePushStatus()', () => {
644
644
  version: 'cli-version',
645
645
  })
646
646
  ).rejects.toThrowErrorMatchingInlineSnapshot(`
647
- "✗ Failed to get push status. Reason: Timeout exceeded
647
+ "✗ Failed to get push status. Reason: Timeout exceeded.
648
648
  "
649
649
  `);
650
650
  });
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { handlePush } from '../push';
4
- import { ReuniteApiClient } from '../../api';
4
+ import { ReuniteApiClient, ReuniteApiError } from '../../api';
5
5
 
6
6
  const remotes = {
7
7
  push: jest.fn(),
@@ -332,6 +332,53 @@ describe('handlePush()', () => {
332
332
  version: 'cli-version',
333
333
  });
334
334
 
335
- expect(ReuniteApiClient).toBeCalledWith('test-domain-from-env', 'test-api-key');
335
+ expect(ReuniteApiClient).toBeCalledWith({
336
+ domain: 'test-domain-from-env',
337
+ apiKey: 'test-api-key',
338
+ version: 'cli-version',
339
+ command: 'push',
340
+ });
341
+ });
342
+
343
+ it('should print error message', async () => {
344
+ const mockConfig = { apis: {} } as any;
345
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
346
+
347
+ remotes.push.mockRestore();
348
+ remotes.push.mockRejectedValueOnce(new ReuniteApiError('Deprecated.', 412));
349
+
350
+ fsStatSyncSpy.mockReturnValueOnce({
351
+ isDirectory() {
352
+ return false;
353
+ },
354
+ } as any);
355
+
356
+ pathResolveSpy.mockImplementationOnce((p) => p);
357
+ pathRelativeSpy.mockImplementationOnce((_, p) => p);
358
+ pathDirnameSpy.mockImplementation((_: string) => '.');
359
+
360
+ expect(
361
+ handlePush({
362
+ argv: {
363
+ domain: 'test-domain',
364
+ 'mount-path': 'test-mount-path',
365
+ organization: 'test-org',
366
+ project: 'test-project',
367
+ branch: 'test-branch',
368
+ namespace: 'test-namespace',
369
+ repository: 'test-repository',
370
+ 'commit-sha': 'test-commit-sha',
371
+ 'commit-url': 'test-commit-url',
372
+ 'default-branch': 'test-branch',
373
+ 'created-at': 'test-created-at',
374
+ author: 'TestAuthor <test-author@mail.com>',
375
+ message: 'Test message',
376
+ files: ['test-file'],
377
+ 'max-execution-time': 10,
378
+ },
379
+ config: mockConfig,
380
+ version: 'cli-version',
381
+ })
382
+ ).rejects.toThrow('✗ File upload failed. Reason: Deprecated.');
336
383
  });
337
384
  });
@@ -32,7 +32,7 @@ describe('retryUntilConditionMet()', () => {
32
32
  retryIntervalMs: 100,
33
33
  retryTimeoutMs: 1000,
34
34
  })
35
- ).rejects.toThrow('Timeout exceeded');
35
+ ).rejects.toThrow('Timeout exceeded.');
36
36
  });
37
37
 
38
38
  it('should call "onConditionNotMet" and "onRetry" callbacks', async () => {
@@ -4,7 +4,7 @@ import { Spinner } from '../../utils/spinner';
4
4
  import { DeploymentError } from '../utils';
5
5
  import { ReuniteApiClient, getApiKeys, getDomain } from '../api';
6
6
  import { capitalize } from '../../utils/js-utils';
7
- import { retryUntilConditionMet } from './utils';
7
+ import { handleReuniteError, retryUntilConditionMet } from './utils';
8
8
 
9
9
  import type { OutputFormat } from '@redocly/openapi-core';
10
10
  import type { CommandArgs } from '../../wrapper';
@@ -41,7 +41,8 @@ export interface PushStatusSummary {
41
41
  export async function handlePushStatus({
42
42
  argv,
43
43
  config,
44
- }: CommandArgs<PushStatusOptions>): Promise<PushStatusSummary | undefined> {
44
+ version,
45
+ }: CommandArgs<PushStatusOptions>): Promise<PushStatusSummary | void> {
45
46
  const startedAt = performance.now();
46
47
  const spinner = new Spinner();
47
48
 
@@ -67,7 +68,7 @@ export async function handlePushStatus({
67
68
 
68
69
  try {
69
70
  const apiKey = getApiKeys(domain);
70
- const client = new ReuniteApiClient(domain, apiKey);
71
+ const client = new ReuniteApiClient({ domain, apiKey, version, command: 'push-status' });
71
72
 
72
73
  let pushResponse: PushResponse;
73
74
 
@@ -178,12 +179,7 @@ export async function handlePushStatus({
178
179
  } catch (err) {
179
180
  spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
180
181
 
181
- const message =
182
- err instanceof DeploymentError
183
- ? err.message
184
- : `✗ Failed to get push status. Reason: ${err.message}\n`;
185
- exitWithError(message);
186
- return;
182
+ handleReuniteError('✗ Failed to get push status.', err);
187
183
  } finally {
188
184
  spinner.stop(); // Spinner can block process exit, so we need to stop it explicitly.
189
185
  }
@@ -3,9 +3,10 @@ import * as path from 'path';
3
3
  import { slash } from '@redocly/openapi-core';
4
4
  import { pluralize } from '@redocly/openapi-core/lib/utils';
5
5
  import { green, yellow } from 'colorette';
6
- import { exitWithError, HandledError, printExecutionTime } from '../../utils/miscellaneous';
6
+ import { exitWithError, printExecutionTime } from '../../utils/miscellaneous';
7
7
  import { handlePushStatus } from './push-status';
8
8
  import { ReuniteApiClient, getDomain, getApiKeys } from '../api';
9
+ import { handleReuniteError } from './utils';
9
10
 
10
11
  import type { OutputFormat } from '@redocly/openapi-core';
11
12
  import type { CommandArgs } from '../../wrapper';
@@ -52,7 +53,7 @@ export async function handlePush({
52
53
  const orgId = organization || config.organization;
53
54
 
54
55
  if (!argv.message || !argv.author || !argv.branch) {
55
- exitWithError('Error: message, author and branch are required for push to the CMS.');
56
+ exitWithError('Error: message, author and branch are required for push to the Reunite.');
56
57
  }
57
58
 
58
59
  if (!orgId) {
@@ -85,7 +86,7 @@ export async function handlePush({
85
86
  return printExecutionTime('push', startedAt, `No files to upload`);
86
87
  }
87
88
 
88
- const client = new ReuniteApiClient(domain, apiKey);
89
+ const client = new ReuniteApiClient({ domain, apiKey, version, command: 'push' });
89
90
  const projectDefaultBranch = await client.remotes.getDefaultBranch(orgId, projectId);
90
91
  const remote = await client.remotes.upsert(orgId, projectId, {
91
92
  mountBranchName: projectDefaultBranch,
@@ -158,9 +159,7 @@ export async function handlePush({
158
159
  pushId: id,
159
160
  };
160
161
  } catch (err) {
161
- const message =
162
- err instanceof HandledError ? '' : `✗ File upload failed. Reason: ${err.message}`;
163
- exitWithError(message);
162
+ handleReuniteError('✗ File upload failed.', err);
164
163
  }
165
164
  }
166
165
 
@@ -1,4 +1,8 @@
1
1
  import { pause } from '@redocly/openapi-core';
2
+ import { DeploymentError } from '../utils';
3
+ import { exitWithError } from '../../utils/miscellaneous';
4
+
5
+ import type { ReuniteApiError } from '../api';
2
6
 
3
7
  /**
4
8
  * This function retries an operation until a condition is met or a timeout is exceeded.
@@ -39,7 +43,7 @@ export async function retryUntilConditionMet<T>({
39
43
  if (condition(result)) {
40
44
  return result;
41
45
  } else if (Date.now() - startTime > retryTimeoutMs) {
42
- throw new Error('Timeout exceeded');
46
+ throw new Error('Timeout exceeded.');
43
47
  } else {
44
48
  onConditionNotMet?.(result);
45
49
  await pause(retryIntervalMs);
@@ -50,3 +54,13 @@ export async function retryUntilConditionMet<T>({
50
54
 
51
55
  return attempt();
52
56
  }
57
+
58
+ export function handleReuniteError(
59
+ message: string,
60
+ error: ReuniteApiError | DeploymentError | Error
61
+ ) {
62
+ const errorMessage =
63
+ error instanceof DeploymentError ? error.message : `${message} Reason: ${error.message}\n`;
64
+
65
+ return exitWithError(errorMessage);
66
+ }
@@ -21,7 +21,7 @@ export type BundleOptions = {
21
21
  apis?: string[];
22
22
  extends?: string[];
23
23
  output?: string;
24
- ext: OutputExtensions;
24
+ ext?: OutputExtensions;
25
25
  dereferenced?: boolean;
26
26
  force?: boolean;
27
27
  metafile?: string;
@@ -45,7 +45,7 @@ export async function handleBundle({
45
45
 
46
46
  checkForDeprecatedOptions(argv, deprecatedOptions);
47
47
 
48
- for (const { path, alias } of apis) {
48
+ for (const { path, alias, output } of apis) {
49
49
  try {
50
50
  const startedAt = performance.now();
51
51
  const resolvedConfig = getMergedConfig(config, alias);
@@ -70,19 +70,19 @@ export async function handleBundle({
70
70
  });
71
71
 
72
72
  const fileTotals = getTotals(problems);
73
- const { outputFile, ext } = getOutputFileName(path, apis.length, argv.output, argv.ext);
73
+ const { outputFile, ext } = getOutputFileName(path, output || argv.output, argv.ext);
74
74
 
75
75
  if (fileTotals.errors === 0 || argv.force) {
76
- if (!argv.output) {
77
- const output = dumpBundle(
76
+ if (!outputFile) {
77
+ const bundled = dumpBundle(
78
78
  sortTopLevelKeysForOas(result.parsed),
79
79
  argv.ext || 'yaml',
80
80
  argv.dereferenced
81
81
  );
82
- process.stdout.write(output);
82
+ process.stdout.write(bundled);
83
83
  } else {
84
- const output = dumpBundle(sortTopLevelKeysForOas(result.parsed), ext, argv.dereferenced);
85
- saveBundle(outputFile, output);
84
+ const bundled = dumpBundle(sortTopLevelKeysForOas(result.parsed), ext, argv.dereferenced);
85
+ saveBundle(outputFile, bundled);
86
86
  }
87
87
  }
88
88
 
@@ -111,9 +111,9 @@ export async function handleBundle({
111
111
  if (fileTotals.errors > 0) {
112
112
  if (argv.force) {
113
113
  process.stderr.write(
114
- `❓ Created a bundle for ${blue(path)} at ${blue(outputFile)} with errors ${green(
115
- elapsed
116
- )}.\n${yellow('Errors ignored because of --force')}.\n`
114
+ `❓ Created a bundle for ${blue(path)} at ${blue(
115
+ outputFile || 'stdout'
116
+ )} with errors ${green(elapsed)}.\n${yellow('Errors ignored because of --force')}.\n`
117
117
  );
118
118
  } else {
119
119
  process.stderr.write(
@@ -124,7 +124,9 @@ export async function handleBundle({
124
124
  }
125
125
  } else {
126
126
  process.stderr.write(
127
- `📦 Created a bundle for ${blue(path)} at ${blue(outputFile)} ${green(elapsed)}.\n`
127
+ `📦 Created a bundle for ${blue(path)} at ${blue(outputFile || 'stdout')} ${green(
128
+ elapsed
129
+ )}.\n`
128
130
  );
129
131
  }
130
132
 
@@ -5,7 +5,7 @@ import type { VerifyConfigOptions } from '../types';
5
5
 
6
6
  export type EjectOptions = {
7
7
  type: 'component';
8
- path: string;
8
+ path?: string;
9
9
  'project-dir'?: string;
10
10
  force: boolean;
11
11
  } & VerifyConfigOptions;
@@ -20,7 +20,7 @@ export const handleEject = async ({ argv }: CommandArgs<EjectOptions>) => {
20
20
  '@redocly/realm',
21
21
  'eject',
22
22
  `${argv.type}`,
23
- `${argv.path}`,
23
+ `${argv.path ?? ''}`,
24
24
  `-d=${argv['project-dir']}`,
25
25
  argv.force ? `--force=${argv.force}` : '',
26
26
  ],
@@ -26,7 +26,7 @@ export const previewProject = async ({ argv }: CommandArgs<PreviewProjectOptions
26
26
 
27
27
  spawn(
28
28
  npxExecutableName,
29
- ['-y', packageName, 'develop', `--plan=${plan}`, `--port=${port || 4000}`],
29
+ ['-y', packageName, 'preview', `--plan=${plan}`, `--port=${port || 4000}`],
30
30
  {
31
31
  stdio: 'inherit',
32
32
  cwd: projectDir,
package/src/index.ts CHANGED
@@ -817,7 +817,7 @@ yargs
817
817
  }
818
818
  )
819
819
  .command(
820
- 'eject <type> <path>',
820
+ 'eject <type> [path]',
821
821
  'Helper function to eject project elements for customization.',
822
822
  (yargs) =>
823
823
  yargs
@@ -830,7 +830,6 @@ yargs
830
830
  .positional('path', {
831
831
  description: 'Filepath to a component or filepath with glob pattern.',
832
832
  type: 'string',
833
- demandOption: true,
834
833
  })
835
834
  .options({
836
835
  'project-dir': {
package/src/types.ts CHANGED
@@ -23,6 +23,7 @@ export type Totals = {
23
23
  export type Entrypoint = {
24
24
  path: string;
25
25
  alias?: string;
26
+ output?: string;
26
27
  };
27
28
  export const outputExtensions = ['json', 'yaml', 'yml'] as ReadonlyArray<BundleOutputFormat>;
28
29
  export type OutputExtensions = 'json' | 'yaml' | 'yml' | undefined;
@@ -20,3 +20,4 @@ export const sortTopLevelKeysForOas = jest.fn((document) => document);
20
20
  export const getAndValidateFileExtension = jest.fn((fileName: string) => fileName.split('.').pop());
21
21
  export const writeToFileByExtension = jest.fn();
22
22
  export const checkForDeprecatedOptions = jest.fn();
23
+ export const saveBundle = jest.fn();