Skip to content

Commit

Permalink
Remove node-fetch.
Browse files Browse the repository at this point in the history
  • Loading branch information
iclanton committed Dec 5, 2024
1 parent ea91a68 commit cbfb7ee
Show file tree
Hide file tree
Showing 14 changed files with 302 additions and 452 deletions.
10 changes: 10 additions & 0 deletions common/changes/@microsoft/rush/main_2024-12-05-18-29.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Remove the dependency on node-fetch.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions common/config/subspaces/build-tests-subspace/repo-state.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "48a2b5871414899c79b9a7bb240a9a3f567c13be",
"pnpmShrinkwrapHash": "73c8cd88b3abb997315c56b4150a61c52c4d1bc5",
"preferredVersionsHash": "ce857ea0536b894ec8f346aaea08cfd85a5af648",
"packageJsonInjectedDependenciesHash": "342b446dfdfe891b4ca092fed7987732d713d46f"
"packageJsonInjectedDependenciesHash": "bda7d42cd7f07d7d90db82b92a1b35c597ddc99e"
}
9 changes: 0 additions & 9 deletions common/config/subspaces/default/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions libraries/rush-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"@rushstack/stream-collator": "workspace:*",
"@rushstack/terminal": "workspace:*",
"@rushstack/ts-command-line": "workspace:*",
"@types/node-fetch": "2.6.2",
"@yarnpkg/lockfile": "~1.0.2",
"builtin-modules": "~3.1.0",
"cli-table": "~0.3.1",
Expand All @@ -53,7 +52,6 @@
"ignore": "~5.1.6",
"inquirer": "~7.3.3",
"js-yaml": "~3.13.1",
"node-fetch": "2.6.7",
"npm-check": "~6.0.1",
"npm-package-arg": "~6.1.0",
"read-package-tree": "~5.1.5",
Expand Down
10 changes: 5 additions & 5 deletions libraries/rush-lib/src/logic/base/BaseInstallManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type * as fetch from 'node-fetch';
import * as os from 'os';
import * as path from 'path';
import * as crypto from 'crypto';
Expand Down Expand Up @@ -48,7 +47,7 @@ import { ShrinkwrapFileFactory } from '../ShrinkwrapFileFactory';
import { Utilities } from '../../utilities/Utilities';
import { InstallHelpers } from '../installManager/InstallHelpers';
import * as PolicyValidator from '../policy/PolicyValidator';
import type { WebClient as WebClientType, WebClientResponse } from '../../utilities/WebClient';
import type { WebClient as WebClientType, IWebClientResponse } from '../../utilities/WebClient';
import { SetupPackageRegistry } from '../setup/SetupPackageRegistry';
import { PnpmfileConfiguration } from '../pnpm/PnpmfileConfiguration';
import type { IInstallManagerOptions } from './BaseInstallManagerTypes';
Expand Down Expand Up @@ -1066,12 +1065,13 @@ ${gitLfsHookHandling}
webClient.userAgent = `pnpm/? npm/? node/${process.version} ${os.platform()} ${os.arch()}`;
webClient.accept = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*';

const response: WebClientResponse = await webClient.fetchAsync(queryUrl);
const response: IWebClientResponse = await webClient.fetchAsync(queryUrl);
if (!response.ok) {
throw new Error('Failed to query');
}

const data: { versions: { [version: string]: { dist: { tarball: string } } } } = await response.json();
const data: { versions: { [version: string]: { dist: { tarball: string } } } } =
await response.getJsonAsync();
let url: string;
try {
if (!data.versions[Rush.version]) {
Expand All @@ -1090,7 +1090,7 @@ ${gitLfsHookHandling}
// Make sure the tarball wasn't deleted from the CDN
webClient.accept = '*/*';

const response2: fetch.Response = await webClient.fetchAsync(url);
const response2: IWebClientResponse = await webClient.fetchAsync(url);

if (!response2.ok) {
if (response2.status === 404) {
Expand Down
6 changes: 3 additions & 3 deletions libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { PrintUtilities, Colorize, ConsoleTerminalProvider, Terminal } from '@ru
import type { RushConfiguration } from '../../api/RushConfiguration';
import { Utilities } from '../../utilities/Utilities';
import { type IArtifactoryPackageRegistryJson, ArtifactoryConfiguration } from './ArtifactoryConfiguration';
import type { WebClient as WebClientType, WebClientResponse } from '../../utilities/WebClient';
import type { WebClient as WebClientType, IWebClientResponse } from '../../utilities/WebClient';
import { TerminalInput } from './TerminalInput';

interface IArtifactoryCustomizableMessages {
Expand Down Expand Up @@ -300,7 +300,7 @@ export class SetupPackageRegistry {
// our token.
queryUrl += `auth/.npm`;

let response: WebClientResponse;
let response: IWebClientResponse;
try {
response = await webClient.fetchAsync(queryUrl);
} catch (e) {
Expand All @@ -324,7 +324,7 @@ export class SetupPackageRegistry {
// //your-company.jfrog.io/your-artifacts/api/npm/npm-private/:[email protected]
// //your-company.jfrog.io/your-artifacts/api/npm/npm-private/:[email protected]
// //your-company.jfrog.io/your-artifacts/api/npm/npm-private/:always-auth=true
const responseText: string = await response.text();
const responseText: string = await response.getTextAsync();
const responseLines: string[] = Text.convertToLf(responseText).trim().split('\n');
if (responseLines.length < 2 || !responseLines[0].startsWith('@.npm:')) {
throw new Error('Unexpected response from Artifactory');
Expand Down
143 changes: 111 additions & 32 deletions libraries/rush-lib/src/utilities/WebClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,34 @@

import * as os from 'os';
import * as process from 'process';
import * as fetch from 'node-fetch';
import type * as http from 'http';
import { request as httpRequest, type IncomingMessage } from 'node:http';
import { request as httpsRequest, type RequestOptions } from 'node:https';
import type { Socket } from 'node:net';
import { Import } from '@rushstack/node-core-library';

const createHttpsProxyAgent: typeof import('https-proxy-agent') = Import.lazy('https-proxy-agent', require);

/**
* For use with {@link WebClient}.
*/
export type WebClientResponse = fetch.Response;

/**
* For use with {@link WebClient}.
*/
export type WebClientHeaders = fetch.Headers;
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const WebClientHeaders: typeof fetch.Headers = fetch.Headers;
export interface IWebClientResponse {
ok: boolean;
status: number;
statusText?: string;
redirected: boolean;
getTextAsync: () => Promise<string>;
getJsonAsync: <TJson>() => Promise<TJson>;
getBufferAsync: () => Promise<Buffer>;
}

/**
* For use with {@link WebClient}.
*/
export interface IWebFetchOptionsBase {
timeoutMs?: number;
headers?: WebClientHeaders | Record<string, string>;
redirect?: fetch.RequestInit['redirect'];
headers?: Record<string, string>;
redirect?: 'follow' | 'error' | 'manual';
}

/**
Expand All @@ -53,49 +56,125 @@ export enum WebClientProxy {
Detect,
Fiddler
}
export interface IRequestOptions extends RequestOptions, Pick<IFetchOptionsWithBody, 'body' | 'redirect'> {}

export type FetchFn = (
url: string,
options: IRequestOptions,
isRedirect?: boolean
) => Promise<IWebClientResponse>;

const makeRequestAsync: FetchFn = async (
url: string,
options: IRequestOptions,
redirected: boolean = false
) => {
const { body, redirect } = options;

return await new Promise(
(resolve: (result: IWebClientResponse) => void, reject: (error: Error) => void) => {
const parsedUrl: URL = typeof url === 'string' ? new URL(url) : url;
const requestFunction: typeof httpRequest | typeof httpsRequest =
parsedUrl.protocol === 'https:' ? httpsRequest : httpRequest;

requestFunction(url, options, (response: IncomingMessage) => {
const responseBuffers: (Buffer | Uint8Array)[] = [];
response.on('data', (chunk: string | Buffer | Uint8Array) => {
responseBuffers.push(Buffer.from(chunk));
});
response.on('end', () => {
// Handle retries by calling the method recursively with the redirect URL
const statusCode: number | undefined = response.statusCode;
if (statusCode === 301 || statusCode === 302) {
switch (redirect) {
case 'follow': {
const redirectUrl: string | string[] | undefined = response.headers.location;
if (redirectUrl) {
makeRequestAsync(redirectUrl, options, true).then(resolve).catch(reject);
} else {
reject(
new Error(`Received status code ${response.statusCode} with no location header: ${url}`)
);
}

break;
}
case 'error':
reject(new Error(`Received status code ${response.statusCode}: ${url}`));
return;
}
}

const responseData: Buffer = Buffer.concat(responseBuffers);
// const result: WebClientResponse = { response, responseData };
const status: number = response.statusCode || 0;
const statusText: string | undefined = response.statusMessage;
const result: IWebClientResponse = {
ok: status >= 200 && status < 300,
status,
statusText,
redirected,
getTextAsync: async () => responseData.toString(),
getJsonAsync: async () => JSON.parse(responseData.toString()),
getBufferAsync: async () => responseData
};
resolve(result);
});
})
.on('socket', (socket: Socket) => {
socket.on('error', (error: Error) => {
reject(error);
});
})
.on('error', (error: Error) => {
reject(error);
})
.end(body);
}
);
};

export const AUTHORIZATION_HEADER_NAME: 'Authorization' = 'Authorization';
const ACCEPT_HEADER_NAME: 'accept' = 'accept';
const USER_AGENT_HEADER_NAME: 'user-agent' = 'user-agent';

/**
* A helper for issuing HTTP requests.
*/
export class WebClient {
private static _requestFn: typeof fetch.default = fetch.default;
private static _requestFn: FetchFn = makeRequestAsync;

public readonly standardHeaders: fetch.Headers = new fetch.Headers();
public readonly standardHeaders: Record<string, string> = {};

public accept: string | undefined = '*/*';
public userAgent: string | undefined = `rush node/${process.version} ${os.platform()} ${os.arch()}`;

public proxy: WebClientProxy = WebClientProxy.Detect;

public static mockRequestFn(fn: typeof fetch.default): void {
public static mockRequestFn(fn: FetchFn): void {
WebClient._requestFn = fn;
}

public static resetMockRequestFn(): void {
WebClient._requestFn = fetch.default;
WebClient._requestFn = makeRequestAsync;
}

public static mergeHeaders(target: fetch.Headers, source: fetch.Headers | Record<string, string>): void {
const iterator: Iterable<[string, string]> =
'entries' in source && typeof source.entries === 'function' ? source.entries() : Object.entries(source);

for (const [name, value] of iterator) {
target.set(name, value);
public static mergeHeaders(target: Record<string, string>, source: Record<string, string>): void {
for (const [name, value] of Object.entries(source)) {
target[name] = value;
}
}

public addBasicAuthHeader(userName: string, password: string): void {
this.standardHeaders.set(
'Authorization',
'Basic ' + Buffer.from(userName + ':' + password).toString('base64')
);
this.standardHeaders[AUTHORIZATION_HEADER_NAME] =
'Basic ' + Buffer.from(userName + ':' + password).toString('base64');
}

public async fetchAsync(
url: string,
options?: IGetFetchOptions | IFetchOptionsWithBody
): Promise<WebClientResponse> {
const headers: fetch.Headers = new fetch.Headers();
): Promise<IWebClientResponse> {
const headers: Record<string, string> = {};

WebClient.mergeHeaders(headers, this.standardHeaders);

Expand All @@ -104,11 +183,11 @@ export class WebClient {
}

if (this.userAgent) {
headers.set('user-agent', this.userAgent);
headers[USER_AGENT_HEADER_NAME] = this.userAgent;
}

if (this.accept) {
headers.set('accept', this.accept);
headers[ACCEPT_HEADER_NAME] = this.accept;
}

let proxyUrl: string = '';
Expand Down Expand Up @@ -136,10 +215,10 @@ export class WebClient {
}

const timeoutMs: number = options?.timeoutMs !== undefined ? options.timeoutMs : 15 * 1000; // 15 seconds
const requestInit: fetch.RequestInit = {
const requestInit: IRequestOptions = {
method: options?.verb,
headers: headers,
agent: agent,
headers,
agent,
timeout: timeoutMs,
redirect: options?.redirect
};
Expand Down
Loading

0 comments on commit cbfb7ee

Please sign in to comment.