@nomicfoundation/hardhat-utils 3.0.0-next.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/LICENSE +9 -0
- package/README.md +5 -0
- package/dist/src/bigint.d.ts +32 -0
- package/dist/src/bigint.d.ts.map +1 -0
- package/dist/src/bigint.js +54 -0
- package/dist/src/bigint.js.map +1 -0
- package/dist/src/bytecode.d.ts +27 -0
- package/dist/src/bytecode.d.ts.map +1 -0
- package/dist/src/bytecode.js +60 -0
- package/dist/src/bytecode.js.map +1 -0
- package/dist/src/bytes.d.ts +34 -0
- package/dist/src/bytes.d.ts.map +1 -0
- package/dist/src/bytes.js +47 -0
- package/dist/src/bytes.js.map +1 -0
- package/dist/src/ci.d.ts +7 -0
- package/dist/src/ci.d.ts.map +1 -0
- package/dist/src/ci.js +18 -0
- package/dist/src/ci.js.map +1 -0
- package/dist/src/common-errors.d.ts +5 -0
- package/dist/src/common-errors.d.ts.map +1 -0
- package/dist/src/common-errors.js +7 -0
- package/dist/src/common-errors.js.map +1 -0
- package/dist/src/crypto.d.ts +25 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +32 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/date.d.ts +21 -0
- package/dist/src/date.d.ts.map +1 -0
- package/dist/src/date.js +27 -0
- package/dist/src/date.js.map +1 -0
- package/dist/src/debug.d.ts +16 -0
- package/dist/src/debug.d.ts.map +1 -0
- package/dist/src/debug.js +28 -0
- package/dist/src/debug.js.map +1 -0
- package/dist/src/error.d.ts +80 -0
- package/dist/src/error.d.ts.map +1 -0
- package/dist/src/error.js +97 -0
- package/dist/src/error.js.map +1 -0
- package/dist/src/errors/bytecode.d.ts +18 -0
- package/dist/src/errors/bytecode.d.ts.map +1 -0
- package/dist/src/errors/bytecode.js +59 -0
- package/dist/src/errors/bytecode.js.map +1 -0
- package/dist/src/errors/fs.d.ts +25 -0
- package/dist/src/errors/fs.d.ts.map +1 -0
- package/dist/src/errors/fs.js +41 -0
- package/dist/src/errors/fs.js.map +1 -0
- package/dist/src/errors/package.d.ts +8 -0
- package/dist/src/errors/package.d.ts.map +1 -0
- package/dist/src/errors/package.js +12 -0
- package/dist/src/errors/package.js.map +1 -0
- package/dist/src/errors/request.d.ts +25 -0
- package/dist/src/errors/request.d.ts.map +1 -0
- package/dist/src/errors/request.js +64 -0
- package/dist/src/errors/request.js.map +1 -0
- package/dist/src/errors/subprocess.d.ts +8 -0
- package/dist/src/errors/subprocess.d.ts.map +1 -0
- package/dist/src/errors/subprocess.js +12 -0
- package/dist/src/errors/subprocess.js.map +1 -0
- package/dist/src/eth.d.ts +56 -0
- package/dist/src/eth.d.ts.map +1 -0
- package/dist/src/eth.js +78 -0
- package/dist/src/eth.js.map +1 -0
- package/dist/src/fs.d.ts +241 -0
- package/dist/src/fs.d.ts.map +1 -0
- package/dist/src/fs.js +581 -0
- package/dist/src/fs.js.map +1 -0
- package/dist/src/global-dir.d.ts +28 -0
- package/dist/src/global-dir.d.ts.map +1 -0
- package/dist/src/global-dir.js +42 -0
- package/dist/src/global-dir.js.map +1 -0
- package/dist/src/hex.d.ts +117 -0
- package/dist/src/hex.d.ts.map +1 -0
- package/dist/src/hex.js +187 -0
- package/dist/src/hex.js.map +1 -0
- package/dist/src/internal/bytecode.d.ts +45 -0
- package/dist/src/internal/bytecode.d.ts.map +1 -0
- package/dist/src/internal/bytecode.js +73 -0
- package/dist/src/internal/bytecode.js.map +1 -0
- package/dist/src/internal/eth.d.ts +21 -0
- package/dist/src/internal/eth.d.ts.map +1 -0
- package/dist/src/internal/eth.js +58 -0
- package/dist/src/internal/eth.js.map +1 -0
- package/dist/src/internal/global-dir.d.ts +4 -0
- package/dist/src/internal/global-dir.d.ts.map +1 -0
- package/dist/src/internal/global-dir.js +6 -0
- package/dist/src/internal/global-dir.js.map +1 -0
- package/dist/src/internal/hex.d.ts +2 -0
- package/dist/src/internal/hex.d.ts.map +1 -0
- package/dist/src/internal/hex.js +16 -0
- package/dist/src/internal/hex.js.map +1 -0
- package/dist/src/internal/lang.d.ts +2 -0
- package/dist/src/internal/lang.d.ts.map +1 -0
- package/dist/src/internal/lang.js +9 -0
- package/dist/src/internal/lang.js.map +1 -0
- package/dist/src/internal/package.d.ts +2 -0
- package/dist/src/internal/package.d.ts.map +1 -0
- package/dist/src/internal/package.js +15 -0
- package/dist/src/internal/package.js.map +1 -0
- package/dist/src/internal/request.d.ts +20 -0
- package/dist/src/internal/request.d.ts.map +1 -0
- package/dist/src/internal/request.js +101 -0
- package/dist/src/internal/request.js.map +1 -0
- package/dist/src/lang.d.ts +30 -0
- package/dist/src/lang.d.ts.map +1 -0
- package/dist/src/lang.js +41 -0
- package/dist/src/lang.js.map +1 -0
- package/dist/src/number.d.ts +23 -0
- package/dist/src/number.d.ts.map +1 -0
- package/dist/src/number.js +30 -0
- package/dist/src/number.js.map +1 -0
- package/dist/src/package.d.ts +75 -0
- package/dist/src/package.d.ts.map +1 -0
- package/dist/src/package.js +97 -0
- package/dist/src/package.js.map +1 -0
- package/dist/src/path.d.ts +26 -0
- package/dist/src/path.d.ts.map +1 -0
- package/dist/src/path.js +38 -0
- package/dist/src/path.js.map +1 -0
- package/dist/src/request.d.ts +118 -0
- package/dist/src/request.d.ts.map +1 -0
- package/dist/src/request.js +208 -0
- package/dist/src/request.js.map +1 -0
- package/dist/src/stream.d.ts +10 -0
- package/dist/src/stream.d.ts.map +1 -0
- package/dist/src/stream.js +16 -0
- package/dist/src/stream.js.map +1 -0
- package/dist/src/string.d.ts +45 -0
- package/dist/src/string.d.ts.map +1 -0
- package/dist/src/string.js +60 -0
- package/dist/src/string.js.map +1 -0
- package/dist/src/subprocess.d.ts +13 -0
- package/dist/src/subprocess.d.ts.map +1 -0
- package/dist/src/subprocess.js +33 -0
- package/dist/src/subprocess.js.map +1 -0
- package/dist/src/synchronization.d.ts +6 -0
- package/dist/src/synchronization.d.ts.map +1 -0
- package/dist/src/synchronization.js +107 -0
- package/dist/src/synchronization.js.map +1 -0
- package/package.json +90 -0
- package/src/bigint.ts +61 -0
- package/src/bytecode.ts +90 -0
- package/src/bytes.ts +51 -0
- package/src/ci.ts +19 -0
- package/src/common-errors.ts +7 -0
- package/src/crypto.ts +35 -0
- package/src/date.ts +28 -0
- package/src/debug.ts +38 -0
- package/src/error.ts +105 -0
- package/src/errors/bytecode.ts +75 -0
- package/src/errors/fs.ts +47 -0
- package/src/errors/package.ts +13 -0
- package/src/errors/request.ts +84 -0
- package/src/errors/subprocess.ts +17 -0
- package/src/eth.ts +91 -0
- package/src/fs.ts +678 -0
- package/src/global-dir.ts +51 -0
- package/src/hex.ts +232 -0
- package/src/internal/bytecode.ts +129 -0
- package/src/internal/eth.ts +71 -0
- package/src/internal/global-dir.ts +10 -0
- package/src/internal/hex.ts +21 -0
- package/src/internal/lang.ts +12 -0
- package/src/internal/package.ts +15 -0
- package/src/internal/request.ts +165 -0
- package/src/lang.ts +48 -0
- package/src/number.ts +38 -0
- package/src/package.ts +143 -0
- package/src/path.ts +45 -0
- package/src/request.ts +341 -0
- package/src/stream.ts +16 -0
- package/src/string.ts +69 -0
- package/src/subprocess.ts +46 -0
- package/src/synchronization.ts +126 -0
package/src/request.ts
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import type EventEmitter from "node:events";
|
|
2
|
+
import type { ParsedUrlQueryInput } from "node:querystring";
|
|
3
|
+
import type UndiciT from "undici";
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import querystring from "node:querystring";
|
|
7
|
+
import stream from "node:stream/promises";
|
|
8
|
+
|
|
9
|
+
import { ensureError } from "./error.js";
|
|
10
|
+
import {
|
|
11
|
+
DownloadError,
|
|
12
|
+
RequestError,
|
|
13
|
+
DispatcherError,
|
|
14
|
+
} from "./errors/request.js";
|
|
15
|
+
import { move } from "./fs.js";
|
|
16
|
+
import {
|
|
17
|
+
generateTempFilePath,
|
|
18
|
+
getBaseDispatcherOptions,
|
|
19
|
+
getBaseRequestOptions,
|
|
20
|
+
getBasicDispatcher,
|
|
21
|
+
getPoolDispatcher,
|
|
22
|
+
getProxyDispatcher,
|
|
23
|
+
handleError,
|
|
24
|
+
} from "./internal/request.js";
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_TIMEOUT_IN_MILLISECONDS = 30_000;
|
|
27
|
+
export const DEFAULT_MAX_REDIRECTS = 10;
|
|
28
|
+
export const DEFAULT_POOL_MAX_CONNECTIONS = 128;
|
|
29
|
+
export const DEFAULT_USER_AGENT = "Hardhat";
|
|
30
|
+
|
|
31
|
+
export type Dispatcher = UndiciT.Dispatcher;
|
|
32
|
+
export type TestDispatcher = UndiciT.MockAgent;
|
|
33
|
+
export type Interceptable = UndiciT.Interceptable;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options to configure the dispatcher.
|
|
37
|
+
*
|
|
38
|
+
* @param timeout The timeout in milliseconds. Defaults to {@link DEFAULT_TIMEOUT_IN_MILLISECONDS}.
|
|
39
|
+
* @param proxy The proxy to use. If not provided, no proxy is used.
|
|
40
|
+
* @param pool Whether to use a pool dispatcher. Defaults to `false`.
|
|
41
|
+
* @param maxConnections The maximum number of connections to use in the pool. Defaults to {@link DEFAULT_POOL_MAX_CONNECTIONS}.
|
|
42
|
+
* @param isTestDispatcher Whether to use a test dispatcher. Defaults to `false`. It's highly recommended to use a test dispatcher in tests to avoid hanging tests.
|
|
43
|
+
*/
|
|
44
|
+
export interface DispatcherOptions {
|
|
45
|
+
timeout?: number;
|
|
46
|
+
proxy?: string;
|
|
47
|
+
pool?: boolean;
|
|
48
|
+
maxConnections?: number;
|
|
49
|
+
isTestDispatcher?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Options to configure a request.
|
|
54
|
+
*
|
|
55
|
+
* @param queryParams The query parameters to append to the url.
|
|
56
|
+
* @param extraHeaders Additional headers to include in the request.
|
|
57
|
+
* @param abortSignal The signal to abort the request.
|
|
58
|
+
*/
|
|
59
|
+
export interface RequestOptions {
|
|
60
|
+
queryParams?: Record<string, any>;
|
|
61
|
+
extraHeaders?: Record<string, string>;
|
|
62
|
+
abortSignal?: AbortSignal | EventEmitter;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Performs a HTTP request.
|
|
67
|
+
*
|
|
68
|
+
* @param url The url to make the request to.
|
|
69
|
+
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
70
|
+
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
71
|
+
* @returns The response data object. See {@link https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-responsedata}.
|
|
72
|
+
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
73
|
+
* @throws RequestTimeoutError If the request times out.
|
|
74
|
+
* @throws RequestError If the request fails for any other reason.
|
|
75
|
+
*/
|
|
76
|
+
export async function getRequest(
|
|
77
|
+
url: string,
|
|
78
|
+
requestOptions: RequestOptions = {},
|
|
79
|
+
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
80
|
+
): Promise<UndiciT.Dispatcher.ResponseData> {
|
|
81
|
+
const { request } = await import("undici");
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const baseRequestOptions = await getBaseRequestOptions(
|
|
85
|
+
url,
|
|
86
|
+
requestOptions,
|
|
87
|
+
dispatcherOrDispatcherOptions,
|
|
88
|
+
);
|
|
89
|
+
return await request(url, {
|
|
90
|
+
method: "GET",
|
|
91
|
+
...baseRequestOptions,
|
|
92
|
+
});
|
|
93
|
+
} catch (e) {
|
|
94
|
+
ensureError(e);
|
|
95
|
+
|
|
96
|
+
handleError(e, url);
|
|
97
|
+
|
|
98
|
+
throw new RequestError(url, "GET", e);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Performs a POST request with a JSON body.
|
|
104
|
+
*
|
|
105
|
+
* @param url The url to make the request to.
|
|
106
|
+
* @param body The body of the request, represented as an object.
|
|
107
|
+
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
108
|
+
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
109
|
+
* @returns The response data object. See {@link https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-responsedata}.
|
|
110
|
+
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
111
|
+
* @throws RequestTimeoutError If the request times out.
|
|
112
|
+
* @throws RequestError If the request fails for any other reason.
|
|
113
|
+
*/
|
|
114
|
+
export async function postJsonRequest(
|
|
115
|
+
url: string,
|
|
116
|
+
body: unknown,
|
|
117
|
+
requestOptions: RequestOptions = {},
|
|
118
|
+
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
119
|
+
): Promise<UndiciT.Dispatcher.ResponseData> {
|
|
120
|
+
const { request } = await import("undici");
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const { headers, ...baseRequestOptions } = await getBaseRequestOptions(
|
|
124
|
+
url,
|
|
125
|
+
requestOptions,
|
|
126
|
+
dispatcherOrDispatcherOptions,
|
|
127
|
+
);
|
|
128
|
+
return await request(url, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
...baseRequestOptions,
|
|
131
|
+
headers: {
|
|
132
|
+
...headers,
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify(body),
|
|
136
|
+
});
|
|
137
|
+
} catch (e) {
|
|
138
|
+
ensureError(e);
|
|
139
|
+
|
|
140
|
+
handleError(e, url);
|
|
141
|
+
|
|
142
|
+
throw new RequestError(url, "POST", e);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Performs a POST request with a form body.
|
|
148
|
+
*
|
|
149
|
+
* @param url The url to make the request to.
|
|
150
|
+
* @param body The body of the request, represented as an object.
|
|
151
|
+
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
152
|
+
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
153
|
+
* @returns The response data object. See {@link https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-responsedata}.
|
|
154
|
+
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
155
|
+
* @throws RequestTimeoutError If the request times out.
|
|
156
|
+
* @throws RequestError If the request fails for any other reason.
|
|
157
|
+
*/
|
|
158
|
+
export async function postFormRequest(
|
|
159
|
+
url: string,
|
|
160
|
+
body: unknown,
|
|
161
|
+
requestOptions: RequestOptions = {},
|
|
162
|
+
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
163
|
+
): Promise<UndiciT.Dispatcher.ResponseData> {
|
|
164
|
+
const { request } = await import("undici");
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const { headers, ...baseRequestOptions } = await getBaseRequestOptions(
|
|
168
|
+
url,
|
|
169
|
+
requestOptions,
|
|
170
|
+
dispatcherOrDispatcherOptions,
|
|
171
|
+
);
|
|
172
|
+
return await request(url, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
...baseRequestOptions,
|
|
175
|
+
headers: {
|
|
176
|
+
...headers,
|
|
177
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
178
|
+
},
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Add a runtime check for body's type
|
|
180
|
+
body: querystring.stringify(body as ParsedUrlQueryInput),
|
|
181
|
+
});
|
|
182
|
+
} catch (e) {
|
|
183
|
+
ensureError(e);
|
|
184
|
+
|
|
185
|
+
handleError(e, url);
|
|
186
|
+
|
|
187
|
+
throw new RequestError(url, "POST", e);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Downloads a file from a url to a destination path.
|
|
193
|
+
*
|
|
194
|
+
* @param url The url to download from.
|
|
195
|
+
* @param destination The absolute path to save the file to.
|
|
196
|
+
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
197
|
+
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
198
|
+
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
199
|
+
* @throws RequestTimeoutError If the request times out.
|
|
200
|
+
* @throws DownloadFailedError If the download fails for any other reason.
|
|
201
|
+
*/
|
|
202
|
+
export async function download(
|
|
203
|
+
url: string,
|
|
204
|
+
destination: string,
|
|
205
|
+
requestOptions: RequestOptions = {},
|
|
206
|
+
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
let statusCode: number | undefined;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const response = await getRequest(
|
|
212
|
+
url,
|
|
213
|
+
requestOptions,
|
|
214
|
+
dispatcherOrDispatcherOptions,
|
|
215
|
+
);
|
|
216
|
+
const { body } = response;
|
|
217
|
+
statusCode = response.statusCode;
|
|
218
|
+
|
|
219
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
220
|
+
throw new Error(await body.text());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const tempFilePath = await generateTempFilePath(destination);
|
|
224
|
+
const fileStream = fs.createWriteStream(tempFilePath);
|
|
225
|
+
await stream.pipeline(body, fileStream);
|
|
226
|
+
await move(tempFilePath, destination);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
ensureError(e);
|
|
229
|
+
|
|
230
|
+
handleError(e, url);
|
|
231
|
+
|
|
232
|
+
throw new DownloadError(url, e);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Creates a dispatcher based on the provided options.
|
|
238
|
+
* If the `proxy` option is set, it creates a {@link UndiciT.ProxyAgent} dispatcher.
|
|
239
|
+
* If the `pool` option is set to `true`, it creates a {@link UndiciT.Pool} dispatcher.
|
|
240
|
+
* Otherwise, it creates a basic {@link UndiciT.Agent} dispatcher.
|
|
241
|
+
*
|
|
242
|
+
* @param url The url to make requests to.
|
|
243
|
+
* @param options The options to configure the dispatcher. See {@link DispatcherOptions}.
|
|
244
|
+
* @returns The configured dispatcher instance.
|
|
245
|
+
* @throws DispatcherError If the dispatcher can't be created.
|
|
246
|
+
*/
|
|
247
|
+
export async function getDispatcher(
|
|
248
|
+
url: string,
|
|
249
|
+
{
|
|
250
|
+
timeout,
|
|
251
|
+
proxy,
|
|
252
|
+
pool,
|
|
253
|
+
maxConnections,
|
|
254
|
+
isTestDispatcher,
|
|
255
|
+
}: DispatcherOptions = {},
|
|
256
|
+
): Promise<Dispatcher> {
|
|
257
|
+
try {
|
|
258
|
+
if (pool !== undefined && proxy !== undefined) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
"The pool and proxy options can't be used at the same time",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const baseOptions = getBaseDispatcherOptions(timeout, isTestDispatcher);
|
|
264
|
+
|
|
265
|
+
if (proxy !== undefined) {
|
|
266
|
+
return await getProxyDispatcher(proxy, baseOptions);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (pool === true) {
|
|
270
|
+
return await getPoolDispatcher(url, {
|
|
271
|
+
...baseOptions,
|
|
272
|
+
connections: maxConnections ?? DEFAULT_POOL_MAX_CONNECTIONS,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return await getBasicDispatcher(baseOptions);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
ensureError(e);
|
|
279
|
+
throw new DispatcherError(e.message, e);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function getTestDispatcher(
|
|
284
|
+
options: {
|
|
285
|
+
timeout?: number;
|
|
286
|
+
} = {},
|
|
287
|
+
): Promise<TestDispatcher> {
|
|
288
|
+
const { MockAgent } = await import("undici");
|
|
289
|
+
|
|
290
|
+
const baseOptions = getBaseDispatcherOptions(options.timeout, true);
|
|
291
|
+
return new MockAgent(baseOptions);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Determines whether a proxy should be used for a given url.
|
|
296
|
+
*
|
|
297
|
+
* @param url The url to check.
|
|
298
|
+
* @returns `true` if a proxy should be used for the url, `false` otherwise.
|
|
299
|
+
*/
|
|
300
|
+
export function shouldUseProxy(url: string): boolean {
|
|
301
|
+
const { hostname } = new URL(url);
|
|
302
|
+
const noProxy = process.env.NO_PROXY;
|
|
303
|
+
|
|
304
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || noProxy === "*") {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (noProxy !== undefined && noProxy !== "") {
|
|
309
|
+
const noProxySet = new Set(noProxy.split(","));
|
|
310
|
+
|
|
311
|
+
if (noProxySet.has(hostname)) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Determines whether an absolute url is valid.
|
|
321
|
+
*
|
|
322
|
+
* @param url The url to check.
|
|
323
|
+
* @returns `true` if the url is valid, `false` otherwise.
|
|
324
|
+
*/
|
|
325
|
+
export function isValidUrl(url: string): boolean {
|
|
326
|
+
try {
|
|
327
|
+
new URL(url);
|
|
328
|
+
return true;
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export {
|
|
335
|
+
ConnectionRefusedError,
|
|
336
|
+
DispatcherError,
|
|
337
|
+
DownloadError,
|
|
338
|
+
RequestError,
|
|
339
|
+
RequestTimeoutError,
|
|
340
|
+
ResponseStatusCodeError,
|
|
341
|
+
} from "./errors/request.js";
|
package/src/stream.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Tansform that writes everything to actualWritable, without closing it
|
|
5
|
+
* when finished.
|
|
6
|
+
*
|
|
7
|
+
* This is useful to pipe things to stdout, without closing it, while being
|
|
8
|
+
* able to await for the result of the pipe to finish.
|
|
9
|
+
*/
|
|
10
|
+
export function createNonClosingWriter(actualWritable: Writable): Writable {
|
|
11
|
+
return new Writable({
|
|
12
|
+
write(chunk, encoding, callback) {
|
|
13
|
+
actualWritable.write(chunk, encoding, callback);
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
package/src/string.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a word in singular form to a pluralized string based on the number
|
|
3
|
+
* of items.
|
|
4
|
+
*
|
|
5
|
+
* @param singular The singular form of the word.
|
|
6
|
+
* @param count The number of items. This determines whether the singular or
|
|
7
|
+
* plural form is used.
|
|
8
|
+
* @param plural The optional plural form of the word. If not provided, the
|
|
9
|
+
* plural form is created by appending an "s" to the singular form.
|
|
10
|
+
* @returns The pluralized string.
|
|
11
|
+
*/
|
|
12
|
+
export function pluralize(
|
|
13
|
+
singular: string,
|
|
14
|
+
count: number,
|
|
15
|
+
plural?: string,
|
|
16
|
+
): string {
|
|
17
|
+
if (count === 1) {
|
|
18
|
+
return singular;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return plural !== undefined ? plural : `${singular}s`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Capitalizes the first letter of a string.
|
|
26
|
+
*
|
|
27
|
+
* @param str The string to capitalize.
|
|
28
|
+
* @returns The string with the first letter capitalized.
|
|
29
|
+
*/
|
|
30
|
+
export function capitalize(str: string): string {
|
|
31
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Converts a kebab-case string to camelCase.
|
|
36
|
+
*
|
|
37
|
+
* @param str The kebab-case string to convert.
|
|
38
|
+
* @returns The camelCase string.
|
|
39
|
+
*/
|
|
40
|
+
export function kebabToCamelCase(str: string): string {
|
|
41
|
+
return str.replace(/-./g, (match) => match.charAt(1).toUpperCase());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Converts a camelCase string to snake_case.
|
|
46
|
+
*
|
|
47
|
+
* @param str The camelCase string to convert.
|
|
48
|
+
* @returns The snake_case string.
|
|
49
|
+
*/
|
|
50
|
+
export function camelToSnakeCase(str: string): string {
|
|
51
|
+
return str.replace(/[A-Z0-9]/g, (match) => `_${match.toLowerCase()}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Converts a camelCase string to kebab-case.
|
|
56
|
+
*
|
|
57
|
+
* @param str The camelCase string to convert.
|
|
58
|
+
* @returns The kebab-case string.
|
|
59
|
+
*/
|
|
60
|
+
export function camelToKebabCase(str: string): string {
|
|
61
|
+
return str.replace(/[A-Z0-9]/g, (match) => `-${match.toLowerCase()}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ensures a string ends with a slash.
|
|
66
|
+
*/
|
|
67
|
+
export function ensureTrailingSlash(path: string): string {
|
|
68
|
+
return path.endsWith("/") ? path : path + "/";
|
|
69
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SubprocessFileNotFoundError,
|
|
5
|
+
SubprocessPathIsDirectoryError,
|
|
6
|
+
} from "./errors/subprocess.js";
|
|
7
|
+
import { exists, isDirectory } from "./fs.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Spawns a detached subprocess to execute a given file with optional arguments.
|
|
11
|
+
*
|
|
12
|
+
* @param absolutePathToSubProcessFile - The absolute path to the subprocess file to be executed.
|
|
13
|
+
* @param args - Optional list of string arguments to pass to the subprocess.
|
|
14
|
+
* @param env - Optional environment key-value pairs to pass to the subprocess.
|
|
15
|
+
*
|
|
16
|
+
* The subprocess runs in a detached mode and its standard input/output is ignored.
|
|
17
|
+
* This function does not wait for the subprocess to complete and the subprocess is unreferenced
|
|
18
|
+
* to allow the parent process to exit independently.
|
|
19
|
+
*/
|
|
20
|
+
export async function spawnDetachedSubProcess(
|
|
21
|
+
absolutePathToSubProcessFile: string,
|
|
22
|
+
args: string[] = [],
|
|
23
|
+
env: Record<string, string> = {},
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
if ((await exists(absolutePathToSubProcessFile)) === false) {
|
|
26
|
+
throw new SubprocessFileNotFoundError(absolutePathToSubProcessFile);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ((await isDirectory(absolutePathToSubProcessFile)) === true) {
|
|
30
|
+
throw new SubprocessPathIsDirectoryError(absolutePathToSubProcessFile);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const subprocessArgs = [absolutePathToSubProcessFile, ...args];
|
|
34
|
+
|
|
35
|
+
if (absolutePathToSubProcessFile.endsWith(".ts")) {
|
|
36
|
+
subprocessArgs.unshift("--import", "tsx/esm");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const subprocess = spawn(process.execPath, subprocessArgs, {
|
|
40
|
+
detached: true,
|
|
41
|
+
env,
|
|
42
|
+
stdio: "ignore",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
subprocess.unref();
|
|
46
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Logic explanation: the fs.writeFile function, when used with the wx+ flag, performs an atomic operation to create a file.
|
|
2
|
+
// If multiple processes try to create the same file simultaneously, only one will succeed.
|
|
3
|
+
// This logic can be utilized to implement a mutex.
|
|
4
|
+
// ATTENTION: in the current implementation, there's still a risk of two processes running simultaneously.
|
|
5
|
+
// For example, if processA has locked the mutex and is running, processB will wait.
|
|
6
|
+
// During this wait, processB continuously checks the elapsed time since the mutex lock file was created.
|
|
7
|
+
// If an excessive amount of time has passed, processB will assume ownership of the mutex to avoid stale locks.
|
|
8
|
+
// However, there's a possibility that processB might take ownership because the mutex creation file is outdated, even though processA is still running
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
import debug from "debug";
|
|
15
|
+
|
|
16
|
+
import { ensureError } from "./error.js";
|
|
17
|
+
import { FileSystemAccessError } from "./errors/fs.js";
|
|
18
|
+
import { sleep } from "./lang.js";
|
|
19
|
+
|
|
20
|
+
const log = debug("hardhat:util:multi-process-mutex");
|
|
21
|
+
const DEFAULT_MAX_MUTEX_LIFESPAN_IN_MS = 60000;
|
|
22
|
+
const MUTEX_LOOP_WAITING_TIME_IN_MS = 100;
|
|
23
|
+
|
|
24
|
+
export class MultiProcessMutex {
|
|
25
|
+
readonly #mutexFilePath: string;
|
|
26
|
+
readonly #mutexLifespanInMs: number;
|
|
27
|
+
|
|
28
|
+
constructor(mutexName: string, maxMutexLifespanInMs?: number) {
|
|
29
|
+
log(`Creating mutex with name '${mutexName}'`);
|
|
30
|
+
|
|
31
|
+
this.#mutexFilePath = path.join(os.tmpdir(), `${mutexName}.txt`);
|
|
32
|
+
this.#mutexLifespanInMs =
|
|
33
|
+
maxMutexLifespanInMs ?? DEFAULT_MAX_MUTEX_LIFESPAN_IN_MS;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async use<T>(f: () => Promise<T>): Promise<T> {
|
|
37
|
+
log(`Starting mutex process with mutex file '${this.#mutexFilePath}'`);
|
|
38
|
+
|
|
39
|
+
while (true) {
|
|
40
|
+
if (await this.#tryToAcquireMutex()) {
|
|
41
|
+
// Mutex has been acquired
|
|
42
|
+
return this.#executeFunctionAndReleaseMutex(f);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Mutex not acquired
|
|
46
|
+
if (this.#isMutexFileTooOld()) {
|
|
47
|
+
// If the mutex file is too old, it likely indicates a stale lock, so the file should be removed
|
|
48
|
+
log(
|
|
49
|
+
`Current mutex file is too old, removing it at path '${this.#mutexFilePath}'`,
|
|
50
|
+
);
|
|
51
|
+
this.#deleteMutexFile();
|
|
52
|
+
} else {
|
|
53
|
+
// wait
|
|
54
|
+
await sleep(MUTEX_LOOP_WAITING_TIME_IN_MS / 1000);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async #tryToAcquireMutex() {
|
|
60
|
+
try {
|
|
61
|
+
// Create a file only if it does not exist
|
|
62
|
+
fs.writeFileSync(this.#mutexFilePath, "", { flag: "wx+" });
|
|
63
|
+
return true;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
ensureError<NodeJS.ErrnoException>(e);
|
|
66
|
+
|
|
67
|
+
if (e.code === "EEXIST") {
|
|
68
|
+
// File already exists, so the mutex is already acquired
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new FileSystemAccessError(e.message, e);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async #executeFunctionAndReleaseMutex<T>(f: () => Promise<T>): Promise<T> {
|
|
77
|
+
log(`Mutex acquired at path '${this.#mutexFilePath}'`);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
return await f();
|
|
81
|
+
} finally {
|
|
82
|
+
// Release the mutex
|
|
83
|
+
log(`Mutex released at path '${this.#mutexFilePath}'`);
|
|
84
|
+
this.#deleteMutexFile();
|
|
85
|
+
log(`Mutex released at path '${this.#mutexFilePath}'`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#isMutexFileTooOld(): boolean {
|
|
90
|
+
let fileStat;
|
|
91
|
+
try {
|
|
92
|
+
fileStat = fs.statSync(this.#mutexFilePath);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
ensureError<NodeJS.ErrnoException>(e);
|
|
95
|
+
|
|
96
|
+
if (e.code === "ENOENT") {
|
|
97
|
+
// The file might have been deleted by another process while this function was trying to access it.
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new FileSystemAccessError(e.message, e);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const now = new Date();
|
|
105
|
+
const fileDate = new Date(fileStat.ctime);
|
|
106
|
+
const diff = now.getTime() - fileDate.getTime();
|
|
107
|
+
|
|
108
|
+
return diff > this.#mutexLifespanInMs;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#deleteMutexFile() {
|
|
112
|
+
try {
|
|
113
|
+
log(`Deleting mutex file at path '${this.#mutexFilePath}'`);
|
|
114
|
+
fs.unlinkSync(this.#mutexFilePath);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
ensureError<NodeJS.ErrnoException>(e);
|
|
117
|
+
|
|
118
|
+
if (e.code === "ENOENT") {
|
|
119
|
+
// The file might have been deleted by another process while this function was trying to access it.
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw new FileSystemAccessError(e.message, e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|