Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"@types/node": "^22.8.1",
"prebundle": "1.6.4",
"socket.io-client": "4.8.1",
"axios": "1.14.0",
"typescript": "^5.9.2",
"zod": "^3.25.76"
},
Expand Down
1 change: 0 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"@rsdoctor/client": "workspace:*",
"cac": "^7.0.0",
"typescript": "^5.9.2",
"axios": "1.14.0",
"picocolors": "^1.1.1"
},
"peerDependencies": {
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/fetch-http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Fetch } from '@rsdoctor/utils/common';

/** Thin wrapper so tests can `rs.mock('./fetch-http')` without depending on `Fetch.getFetch` internals. */
export function fetchWithTimeout(
url: string,
options: Parameters<typeof Fetch.fetchWithTimeout>[1],
) {
return Fetch.fetchWithTimeout(url, options);
}
63 changes: 63 additions & 0 deletions packages/cli/src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { afterEach, describe, expect, it, rs } from '@rstest/core';

const { fetchWithTimeoutMock } = rs.hoisted(() => ({
fetchWithTimeoutMock: rs.fn(),
}));

rs.mock('./fetch-http', () => ({
fetchWithTimeout: fetchWithTimeoutMock,
}));

import { fetchText, loadJSON } from './utils';

describe('cli utils', () => {
afterEach(() => {
fetchWithTimeoutMock.mockReset();
});

it('fetchText() returns response text for 2xx', async () => {
fetchWithTimeoutMock.mockResolvedValue({
ok: true,
text: async () => 'plain text',
} as Response);

const result = await fetchText('https://example.com/file.txt');

expect(result).toBe('plain text');
expect(fetchWithTimeoutMock).toBeCalledTimes(1);
expect(fetchWithTimeoutMock).toBeCalledWith(
'https://example.com/file.txt',
expect.objectContaining({
timeout: 60000,
headers: {
Accept: 'text/plain; charset=utf-8',
'Accept-Encoding': 'gzip,deflate,compress',
},
}),
);
});

it('fetchText() throws when response is non-2xx', async () => {
fetchWithTimeoutMock.mockRejectedValue(
new Error('Request failed with status 404'),
);

await expect(fetchText('https://example.com/missing.txt')).rejects.toThrow(
'Request failed with status 404',
);
});

it('loadJSON() parses remote json text', async () => {
fetchWithTimeoutMock.mockResolvedValue({
ok: true,
text: async () => '{"id":7,"name":"remote"}',
} as Response);

const data = await loadJSON<{ id: number; name: string }>(
'https://example.com/data.json',
process.cwd(),
);

expect(data).toStrictEqual({ id: 7, name: 'remote' });
});
});
10 changes: 5 additions & 5 deletions packages/cli/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import axios from 'axios';
import path from 'path';
import fs from 'node:fs';
import { Ora } from 'ora';
import { Command } from './types';
import { Common } from '@rsdoctor/types';
import { Url } from '@rsdoctor/utils/common';
import { fetchWithTimeout } from './fetch-http';

export function enhanceCommand<CMD extends string, Options, Result>(
fn: Command<CMD, Options, Result>,
Expand All @@ -16,14 +16,14 @@ export function enhanceCommand<CMD extends string, Options, Result>(
}

export async function fetchText(url: string) {
const { data } = await axios.get(url, {
const res = await fetchWithTimeout(url, {
timeout: 60000,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
Accept: 'text/plain; charset=utf-8',
'Accept-Encoding': 'gzip,deflate,compress',
},
});
return data;
return res.text();
}

export async function readFile(url: string, cwd: string) {
Expand All @@ -39,7 +39,7 @@ export async function loadJSON<T extends Common.PlainObject>(
if (Url.isUrl(uri)) {
const data = await fetchText(uri);

return data;
return JSON.parse(data) as T;
}

const file = await readFile(uri, cwd);
Expand Down
2 changes: 1 addition & 1 deletion packages/client/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export default defineConfig(({ env }) => {
vender: {
chunks: 'all',
name: 'vender',
test: /node_modules\/(acorn|lodash|i18next|socket.io-*|axios|remark-*)/,
test: /node_modules\/(acorn|lodash|i18next|socket.io-*|remark-*)/,
maxSize: 1000000,
minSize: 200000,
},
Expand Down
1 change: 0 additions & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"@rsdoctor/utils": "workspace:*",
"ansi-to-react": "6.2.6",
"antd": "5.19.1",
"axios": "1.14.0",
"clsx": "^2.1.1",
"dayjs": "1.11.20",
"echarts": "^5.6.0",
Expand Down
10 changes: 4 additions & 6 deletions packages/components/src/components/Alert/change.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import {
Space,
Typography,
} from 'antd';
import axios from 'axios';
import React, { useState } from 'react';
import { useRuleIndexNavigate } from '../../utils';
import { postServerAPI, useRuleIndexNavigate } from '../../utils';
import { DiffViewer } from '../base';
import { CodeOpener } from '../Opener';
import { TextDrawer } from '../TextDrawer';
Expand All @@ -31,10 +30,9 @@ const CodeChangeDrawerContent: React.FC<CodeChangeAlertProps & FixedProps> = ({
const { file, id } = data;
const { path, line, isFixed, actual, expected } = file;
// const [isFixed, setIsFixed] = useState(file.isFixed ?? false);
const applyFix = () => {
axios.post(SDK.ServerAPI.API.ApplyErrorFix, { id }).then(() => {
setIsFixed(true);
});
const applyFix = async () => {
await postServerAPI(SDK.ServerAPI.API.ApplyErrorFix, { id: Number(id) });
setIsFixed(true);
};

const FixButton = () => {
Expand Down
61 changes: 61 additions & 0 deletions packages/components/src/utils/request.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { afterEach, describe, expect, it, rs } from '@rstest/core';
import { fetchJSONByUrl, postServerAPI } from './request';

describe('request utils', () => {
const originalFetch = globalThis.fetch;
const originalNodeEnv = process.env.NODE_ENV;

afterEach(() => {
globalThis.fetch = originalFetch;
process.env.NODE_ENV = originalNodeEnv;
});

it('fetchJSONByUrl() parses json text payload', async () => {
const fetchMock = rs.fn().mockResolvedValue({
ok: true,
text: async () => '{"name":"rsdoctor"}',
});
globalThis.fetch = fetchMock as typeof fetch;
process.env.NODE_ENV = 'production';

const data = await fetchJSONByUrl('https://example.com/manifest.json');

expect(data).toStrictEqual({ name: 'rsdoctor' });
expect(fetchMock).toBeCalledTimes(1);
});

it('fetchJSONByUrl() throws for non-2xx response', async () => {
const fetchMock = rs.fn().mockResolvedValue({
ok: false,
status: 500,
});
globalThis.fetch = fetchMock as typeof fetch;
process.env.NODE_ENV = 'production';

await expect(
fetchJSONByUrl('https://example.com/manifest.json'),
).rejects.toThrow('Request failed with status 500');
});

it('postServerAPI() sends json body and parses response json', async () => {
const fetchMock = rs.fn().mockResolvedValue({
ok: true,
json: async () => ({ ok: true }),
});
globalThis.fetch = fetchMock as typeof fetch;
process.env.NODE_ENV = 'production';

const result = await (postServerAPI as any)('/api/demo', { id: 1 });
const [url, init] = fetchMock.mock.calls[0];

expect(url).toContain('/api/demo?_t=');
expect(init).toMatchObject({
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: 1 }),
});
expect(result).toStrictEqual({ ok: true });
});
});
75 changes: 37 additions & 38 deletions packages/components/src/utils/request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import axios from 'axios';
import { Manifest, SDK } from '@rsdoctor/types';
import { Manifest as ManifestMethod, Url } from '@rsdoctor/utils/common';
import { Fetch, Manifest as ManifestMethod, Url } from '@rsdoctor/utils/common';
import { APILoaderMode4Dev } from '../constants';
import { getManifestUrlFromUrlQuery } from './url';
import { getAPILoaderModeFromStorage } from './storage';
Expand All @@ -9,11 +8,30 @@ function random() {
return `${Date.now()}${Math.floor(Math.random() * 10000)}`;
}

function resolveRequestUrl(url: string): string {
if (
process.env.NODE_ENV === 'development' &&
getAPILoaderModeFromStorage() === APILoaderMode4Dev.Local &&
url.startsWith('/')
) {
const nextUrl =
url === manifestUrlForDev ? SDK.ServerAPI.API.Manifest : url;
const currentUrl = new URL(location.href);
currentUrl.port = String(process.env.LOCAL_CLI_PORT!);
return `${currentUrl.origin}${nextUrl}`;
}

return url;
}

async function requestText(url: string, timeout: number) {
const res = await Fetch.fetchWithTimeout(resolveRequestUrl(url), { timeout });
return res.text();
}

export async function fetchShardingFile(url: string): Promise<string> {
if (Url.isUrl(url)) {
return axios
.get(url, { timeout: 999999, responseType: 'text' })
.then((e) => e.data);
return requestText(url, 999999);
}
// json string
return url;
Expand All @@ -27,8 +45,7 @@ export async function loadManifestByUrl(url: string) {
}

export async function fetchJSONByUrl(url: string) {
const res = await axios.get(url, { timeout: 30000 });
let json: unknown = res.data;
let json: unknown = await requestText(url, 30000);

if (typeof json === 'string') {
const trimmed = json.trim();
Expand Down Expand Up @@ -129,41 +146,23 @@ export async function fetchManifest(url = getManifestUrl()) {
return res;
}

// test for cli
if (process.env.NODE_ENV === 'development') {
if (getAPILoaderModeFromStorage() === APILoaderMode4Dev.Local) {
axios.interceptors.request.use((c) => {
c.withCredentials = false;
if (c.url?.startsWith('/')) {
if (c.url === manifestUrlForDev) {
c.url = SDK.ServerAPI.API.Manifest;
}
const url = new URL(location.href);
url.port = String(process.env.LOCAL_CLI_PORT!);
return {
...c,
url: `${url.origin}${c.url}`,
};
}

return c;
});
}
}

export async function postServerAPI<
T extends SDK.ServerAPI.API,
B extends
SDK.ServerAPI.InferRequestBodyType<T> = SDK.ServerAPI.InferRequestBodyType<T>,
R extends
SDK.ServerAPI.InferResponseType<T> = SDK.ServerAPI.InferResponseType<T>,
B extends SDK.ServerAPI.InferRequestBodyType<T> =
SDK.ServerAPI.InferRequestBodyType<T>,
R extends SDK.ServerAPI.InferResponseType<T> =
SDK.ServerAPI.InferResponseType<T>,
>(...args: B extends void ? [api: T] : [api: T, body: B]): Promise<R> {
const [api, body] = args;
const timeout = process.env.NODE_ENV === 'development' ? 10000 : 60000;
const { data } = await axios.post<SDK.ServerAPI.InferResponseType<T>>(
`${api}?_t=${random()}`,
body,
{ timeout },
const res = await Fetch.fetchWithTimeout(
resolveRequestUrl(`${api}?_t=${random()}`),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
timeout,
},
);
return data as R;
return (await res.json()) as R;
}
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
"source-map": "^0.7.6"
},
"devDependencies": {
"axios": "1.14.0",
"@rspack/core": "2.0.0-canary-20260116",
"@scripts/test-helper": "workspace:*",
"@types/fs-extra": "^11.0.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/prebundle.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @type {import('prebundle').Config} */
export default {
dependencies: ['axios'],
dependencies: [],
exclude: [
'@rsdoctor/client',
'@rsdoctor/graph',
Expand Down
Loading