@redocly/cli 1.27.1 → 1.28.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @redocly/cli
2
2
 
3
+ ## 1.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Switched to using native `fetch` API instead of `node-fetch` dependency, improving performance and reducing bundle size.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated @redocly/openapi-core to v1.28.0.
12
+
13
+ ## 1.27.2
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated the `sideNavStyle` configuration schema to include the `path-only` option.
18
+ - Updated @redocly/openapi-core to v1.27.2.
19
+
3
20
  ## 1.27.1
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -28,7 +28,7 @@ Then you can use it as `redocly [command] [options]`, for example:
28
28
  redocly lint path-to-root-file.yaml
29
29
  ```
30
30
 
31
- The minimum required versions of Node.js and NPM are 14.19.0 and 7.0.0 respectively.
31
+ The minimum required versions of Node.js and NPM are 18.17.0 and 10.8.2 respectively.
32
32
 
33
33
  ### Docker
34
34
 
@@ -4,26 +4,59 @@ const openapi_core_1 = require("@redocly/openapi-core");
4
4
  const push_1 = require("../../commands/push");
5
5
  const login_1 = require("../../commands/login");
6
6
  const config_1 = require("../fixtures/config");
7
- jest.mock('fs');
8
- jest.mock('node-fetch', () => ({
9
- default: jest.fn(() => ({
10
- ok: true,
11
- json: jest.fn().mockResolvedValue({}),
12
- })),
7
+ const node_stream_1 = require("node:stream");
8
+ // Mock fs operations
9
+ jest.mock('fs', () => ({
10
+ ...jest.requireActual('fs'),
11
+ createReadStream: () => {
12
+ const readable = new node_stream_1.Readable();
13
+ readable.push('test data');
14
+ readable.push(null);
15
+ return readable;
16
+ },
17
+ statSync: () => ({ size: 9 }),
18
+ readFileSync: () => Buffer.from('test data'),
19
+ existsSync: () => false,
20
+ readdirSync: () => [],
13
21
  }));
22
+ openapi_core_1.getMergedConfig.mockImplementation((config) => config);
23
+ // Mock OpenAPI core
14
24
  jest.mock('@redocly/openapi-core');
15
25
  jest.mock('../../commands/login');
16
26
  jest.mock('../../utils/miscellaneous');
17
- openapi_core_1.getMergedConfig.mockImplementation((config) => config);
18
27
  const mockPromptClientToken = login_1.promptClientToken;
19
28
  describe('push-with-region', () => {
20
29
  const redoclyClient = require('@redocly/openapi-core').__redoclyClient;
21
30
  redoclyClient.isAuthorizedWithRedoclyByRegion = jest.fn().mockResolvedValue(false);
31
+ const originalFetch = fetch;
22
32
  beforeAll(() => {
33
+ // Mock global fetch
34
+ global.fetch = jest.fn(() => Promise.resolve({
35
+ ok: true,
36
+ status: 200,
37
+ json: () => Promise.resolve({}),
38
+ headers: new Headers(),
39
+ statusText: 'OK',
40
+ redirected: false,
41
+ type: 'default',
42
+ url: '',
43
+ clone: () => ({}),
44
+ body: new ReadableStream(),
45
+ bodyUsed: false,
46
+ arrayBuffer: async () => new ArrayBuffer(0),
47
+ blob: async () => new Blob(),
48
+ formData: async () => new FormData(),
49
+ text: async () => '',
50
+ }));
51
+ });
52
+ afterAll(() => {
53
+ global.fetch = originalFetch;
54
+ });
55
+ beforeEach(() => {
23
56
  jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
24
57
  });
25
58
  it('should call login with default domain when region is US', async () => {
26
- redoclyClient.domain = 'redoc.ly';
59
+ redoclyClient.domain = 'redocly.com';
27
60
  await (0, push_1.handlePush)({
28
61
  argv: {
29
62
  upsert: true,
@@ -39,6 +72,8 @@ describe('push-with-region', () => {
39
72
  });
40
73
  it('should call login with EU domain when region is EU', async () => {
41
74
  redoclyClient.domain = 'eu.redocly.com';
75
+ // Update config for EU region
76
+ const euConfig = { ...config_1.ConfigFixture, region: 'eu' };
42
77
  await (0, push_1.handlePush)({
43
78
  argv: {
44
79
  upsert: true,
@@ -46,7 +81,7 @@ describe('push-with-region', () => {
46
81
  destination: '@org/my-api@1.0.0',
47
82
  branchName: 'test',
48
83
  },
49
- config: config_1.ConfigFixture,
84
+ config: euConfig,
50
85
  version: 'cli-version',
51
86
  });
52
87
  expect(mockPromptClientToken).toBeCalledTimes(1);
@@ -6,21 +6,54 @@ const miscellaneous_1 = require("../../utils/miscellaneous");
6
6
  const push_1 = require("../../commands/push");
7
7
  const config_1 = require("../fixtures/config");
8
8
  const colorette_1 = require("colorette");
9
- jest.mock('fs');
10
- jest.mock('node-fetch', () => ({
11
- default: jest.fn(() => ({
12
- ok: true,
13
- json: jest.fn().mockResolvedValue({}),
14
- })),
9
+ const node_stream_1 = require("node:stream");
10
+ // Mock fs operations
11
+ jest.mock('fs', () => ({
12
+ ...jest.requireActual('fs'),
13
+ createReadStream: jest.fn(() => {
14
+ const readable = new node_stream_1.Readable();
15
+ readable.push('test data');
16
+ readable.push(null);
17
+ return readable;
18
+ }),
19
+ statSync: jest.fn(() => ({ isDirectory: () => false, size: 10 })),
20
+ readFileSync: jest.fn(() => Buffer.from('test data')),
21
+ existsSync: jest.fn(() => false),
22
+ readdirSync: jest.fn(() => []),
15
23
  }));
16
24
  jest.mock('@redocly/openapi-core');
17
25
  jest.mock('../../utils/miscellaneous');
26
+ // Mock fetch
27
+ const mockFetch = jest.fn(() => Promise.resolve({
28
+ ok: true,
29
+ status: 200,
30
+ json: () => Promise.resolve({}),
31
+ headers: new Headers(),
32
+ statusText: 'OK',
33
+ redirected: false,
34
+ type: 'default',
35
+ url: '',
36
+ clone: () => ({}),
37
+ body: null,
38
+ bodyUsed: false,
39
+ arrayBuffer: async () => new ArrayBuffer(0),
40
+ blob: async () => new Blob(),
41
+ formData: async () => new FormData(),
42
+ text: async () => '',
43
+ }));
44
+ const originalFetch = global.fetch;
18
45
  openapi_core_1.getMergedConfig.mockImplementation((config) => config);
19
46
  describe('push', () => {
20
47
  const redoclyClient = require('@redocly/openapi-core').__redoclyClient;
48
+ beforeAll(() => {
49
+ global.fetch = mockFetch;
50
+ });
21
51
  beforeEach(() => {
22
52
  jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
23
53
  });
54
+ afterAll(() => {
55
+ global.fetch = originalFetch;
56
+ });
24
57
  it('pushes definition', async () => {
25
58
  await (0, push_1.handlePush)({
26
59
  argv: {
@@ -2,11 +2,31 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const abort_controller_1 = require("abort-controller");
4
4
  const fetch_with_timeout_1 = require("../utils/fetch-with-timeout");
5
- const node_fetch_1 = require("node-fetch");
6
5
  const openapi_core_1 = require("@redocly/openapi-core");
7
6
  const https_proxy_agent_1 = require("https-proxy-agent");
8
- jest.mock('node-fetch');
9
7
  jest.mock('@redocly/openapi-core');
8
+ const signalInstance = new abort_controller_1.default().signal;
9
+ const mockFetch = jest.fn(() => Promise.resolve({
10
+ ok: true,
11
+ status: 200,
12
+ json: () => Promise.resolve({}),
13
+ headers: new Headers(),
14
+ statusText: 'OK',
15
+ redirected: false,
16
+ type: 'default',
17
+ url: '',
18
+ clone: () => ({}),
19
+ body: null,
20
+ bodyUsed: false,
21
+ arrayBuffer: async () => new ArrayBuffer(0),
22
+ blob: async () => new Blob(),
23
+ formData: async () => new FormData(),
24
+ text: async () => '',
25
+ signal: signalInstance,
26
+ dispatcher: undefined,
27
+ }));
28
+ const originalFetch = global.fetch;
29
+ global.fetch = mockFetch;
10
30
  describe('fetchWithTimeout', () => {
11
31
  beforeAll(() => {
12
32
  // @ts-ignore
@@ -16,29 +36,29 @@ describe('fetchWithTimeout', () => {
16
36
  beforeEach(() => {
17
37
  openapi_core_1.getProxyAgent.mockReturnValueOnce(undefined);
18
38
  });
19
- afterEach(() => {
20
- jest.clearAllMocks();
39
+ afterAll(() => {
40
+ global.fetch = originalFetch;
21
41
  });
22
- it('should call node-fetch with signal', async () => {
42
+ it('should call fetch with signal', async () => {
23
43
  await (0, fetch_with_timeout_1.default)('url', { timeout: 1000 });
24
44
  expect(global.setTimeout).toHaveBeenCalledTimes(1);
25
- expect(node_fetch_1.default).toHaveBeenCalledWith('url', {
26
- signal: new abort_controller_1.default().signal,
27
- agent: undefined,
28
- });
45
+ expect(global.fetch).toHaveBeenCalledWith('url', expect.objectContaining({
46
+ signal: expect.any(AbortSignal),
47
+ dispatcher: undefined,
48
+ }));
29
49
  expect(global.clearTimeout).toHaveBeenCalledTimes(1);
30
50
  });
31
- it('should call node-fetch with proxy agent', async () => {
51
+ it('should call fetch with proxy agent', async () => {
32
52
  openapi_core_1.getProxyAgent.mockRestore();
33
53
  const proxyAgent = new https_proxy_agent_1.HttpsProxyAgent('http://localhost');
34
54
  openapi_core_1.getProxyAgent.mockReturnValueOnce(proxyAgent);
35
55
  await (0, fetch_with_timeout_1.default)('url');
36
- expect(node_fetch_1.default).toHaveBeenCalledWith('url', { agent: proxyAgent });
56
+ expect(global.fetch).toHaveBeenCalledWith('url', { dispatcher: proxyAgent });
37
57
  });
38
- it('should call node-fetch without signal when timeout is not passed', async () => {
58
+ it('should call fetch without signal when timeout is not passed', async () => {
39
59
  await (0, fetch_with_timeout_1.default)('url');
40
60
  expect(global.setTimeout).not.toHaveBeenCalled();
41
- expect(node_fetch_1.default).toHaveBeenCalledWith('url', { agent: undefined });
61
+ expect(global.fetch).toHaveBeenCalledWith('url', { agent: undefined });
42
62
  expect(global.clearTimeout).not.toHaveBeenCalled();
43
63
  });
44
64
  });
@@ -6,11 +6,19 @@ const wrapper_1 = require("../wrapper");
6
6
  const lint_1 = require("../commands/lint");
7
7
  const push_1 = require("../commands/push");
8
8
  const openapi_core_1 = require("@redocly/openapi-core");
9
- jest.mock('node-fetch');
9
+ const mockFetch = jest.fn();
10
+ const originalFetch = global.fetch;
10
11
  jest.mock('../utils/miscellaneous', () => ({
11
12
  sendTelemetry: jest.fn(),
12
13
  loadConfigAndHandleErrors: jest.fn(),
13
14
  }));
15
+ beforeAll(() => {
16
+ global.fetch = mockFetch;
17
+ });
18
+ afterAll(() => {
19
+ jest.resetAllMocks();
20
+ global.fetch = originalFetch;
21
+ });
14
22
  jest.mock('../commands/lint', () => ({
15
23
  handleLint: jest.fn().mockImplementation(({ collectSpecData }) => {
16
24
  collectSpecData({ openapi: '3.1.0' });
@@ -1,14 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const node_fetch_1 = require("node-fetch");
4
- const FormData = require("form-data");
5
3
  const colorette_1 = require("colorette");
6
4
  const api_client_1 = require("../api-client");
7
- jest.mock('node-fetch', () => ({
8
- default: jest.fn(),
9
- }));
5
+ const originalFetch = global.fetch;
6
+ beforeAll(() => {
7
+ // Reset fetch mock before each test
8
+ global.fetch = jest.fn();
9
+ });
10
+ afterAll(() => {
11
+ // Restore original fetch after each test
12
+ global.fetch = originalFetch;
13
+ });
10
14
  function mockFetchResponse(response) {
11
- node_fetch_1.default.mockResolvedValue(response);
15
+ global.fetch.mockResolvedValue(response);
12
16
  }
13
17
  describe('ApiClient', () => {
14
18
  const testToken = 'test-token';
@@ -31,7 +35,7 @@ describe('ApiClient', () => {
31
35
  }),
32
36
  });
33
37
  const result = await apiClient.remotes.getDefaultBranch(testOrg, testProject);
34
- expect(node_fetch_1.default).toHaveBeenCalledWith(`${testDomain}/api/orgs/${testOrg}/projects/${testProject}/source`, {
38
+ expect(global.fetch).toHaveBeenCalledWith(`${testDomain}/api/orgs/${testOrg}/projects/${testProject}/source`, {
35
39
  method: 'GET',
36
40
  headers: {
37
41
  'Content-Type': 'application/json',
@@ -89,7 +93,7 @@ describe('ApiClient', () => {
89
93
  json: jest.fn().mockResolvedValue(responseMock),
90
94
  });
91
95
  const result = await apiClient.remotes.upsert(testOrg, testProject, remotePayload);
92
- expect(node_fetch_1.default).toHaveBeenCalledWith(`${testDomain}/api/orgs/${testOrg}/projects/${testProject}/remotes`, {
96
+ expect(global.fetch).toHaveBeenCalledWith(`${testDomain}/api/orgs/${testOrg}/projects/${testProject}/remotes`, {
93
97
  method: 'POST',
94
98
  headers: {
95
99
  'Content-Type': 'application/json',
@@ -167,29 +171,29 @@ describe('ApiClient', () => {
167
171
  });
168
172
  it('should push to remote', async () => {
169
173
  let passedFormData = new FormData();
170
- node_fetch_1.default.mockImplementationOnce(async (_, options) => {
174
+ fetch.mockImplementationOnce(async (_, options) => {
171
175
  passedFormData = options.body;
172
176
  return {
173
177
  ok: true,
174
178
  json: jest.fn().mockResolvedValue(responseMock),
175
179
  };
176
180
  });
177
- const formData = new FormData();
181
+ const formData = new globalThis.FormData();
178
182
  formData.append('remoteId', testRemoteId);
179
183
  formData.append('commit[message]', pushPayload.commit.message);
180
184
  formData.append('commit[author][name]', pushPayload.commit.author.name);
181
185
  formData.append('commit[author][email]', pushPayload.commit.author.email);
182
186
  formData.append('commit[branchName]', pushPayload.commit.branchName);
183
- formData.append('files[some-file.yaml]', filesMock[0].stream);
187
+ formData.append('files[some-file.yaml]', new Blob([filesMock[0].stream]));
184
188
  const result = await apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock);
185
- expect(node_fetch_1.default).toHaveBeenCalledWith(`${testDomain}/api/orgs/${testOrg}/projects/${testProject}/pushes`, expect.objectContaining({
189
+ expect(fetch).toHaveBeenCalledWith(`${testDomain}/api/orgs/${testOrg}/projects/${testProject}/pushes`, expect.objectContaining({
186
190
  method: 'POST',
187
191
  headers: {
188
192
  Authorization: `Bearer ${testToken}`,
189
193
  'user-agent': expectedUserAgent,
190
194
  },
191
195
  }));
192
- expect(JSON.stringify(passedFormData).replace(new RegExp(passedFormData.getBoundary(), 'g'), '')).toEqual(JSON.stringify(formData).replace(new RegExp(formData.getBoundary(), 'g'), ''));
196
+ expect(passedFormData).toEqual(formData);
193
197
  expect(result).toEqual(responseMock);
194
198
  });
195
199
  it('should throw parsed error if response is not ok', async () => {
@@ -1,6 +1,6 @@
1
1
  import { type FetchWithTimeoutOptions } from '../../utils/fetch-with-timeout';
2
- import type { Response } from 'node-fetch';
3
2
  import type { ReadStream } from 'fs';
3
+ import type { Readable } from 'node:stream';
4
4
  import type { ListRemotesResponse, PushResponse, UpsertRemoteResponse } from './types';
5
5
  interface BaseApiClient {
6
6
  request(url: string, options: FetchWithTimeoutOptions): Promise<Response>;
@@ -72,4 +72,5 @@ export type PushPayload = {
72
72
  };
73
73
  isMainBranch?: boolean;
74
74
  };
75
+ export declare function streamToBuffer(stream: ReadStream | Readable): Promise<Buffer>;
75
76
  export {};
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ReuniteApi = exports.ReuniteApiError = void 0;
4
+ exports.streamToBuffer = streamToBuffer;
4
5
  const colorette_1 = require("colorette");
5
- const FormData = require("form-data");
6
6
  const fetch_with_timeout_1 = require("../../utils/fetch-with-timeout");
7
7
  class ReuniteApiError extends Error {
8
8
  constructor(message, status) {
@@ -120,7 +120,7 @@ class RemotesApi {
120
120
  }
121
121
  }
122
122
  async push(organizationId, projectId, payload, files) {
123
- const formData = new FormData();
123
+ const formData = new globalThis.FormData();
124
124
  formData.append('remoteId', payload.remoteId);
125
125
  formData.append('commit[message]', payload.commit.message);
126
126
  formData.append('commit[author][name]', payload.commit.author.name);
@@ -132,7 +132,10 @@ class RemotesApi {
132
132
  payload.commit.repository && formData.append('commit[repositoryId]', payload.commit.repository);
133
133
  payload.commit.createdAt && formData.append('commit[createdAt]', payload.commit.createdAt);
134
134
  for (const file of files) {
135
- formData.append(`files[${file.path}]`, file.stream);
135
+ const blob = Buffer.isBuffer(file.stream)
136
+ ? new Blob([file.stream])
137
+ : new Blob([await streamToBuffer(file.stream)]);
138
+ formData.append(`files[${file.path}]`, blob, file.path);
136
139
  }
137
140
  payload.isMainBranch && formData.append('isMainBranch', 'true');
138
141
  try {
@@ -223,3 +226,10 @@ class ReuniteApi {
223
226
  }
224
227
  }
225
228
  exports.ReuniteApi = ReuniteApi;
229
+ async function streamToBuffer(stream) {
230
+ const chunks = [];
231
+ for await (const chunk of stream) {
232
+ chunks.push(chunk);
233
+ }
234
+ return Buffer.concat(chunks);
235
+ }
@@ -7,7 +7,6 @@ exports.getDestinationProps = getDestinationProps;
7
7
  exports.getApiRoot = getApiRoot;
8
8
  const fs = require("fs");
9
9
  const path = require("path");
10
- const node_fetch_1 = require("node-fetch");
11
10
  const perf_hooks_1 = require("perf_hooks");
12
11
  const colorette_1 = require("colorette");
13
12
  const crypto_1 = require("crypto");
@@ -16,6 +15,7 @@ const utils_1 = require("@redocly/openapi-core/lib/utils");
16
15
  const miscellaneous_1 = require("../utils/miscellaneous");
17
16
  const login_1 = require("./login");
18
17
  const push_1 = require("../cms/commands/push");
18
+ const api_client_1 = require("../cms/api/api-client");
19
19
  const DEFAULT_VERSION = 'latest';
20
20
  exports.DESTINATION_REGEX =
21
21
  // eslint-disable-next-line no-useless-escape
@@ -279,17 +279,23 @@ function getApiRoot({ name, version, config: { apis }, }) {
279
279
  const api = apis?.[`${name}@${version}`] || (version === DEFAULT_VERSION && apis?.[name]);
280
280
  return api?.root;
281
281
  }
282
- function uploadFileToS3(url, filePathOrBuffer) {
282
+ async function uploadFileToS3(url, filePathOrBuffer) {
283
283
  const fileSizeInBytes = typeof filePathOrBuffer === 'string'
284
284
  ? fs.statSync(filePathOrBuffer).size
285
285
  : filePathOrBuffer.byteLength;
286
286
  const readStream = typeof filePathOrBuffer === 'string' ? fs.createReadStream(filePathOrBuffer) : filePathOrBuffer;
287
- return (0, node_fetch_1.default)(url, {
287
+ const requestOptions = {
288
288
  method: 'PUT',
289
289
  headers: {
290
290
  'Content-Length': fileSizeInBytes.toString(),
291
291
  },
292
- body: readStream,
293
- agent: (0, openapi_core_1.getProxyAgent)(),
294
- });
292
+ body: Buffer.isBuffer(readStream)
293
+ ? new Blob([readStream])
294
+ : new Blob([await (0, api_client_1.streamToBuffer)(readStream)]),
295
+ };
296
+ const proxyAgent = (0, openapi_core_1.getProxyAgent)();
297
+ if (proxyAgent) {
298
+ requestOptions.dispatcher = proxyAgent;
299
+ }
300
+ return fetch(url, requestOptions);
295
301
  }
@@ -1,4 +1,4 @@
1
- import type { Oas3PathItem, Referenced } from './types';
1
+ import type { Oas3PathItem, Referenced } from '@redocly/openapi-core/lib/typings/openapi';
2
2
  import type { CommandArgs } from '../../wrapper';
3
3
  import type { VerifyConfigOptions } from '../../types';
4
4
  export type SplitOptions = {
@@ -1,5 +1,5 @@
1
- import { Oas3Schema, Oas3_1Schema, Oas3Definition, Oas3_1Definition, Oas3Components, Oas3PathItem, Oas3Paths, Oas3ComponentName, Oas3_1Webhooks, Oas2Definition, Referenced } from '@redocly/openapi-core';
2
- export { Oas3_1Definition, Oas3Definition, Oas2Definition, Oas3Components, Oas3Paths, Oas3PathItem, Oas3ComponentName, Oas3_1Schema, Oas3Schema, Oas3_1Webhooks, Referenced, };
1
+ import type { Oas2Definition } from '@redocly/openapi-core';
2
+ import type { Oas3_1Definition, Oas3Definition } from '@redocly/openapi-core/lib/typings/openapi';
3
3
  export type Definition = Oas3_1Definition | Oas3Definition | Oas2Definition;
4
4
  export interface ComponentsFiles {
5
5
  [schemas: string]: any;
@@ -1,7 +1,6 @@
1
- import { type RequestInit } from 'node-fetch';
2
1
  export declare const DEFAULT_FETCH_TIMEOUT = 3000;
3
2
  export type FetchWithTimeoutOptions = RequestInit & {
4
3
  timeout?: number;
5
4
  };
6
- declare const _default: (url: string, { timeout, ...options }?: FetchWithTimeoutOptions) => Promise<import("node-fetch").Response>;
5
+ declare const _default: (url: string, { timeout, ...options }?: FetchWithTimeoutOptions) => Promise<Response>;
7
6
  export default _default;
@@ -1,25 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_FETCH_TIMEOUT = void 0;
4
- const node_fetch_1 = require("node-fetch");
5
- const abort_controller_1 = require("abort-controller");
6
4
  const openapi_core_1 = require("@redocly/openapi-core");
7
5
  exports.DEFAULT_FETCH_TIMEOUT = 3000;
8
6
  exports.default = async (url, { timeout, ...options } = {}) => {
9
7
  if (!timeout) {
10
- return (0, node_fetch_1.default)(url, {
8
+ return fetch(url, {
11
9
  ...options,
12
- agent: (0, openapi_core_1.getProxyAgent)(),
10
+ dispatcher: (0, openapi_core_1.getProxyAgent)(),
13
11
  });
14
12
  }
15
- const controller = new abort_controller_1.default();
13
+ const controller = new globalThis.AbortController();
16
14
  const timeoutId = setTimeout(() => {
17
15
  controller.abort();
18
16
  }, timeout);
19
- const res = await (0, node_fetch_1.default)(url, {
17
+ const res = await fetch(url, {
20
18
  signal: controller.signal,
21
19
  ...options,
22
- agent: (0, openapi_core_1.getProxyAgent)(),
20
+ dispatcher: (0, openapi_core_1.getProxyAgent)(),
23
21
  });
24
22
  clearTimeout(timeoutId);
25
23
  return res;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/cli",
3
- "version": "1.27.1",
3
+ "version": "1.28.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -8,8 +8,8 @@
8
8
  "redocly": "bin/cli.js"
9
9
  },
10
10
  "engines": {
11
- "node": ">=14.19.0",
12
- "npm": ">=7.0.0"
11
+ "node": ">=18.17.0",
12
+ "npm": ">=10.8.2"
13
13
  },
14
14
  "engineStrict": true,
15
15
  "scripts": {
@@ -36,7 +36,7 @@
36
36
  "Roman Hotsiy <roman@redocly.com> (https://redocly.com/)"
37
37
  ],
38
38
  "dependencies": {
39
- "@redocly/openapi-core": "1.27.1",
39
+ "@redocly/openapi-core": "1.28.0",
40
40
  "abort-controller": "^3.0.0",
41
41
  "chokidar": "^3.5.1",
42
42
  "colorette": "^1.2.0",
@@ -46,7 +46,6 @@
46
46
  "glob": "^7.1.6",
47
47
  "handlebars": "^4.7.6",
48
48
  "mobx": "^6.0.4",
49
- "node-fetch": "^2.6.1",
50
49
  "pluralize": "^8.0.0",
51
50
  "react": "^17.0.0 || ^18.2.0",
52
51
  "react-dom": "^17.0.0 || ^18.2.0",
@@ -2,32 +2,71 @@ import { getMergedConfig } from '@redocly/openapi-core';
2
2
  import { handlePush } from '../../commands/push';
3
3
  import { promptClientToken } from '../../commands/login';
4
4
  import { ConfigFixture } from '../fixtures/config';
5
+ import { Readable } from 'node:stream';
5
6
 
6
- jest.mock('fs');
7
- jest.mock('node-fetch', () => ({
8
- default: jest.fn(() => ({
9
- ok: true,
10
- json: jest.fn().mockResolvedValue({}),
11
- })),
7
+ // Mock fs operations
8
+ jest.mock('fs', () => ({
9
+ ...jest.requireActual('fs'),
10
+ createReadStream: () => {
11
+ const readable = new Readable();
12
+ readable.push('test data');
13
+ readable.push(null);
14
+ return readable;
15
+ },
16
+ statSync: () => ({ size: 9 }),
17
+ readFileSync: () => Buffer.from('test data'),
18
+ existsSync: () => false,
19
+ readdirSync: () => [],
12
20
  }));
21
+
22
+ (getMergedConfig as jest.Mock).mockImplementation((config) => config);
23
+
24
+ // Mock OpenAPI core
13
25
  jest.mock('@redocly/openapi-core');
14
26
  jest.mock('../../commands/login');
15
27
  jest.mock('../../utils/miscellaneous');
16
28
 
17
- (getMergedConfig as jest.Mock).mockImplementation((config) => config);
18
-
19
29
  const mockPromptClientToken = promptClientToken as jest.MockedFunction<typeof promptClientToken>;
20
30
 
21
31
  describe('push-with-region', () => {
22
32
  const redoclyClient = require('@redocly/openapi-core').__redoclyClient;
23
33
  redoclyClient.isAuthorizedWithRedoclyByRegion = jest.fn().mockResolvedValue(false);
24
34
 
35
+ const originalFetch = fetch;
36
+
25
37
  beforeAll(() => {
38
+ // Mock global fetch
39
+ global.fetch = jest.fn(() =>
40
+ Promise.resolve({
41
+ ok: true,
42
+ status: 200,
43
+ json: () => Promise.resolve({}),
44
+ headers: new Headers(),
45
+ statusText: 'OK',
46
+ redirected: false,
47
+ type: 'default',
48
+ url: '',
49
+ clone: () => ({} as Response),
50
+ body: new ReadableStream(),
51
+ bodyUsed: false,
52
+ arrayBuffer: async () => new ArrayBuffer(0),
53
+ blob: async () => new Blob(),
54
+ formData: async () => new FormData(),
55
+ text: async () => '',
56
+ } as Response)
57
+ );
58
+ });
59
+
60
+ afterAll(() => {
61
+ global.fetch = originalFetch;
62
+ });
63
+
64
+ beforeEach(() => {
26
65
  jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
27
66
  });
28
67
 
29
68
  it('should call login with default domain when region is US', async () => {
30
- redoclyClient.domain = 'redoc.ly';
69
+ redoclyClient.domain = 'redocly.com';
31
70
  await handlePush({
32
71
  argv: {
33
72
  upsert: true,
@@ -38,12 +77,16 @@ describe('push-with-region', () => {
38
77
  config: ConfigFixture as any,
39
78
  version: 'cli-version',
40
79
  });
80
+
41
81
  expect(mockPromptClientToken).toBeCalledTimes(1);
42
82
  expect(mockPromptClientToken).toHaveBeenCalledWith(redoclyClient.domain);
43
83
  });
44
84
 
45
85
  it('should call login with EU domain when region is EU', async () => {
46
86
  redoclyClient.domain = 'eu.redocly.com';
87
+ // Update config for EU region
88
+ const euConfig = { ...ConfigFixture, region: 'eu' };
89
+
47
90
  await handlePush({
48
91
  argv: {
49
92
  upsert: true,
@@ -51,9 +94,10 @@ describe('push-with-region', () => {
51
94
  destination: '@org/my-api@1.0.0',
52
95
  branchName: 'test',
53
96
  },
54
- config: ConfigFixture as any,
97
+ config: euConfig as any,
55
98
  version: 'cli-version',
56
99
  });
100
+
57
101
  expect(mockPromptClientToken).toBeCalledTimes(1);
58
102
  expect(mockPromptClientToken).toHaveBeenCalledWith(redoclyClient.domain);
59
103
  });