@nomicfoundation/hardhat-utils 3.0.0-next.2 → 3.0.0-next.21
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/dist/src/bigint.d.ts.map +1 -1
- package/dist/src/bigint.js +1 -0
- package/dist/src/bigint.js.map +1 -1
- package/dist/src/debug.d.ts.map +1 -1
- package/dist/src/error.d.ts +19 -2
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +22 -2
- package/dist/src/error.js.map +1 -1
- package/dist/src/fs.d.ts +35 -3
- package/dist/src/fs.d.ts.map +1 -1
- package/dist/src/fs.js +155 -25
- package/dist/src/fs.js.map +1 -1
- package/dist/src/internal/lang.d.ts +1 -0
- package/dist/src/internal/lang.d.ts.map +1 -1
- package/dist/src/internal/lang.js +28 -0
- package/dist/src/internal/lang.js.map +1 -1
- package/dist/src/lang.d.ts +19 -0
- package/dist/src/lang.d.ts.map +1 -1
- package/dist/src/lang.js +22 -1
- package/dist/src/lang.js.map +1 -1
- package/dist/src/package.d.ts +12 -0
- package/dist/src/package.d.ts.map +1 -1
- package/dist/src/package.js.map +1 -1
- package/dist/src/path.d.ts +2 -2
- package/dist/src/path.d.ts.map +1 -1
- package/dist/src/path.js +12 -3
- package/dist/src/path.js.map +1 -1
- package/dist/src/request.d.ts +17 -7
- package/dist/src/request.d.ts.map +1 -1
- package/dist/src/request.js +13 -5
- package/dist/src/request.js.map +1 -1
- package/dist/src/stream.d.ts +1 -1
- package/dist/src/stream.js +1 -1
- package/dist/src/subprocess.js +1 -1
- package/dist/src/subprocess.js.map +1 -1
- package/dist/src/synchronization.js +4 -4
- package/dist/src/synchronization.js.map +1 -1
- package/package.json +9 -13
- package/src/bigint.ts +1 -0
- package/src/error.ts +26 -2
- package/src/fs.ts +196 -28
- package/src/internal/lang.ts +38 -0
- package/src/lang.ts +26 -1
- package/src/package.ts +26 -0
- package/src/path.ts +16 -6
- package/src/request.ts +25 -9
- package/src/stream.ts +1 -1
- package/src/subprocess.ts +1 -1
- package/src/synchronization.ts +4 -4
package/src/fs.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import type { JsonTypes, ParsedElementInfo } from "@streamparser/json-node";
|
|
2
|
+
import type { FileHandle } from "node:fs/promises";
|
|
3
|
+
|
|
1
4
|
import fsPromises from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
2
6
|
import path from "node:path";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
|
|
9
|
+
import { JSONParser } from "@streamparser/json-node";
|
|
10
|
+
import { JsonStreamStringify } from "json-stream-stringify";
|
|
3
11
|
|
|
4
|
-
import { ensureError } from "./error.js";
|
|
12
|
+
import { ensureError, ensureNodeErrnoExceptionError } from "./error.js";
|
|
5
13
|
import {
|
|
6
14
|
FileNotFoundError,
|
|
7
15
|
FileSystemAccessError,
|
|
@@ -24,7 +32,7 @@ export async function getRealPath(absolutePath: string): Promise<string> {
|
|
|
24
32
|
try {
|
|
25
33
|
return await fsPromises.realpath(path.normalize(absolutePath));
|
|
26
34
|
} catch (e) {
|
|
27
|
-
|
|
35
|
+
ensureNodeErrnoExceptionError(e);
|
|
28
36
|
if (e.code === "ENOENT") {
|
|
29
37
|
throw new FileNotFoundError(absolutePath, e);
|
|
30
38
|
}
|
|
@@ -39,6 +47,7 @@ export async function getRealPath(absolutePath: string): Promise<string> {
|
|
|
39
47
|
*
|
|
40
48
|
* @param dirFrom The absolute path of the directory to start the search from.
|
|
41
49
|
* @param matches A function to filter files (not directories).
|
|
50
|
+
* @param directoryFilter A function to filter which directories to recurse into
|
|
42
51
|
* @returns An array of absolute paths. Each file has its true case, except
|
|
43
52
|
* for the initial dirFrom part, which preserves the given casing.
|
|
44
53
|
* No order is guaranteed. If dirFrom doesn't exist `[]` is returned.
|
|
@@ -47,7 +56,8 @@ export async function getRealPath(absolutePath: string): Promise<string> {
|
|
|
47
56
|
*/
|
|
48
57
|
export async function getAllFilesMatching(
|
|
49
58
|
dirFrom: string,
|
|
50
|
-
matches?: (absolutePathToFile: string) => boolean,
|
|
59
|
+
matches?: (absolutePathToFile: string) => Promise<boolean> | boolean,
|
|
60
|
+
directoryFilter?: (absolutePathToDir: string) => Promise<boolean> | boolean,
|
|
51
61
|
): Promise<string[]> {
|
|
52
62
|
const dirContent = await readdirOrEmpty(dirFrom);
|
|
53
63
|
|
|
@@ -55,8 +65,19 @@ export async function getAllFilesMatching(
|
|
|
55
65
|
dirContent.map(async (file) => {
|
|
56
66
|
const absolutePathToFile = path.join(dirFrom, file);
|
|
57
67
|
if (await isDirectory(absolutePathToFile)) {
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
if (
|
|
69
|
+
directoryFilter === undefined ||
|
|
70
|
+
(await directoryFilter(absolutePathToFile))
|
|
71
|
+
) {
|
|
72
|
+
return getAllFilesMatching(
|
|
73
|
+
absolutePathToFile,
|
|
74
|
+
matches,
|
|
75
|
+
directoryFilter,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [];
|
|
80
|
+
} else if (matches === undefined || (await matches(absolutePathToFile))) {
|
|
60
81
|
return absolutePathToFile;
|
|
61
82
|
} else {
|
|
62
83
|
return [];
|
|
@@ -72,7 +93,7 @@ export async function getAllFilesMatching(
|
|
|
72
93
|
* satisfy the specified condition, returning their absolute paths. Once a
|
|
73
94
|
* directory is found, its subdirectories are not searched.
|
|
74
95
|
*
|
|
75
|
-
* Note: dirFrom is never returned, nor `matches`
|
|
96
|
+
* Note: dirFrom is never returned, nor is `matches` called on it.
|
|
76
97
|
*
|
|
77
98
|
* @param dirFrom The absolute path of the directory to start the search from.
|
|
78
99
|
* @param matches A function to filter directories (not files).
|
|
@@ -84,7 +105,7 @@ export async function getAllFilesMatching(
|
|
|
84
105
|
*/
|
|
85
106
|
export async function getAllDirectoriesMatching(
|
|
86
107
|
dirFrom: string,
|
|
87
|
-
matches?: (absolutePathToDir: string) => boolean,
|
|
108
|
+
matches?: (absolutePathToDir: string) => Promise<boolean> | boolean,
|
|
88
109
|
): Promise<string[]> {
|
|
89
110
|
const dirContent = await readdirOrEmpty(dirFrom);
|
|
90
111
|
|
|
@@ -95,7 +116,7 @@ export async function getAllDirectoriesMatching(
|
|
|
95
116
|
return [];
|
|
96
117
|
}
|
|
97
118
|
|
|
98
|
-
if (matches === undefined || matches(absolutePathToFile)) {
|
|
119
|
+
if (matches === undefined || (await matches(absolutePathToFile))) {
|
|
99
120
|
return absolutePathToFile;
|
|
100
121
|
}
|
|
101
122
|
|
|
@@ -158,7 +179,7 @@ export async function isDirectory(absolutePath: string): Promise<boolean> {
|
|
|
158
179
|
try {
|
|
159
180
|
return (await fsPromises.lstat(absolutePath)).isDirectory();
|
|
160
181
|
} catch (e) {
|
|
161
|
-
|
|
182
|
+
ensureNodeErrnoExceptionError(e);
|
|
162
183
|
if (e.code === "ENOENT") {
|
|
163
184
|
throw new FileNotFoundError(absolutePath, e);
|
|
164
185
|
}
|
|
@@ -182,8 +203,81 @@ export async function readJsonFile<T>(absolutePathToFile: string): Promise<T> {
|
|
|
182
203
|
try {
|
|
183
204
|
return JSON.parse(content.toString());
|
|
184
205
|
} catch (e) {
|
|
185
|
-
ensureError
|
|
206
|
+
ensureError(e);
|
|
207
|
+
throw new InvalidFileFormatError(absolutePathToFile, e);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Reads a JSON file as a stream and parses it. The encoding used is "utf8".
|
|
213
|
+
* This function should be used when parsing very large JSON files.
|
|
214
|
+
*
|
|
215
|
+
* @param absolutePathToFile The path to the file.
|
|
216
|
+
* @returns The parsed JSON object.
|
|
217
|
+
* @throws FileNotFoundError if the file doesn't exist.
|
|
218
|
+
* @throws InvalidFileFormatError if the file is not a valid JSON file.
|
|
219
|
+
* @throws IsDirectoryError if the path is a directory instead of a file.
|
|
220
|
+
* @throws FileSystemAccessError for any other error.
|
|
221
|
+
*/
|
|
222
|
+
export async function readJsonFileAsStream<T>(
|
|
223
|
+
absolutePathToFile: string,
|
|
224
|
+
): Promise<T> {
|
|
225
|
+
let fileHandle: FileHandle | undefined;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
fileHandle = await fsPromises.open(absolutePathToFile, "r");
|
|
229
|
+
|
|
230
|
+
const fileReadStream = fileHandle.createReadStream();
|
|
231
|
+
|
|
232
|
+
// NOTE: We set a separator to disable self-closing to be able to use the parser
|
|
233
|
+
// in the stream.pipeline context; see https://github.com/juanjoDiaz/streamparser-json/issues/47
|
|
234
|
+
const jsonParser = new JSONParser({
|
|
235
|
+
separator: "",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const result: T | undefined = await pipeline(
|
|
239
|
+
fileReadStream,
|
|
240
|
+
jsonParser,
|
|
241
|
+
async (
|
|
242
|
+
elements: AsyncIterable<ParsedElementInfo.ParsedElementInfo>,
|
|
243
|
+
): Promise<any | undefined> => {
|
|
244
|
+
let value: JsonTypes.JsonPrimitive | JsonTypes.JsonStruct | undefined;
|
|
245
|
+
for await (const element of elements) {
|
|
246
|
+
value = element.value;
|
|
247
|
+
}
|
|
248
|
+
return value;
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (result === undefined) {
|
|
253
|
+
throw new Error("No data");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return result;
|
|
257
|
+
} catch (e) {
|
|
258
|
+
ensureError(e);
|
|
259
|
+
|
|
260
|
+
// If the code is defined, we assume the error to be related to the file system
|
|
261
|
+
if ("code" in e) {
|
|
262
|
+
if (e.code === "ENOENT") {
|
|
263
|
+
throw new FileNotFoundError(absolutePathToFile, e);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (e.code === "EISDIR") {
|
|
267
|
+
throw new IsDirectoryError(absolutePathToFile, e);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If the code is defined, we assume the error to be related to the file system
|
|
271
|
+
if (e.code !== undefined) {
|
|
272
|
+
throw new FileSystemAccessError(absolutePathToFile, e);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Otherwise, we assume the error to be related to the file formatting
|
|
186
277
|
throw new InvalidFileFormatError(absolutePathToFile, e);
|
|
278
|
+
} finally {
|
|
279
|
+
// Explicitly closing the file handle to fully release the underlying resources
|
|
280
|
+
await fileHandle?.close();
|
|
187
281
|
}
|
|
188
282
|
}
|
|
189
283
|
|
|
@@ -204,13 +298,66 @@ export async function writeJsonFile<T>(
|
|
|
204
298
|
try {
|
|
205
299
|
content = JSON.stringify(object, null, 2);
|
|
206
300
|
} catch (e) {
|
|
207
|
-
ensureError
|
|
301
|
+
ensureError(e);
|
|
208
302
|
throw new JsonSerializationError(absolutePathToFile, e);
|
|
209
303
|
}
|
|
210
304
|
|
|
211
305
|
await writeUtf8File(absolutePathToFile, content);
|
|
212
306
|
}
|
|
213
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Writes an object to a JSON file as stream. The encoding used is "utf8" and the file is overwritten.
|
|
310
|
+
* If part of the path doesn't exist, it will be created.
|
|
311
|
+
* This function should be used when stringifying very large JSON objects.
|
|
312
|
+
*
|
|
313
|
+
* @param absolutePathToFile The path to the file. If the file exists, it will be overwritten.
|
|
314
|
+
* @param object The object to write.
|
|
315
|
+
* @throws JsonSerializationError if the object can't be serialized to JSON.
|
|
316
|
+
* @throws FileSystemAccessError for any other error.
|
|
317
|
+
*/
|
|
318
|
+
export async function writeJsonFileAsStream<T>(
|
|
319
|
+
absolutePathToFile: string,
|
|
320
|
+
object: T,
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
const dirPath = path.dirname(absolutePathToFile);
|
|
323
|
+
const dirExists = await exists(dirPath);
|
|
324
|
+
if (!dirExists) {
|
|
325
|
+
await mkdir(dirPath);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let fileHandle: FileHandle | undefined;
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
fileHandle = await fsPromises.open(absolutePathToFile, "w");
|
|
332
|
+
|
|
333
|
+
const jsonStream = new JsonStreamStringify(object);
|
|
334
|
+
const fileWriteStream = fileHandle.createWriteStream();
|
|
335
|
+
|
|
336
|
+
await pipeline(jsonStream, fileWriteStream);
|
|
337
|
+
} catch (e) {
|
|
338
|
+
ensureError(e);
|
|
339
|
+
// if the directory was created, we should remove it
|
|
340
|
+
if (dirExists === false) {
|
|
341
|
+
try {
|
|
342
|
+
await remove(dirPath);
|
|
343
|
+
// we don't want to override the original error
|
|
344
|
+
} catch (_error) {}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// If the code is defined, we assume the error to be related to the file system
|
|
348
|
+
if ("code" in e && e.code !== undefined) {
|
|
349
|
+
throw new FileSystemAccessError(e.message, e);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Otherwise, we assume the error to be related to the file formatting
|
|
353
|
+
throw new JsonSerializationError(absolutePathToFile, e);
|
|
354
|
+
} finally {
|
|
355
|
+
// NOTE: Historically, not closing the file handle caused issues on Windows,
|
|
356
|
+
// for example, when trying to move the file previously written to by this function
|
|
357
|
+
await fileHandle?.close();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
214
361
|
/**
|
|
215
362
|
* Reads a file and returns its content as a string. The encoding used is "utf8".
|
|
216
363
|
*
|
|
@@ -226,7 +373,7 @@ export async function readUtf8File(
|
|
|
226
373
|
try {
|
|
227
374
|
return await fsPromises.readFile(absolutePathToFile, { encoding: "utf8" });
|
|
228
375
|
} catch (e) {
|
|
229
|
-
|
|
376
|
+
ensureNodeErrnoExceptionError(e);
|
|
230
377
|
|
|
231
378
|
if (e.code === "ENOENT") {
|
|
232
379
|
throw new FileNotFoundError(absolutePathToFile, e);
|
|
@@ -268,13 +415,13 @@ export async function writeUtf8File(
|
|
|
268
415
|
flag,
|
|
269
416
|
});
|
|
270
417
|
} catch (e) {
|
|
271
|
-
|
|
418
|
+
ensureNodeErrnoExceptionError(e);
|
|
272
419
|
// if the directory was created, we should remove it
|
|
273
420
|
if (dirExists === false) {
|
|
274
421
|
try {
|
|
275
422
|
await remove(dirPath);
|
|
276
423
|
// we don't want to override the original error
|
|
277
|
-
} catch (
|
|
424
|
+
} catch (_error) {}
|
|
278
425
|
}
|
|
279
426
|
|
|
280
427
|
if (e.code === "ENOENT") {
|
|
@@ -306,7 +453,7 @@ export async function readBinaryFile(
|
|
|
306
453
|
const buffer = await fsPromises.readFile(absolutePathToFile);
|
|
307
454
|
return new Uint8Array(buffer);
|
|
308
455
|
} catch (e) {
|
|
309
|
-
|
|
456
|
+
ensureNodeErrnoExceptionError(e);
|
|
310
457
|
|
|
311
458
|
if (e.code === "ENOENT") {
|
|
312
459
|
throw new FileNotFoundError(absolutePathToFile, e);
|
|
@@ -333,7 +480,7 @@ export async function readdir(absolutePathToDir: string): Promise<string[]> {
|
|
|
333
480
|
try {
|
|
334
481
|
return await fsPromises.readdir(absolutePathToDir);
|
|
335
482
|
} catch (e) {
|
|
336
|
-
|
|
483
|
+
ensureNodeErrnoExceptionError(e);
|
|
337
484
|
if (e.code === "ENOENT") {
|
|
338
485
|
throw new FileNotFoundError(absolutePathToDir, e);
|
|
339
486
|
}
|
|
@@ -373,7 +520,7 @@ export async function mkdir(absolutePath: string): Promise<void> {
|
|
|
373
520
|
try {
|
|
374
521
|
await fsPromises.mkdir(absolutePath, { recursive: true });
|
|
375
522
|
} catch (e) {
|
|
376
|
-
|
|
523
|
+
ensureNodeErrnoExceptionError(e);
|
|
377
524
|
throw new FileSystemAccessError(e.message, e);
|
|
378
525
|
}
|
|
379
526
|
}
|
|
@@ -384,6 +531,24 @@ export async function mkdir(absolutePath: string): Promise<void> {
|
|
|
384
531
|
*/
|
|
385
532
|
export const ensureDir: typeof mkdir = mkdir;
|
|
386
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Creates a temporary directory with the specified prefix.
|
|
536
|
+
*
|
|
537
|
+
* @param prefix The prefix to use for the temporary directory.
|
|
538
|
+
* @returns The absolute path to the created temporary directory.
|
|
539
|
+
* @throws FileSystemAccessError for any error.
|
|
540
|
+
*/
|
|
541
|
+
export async function mkdtemp(prefix: string): Promise<string> {
|
|
542
|
+
try {
|
|
543
|
+
return await getRealPath(
|
|
544
|
+
await fsPromises.mkdtemp(path.join(tmpdir(), prefix)),
|
|
545
|
+
);
|
|
546
|
+
} catch (e) {
|
|
547
|
+
ensureNodeErrnoExceptionError(e);
|
|
548
|
+
throw new FileSystemAccessError(e.message, e);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
387
552
|
/**
|
|
388
553
|
* Retrieves the last change time of a file or directory's properties.
|
|
389
554
|
* This includes changes to the file's metadata or contents.
|
|
@@ -398,7 +563,7 @@ export async function getChangeTime(absolutePath: string): Promise<Date> {
|
|
|
398
563
|
const stats = await fsPromises.stat(absolutePath);
|
|
399
564
|
return stats.ctime;
|
|
400
565
|
} catch (e) {
|
|
401
|
-
|
|
566
|
+
ensureNodeErrnoExceptionError(e);
|
|
402
567
|
if (e.code === "ENOENT") {
|
|
403
568
|
throw new FileNotFoundError(absolutePath, e);
|
|
404
569
|
}
|
|
@@ -420,7 +585,7 @@ export async function getAccessTime(absolutePath: string): Promise<Date> {
|
|
|
420
585
|
const stats = await fsPromises.stat(absolutePath);
|
|
421
586
|
return stats.atime;
|
|
422
587
|
} catch (e) {
|
|
423
|
-
|
|
588
|
+
ensureNodeErrnoExceptionError(e);
|
|
424
589
|
if (e.code === "ENOENT") {
|
|
425
590
|
throw new FileNotFoundError(absolutePath, e);
|
|
426
591
|
}
|
|
@@ -442,7 +607,7 @@ export async function getFileSize(absolutePath: string): Promise<number> {
|
|
|
442
607
|
const stats = await fsPromises.stat(absolutePath);
|
|
443
608
|
return stats.size;
|
|
444
609
|
} catch (e) {
|
|
445
|
-
|
|
610
|
+
ensureNodeErrnoExceptionError(e);
|
|
446
611
|
if (e.code === "ENOENT") {
|
|
447
612
|
throw new FileNotFoundError(absolutePath, e);
|
|
448
613
|
}
|
|
@@ -461,7 +626,7 @@ export async function exists(absolutePath: string): Promise<boolean> {
|
|
|
461
626
|
try {
|
|
462
627
|
await fsPromises.access(absolutePath);
|
|
463
628
|
return true;
|
|
464
|
-
} catch (
|
|
629
|
+
} catch (_error) {
|
|
465
630
|
return false;
|
|
466
631
|
}
|
|
467
632
|
}
|
|
@@ -480,7 +645,7 @@ export async function copy(source: string, destination: string): Promise<void> {
|
|
|
480
645
|
try {
|
|
481
646
|
await fsPromises.copyFile(source, destination);
|
|
482
647
|
} catch (e) {
|
|
483
|
-
|
|
648
|
+
ensureNodeErrnoExceptionError(e);
|
|
484
649
|
if (e.code === "ENOENT") {
|
|
485
650
|
if (!(await exists(source))) {
|
|
486
651
|
throw new FileNotFoundError(source, e);
|
|
@@ -524,7 +689,7 @@ export async function move(source: string, destination: string): Promise<void> {
|
|
|
524
689
|
try {
|
|
525
690
|
await fsPromises.rename(source, destination);
|
|
526
691
|
} catch (e) {
|
|
527
|
-
|
|
692
|
+
ensureNodeErrnoExceptionError(e);
|
|
528
693
|
if (e.code === "ENOENT") {
|
|
529
694
|
if (!(await exists(source))) {
|
|
530
695
|
throw new FileNotFoundError(source, e);
|
|
@@ -537,7 +702,9 @@ export async function move(source: string, destination: string): Promise<void> {
|
|
|
537
702
|
// On linux, trying to move a non-empty directory will throw ENOTEMPTY,
|
|
538
703
|
// while on Windows it will throw EPERM.
|
|
539
704
|
if (e.code === "ENOTEMPTY" || e.code === "EPERM") {
|
|
540
|
-
|
|
705
|
+
if (await isDirectory(source)) {
|
|
706
|
+
throw new DirectoryNotEmptyError(destination, e);
|
|
707
|
+
}
|
|
541
708
|
}
|
|
542
709
|
|
|
543
710
|
throw new FileSystemAccessError(e.message, e);
|
|
@@ -557,9 +724,10 @@ export async function remove(absolutePath: string): Promise<void> {
|
|
|
557
724
|
recursive: true,
|
|
558
725
|
force: true,
|
|
559
726
|
maxRetries: 3,
|
|
727
|
+
retryDelay: 300,
|
|
560
728
|
});
|
|
561
729
|
} catch (e) {
|
|
562
|
-
|
|
730
|
+
ensureNodeErrnoExceptionError(e);
|
|
563
731
|
throw new FileSystemAccessError(e.message, e);
|
|
564
732
|
}
|
|
565
733
|
}
|
|
@@ -579,7 +747,7 @@ export async function chmod(
|
|
|
579
747
|
try {
|
|
580
748
|
await fsPromises.chmod(absolutePath, mode);
|
|
581
749
|
} catch (e) {
|
|
582
|
-
|
|
750
|
+
ensureNodeErrnoExceptionError(e);
|
|
583
751
|
if (e.code === "ENOENT") {
|
|
584
752
|
throw new FileNotFoundError(absolutePath, e);
|
|
585
753
|
}
|
|
@@ -616,7 +784,7 @@ export async function emptyDir(absolutePath: string): Promise<void> {
|
|
|
616
784
|
isDir = stats.isDirectory();
|
|
617
785
|
mode = stats.mode;
|
|
618
786
|
} catch (e) {
|
|
619
|
-
|
|
787
|
+
ensureNodeErrnoExceptionError(e);
|
|
620
788
|
if (e.code === "ENOENT") {
|
|
621
789
|
await mkdir(absolutePath);
|
|
622
790
|
return;
|
|
@@ -631,7 +799,7 @@ export async function emptyDir(absolutePath: string): Promise<void> {
|
|
|
631
799
|
|
|
632
800
|
await remove(absolutePath);
|
|
633
801
|
await mkdir(absolutePath);
|
|
634
|
-
// eslint-disable-next-line no-bitwise -- Bitwise
|
|
802
|
+
// eslint-disable-next-line no-bitwise -- Bitwise is common in fs permissions
|
|
635
803
|
await chmod(absolutePath, mode & 0o777);
|
|
636
804
|
}
|
|
637
805
|
|
package/src/internal/lang.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type rfdcT from "rfdc";
|
|
2
2
|
|
|
3
|
+
import { isObject } from "../lang.js";
|
|
4
|
+
|
|
3
5
|
let clone: ReturnType<typeof rfdcT> | null = null;
|
|
4
6
|
export async function getDeepCloneFunction(): Promise<<T>(input: T) => T> {
|
|
5
7
|
const { default: rfdc } = await import("rfdc");
|
|
@@ -10,3 +12,39 @@ export async function getDeepCloneFunction(): Promise<<T>(input: T) => T> {
|
|
|
10
12
|
|
|
11
13
|
return clone;
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
export function deepMergeImpl<T extends object, U extends object>(
|
|
17
|
+
target: T,
|
|
18
|
+
source: U,
|
|
19
|
+
): T & U {
|
|
20
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
21
|
+
-- The result is expected to include properties from both target and source,
|
|
22
|
+
but initially only target is spread in, so a cast is needed. */
|
|
23
|
+
const result = { ...target } as T & U;
|
|
24
|
+
|
|
25
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
26
|
+
-- TypeScript cannot infer the correct union of string and symbol keys, but all keys come from U */
|
|
27
|
+
const keys = [
|
|
28
|
+
...Object.keys(source),
|
|
29
|
+
...Object.getOwnPropertySymbols(source),
|
|
30
|
+
] as Array<keyof U>;
|
|
31
|
+
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
if (
|
|
34
|
+
isObject(source[key]) &&
|
|
35
|
+
// Only merge recursively objects that are not class instances
|
|
36
|
+
Object.getPrototypeOf(source[key]) === Object.prototype
|
|
37
|
+
) {
|
|
38
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
39
|
+
-- The call signature expects the second argument to be of type U; the type is correct, but TypeScript can't infer it here. */
|
|
40
|
+
result[key] = deepMergeImpl(result[key] ?? {}, source[key] as U) as (T &
|
|
41
|
+
U)[Extract<keyof U, string>];
|
|
42
|
+
} else {
|
|
43
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
44
|
+
-- Cast required because TypeScript can't guarantee that a dynamic key from `U` exists in `T & U` or has the correct value type. */
|
|
45
|
+
result[key] = source[key] as (T & U)[Extract<keyof U, string>];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
}
|
package/src/lang.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDeepCloneFunction } from "./internal/lang.js";
|
|
1
|
+
import { deepMergeImpl, getDeepCloneFunction } from "./internal/lang.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Creates a deep clone of the provided value.
|
|
@@ -25,6 +25,31 @@ export async function deepEqual<T>(x: T, y: T): Promise<boolean> {
|
|
|
25
25
|
return _deepEqual(x, y);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Deeply merges two objects.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* - Arrays or `undefined` values are not valid inputs.
|
|
33
|
+
* - Functions: If a function exists in both the target and source, the source function overwrites the target.
|
|
34
|
+
* - Symbol properties: Symbol-keyed properties are merged just like string keys.
|
|
35
|
+
* - Class instances: Class instances are not merged recursively. If a class instance exists in the source, it will replace the one in the target.
|
|
36
|
+
*
|
|
37
|
+
* @param target The target object to merge into.
|
|
38
|
+
* @param source The source object to merge from.
|
|
39
|
+
* @returns A new object containing the deeply merged properties.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* deepMerge({ a: { b: 1 } }, { a: { c: 2 } }) // => { a: { b: 1, c: 2 } }
|
|
43
|
+
*
|
|
44
|
+
* deepMerge({ a: { fn: () => "from target" } }, { a: { fn: () => "from source" } }) // => { a: { fn: () => "from source" } }
|
|
45
|
+
*/
|
|
46
|
+
export function deepMerge<T extends object, U extends object>(
|
|
47
|
+
target: T,
|
|
48
|
+
source: U,
|
|
49
|
+
): T & U {
|
|
50
|
+
return deepMergeImpl(target, source);
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
/**
|
|
29
54
|
* Checks if a value is an object. This function returns false for arrays.
|
|
30
55
|
*
|
package/src/package.ts
CHANGED
|
@@ -10,6 +10,31 @@ import { exists, findUp, getRealPath, readJsonFile } from "./fs.js";
|
|
|
10
10
|
import { getFilePath } from "./internal/package.js";
|
|
11
11
|
import { ensureTrailingSlash } from "./string.js";
|
|
12
12
|
|
|
13
|
+
/* Adapted from `resolve.exports`. License: https://github.com/lukeed/resolve.exports/blob/master/license */
|
|
14
|
+
|
|
15
|
+
export type PackageExports =
|
|
16
|
+
| PackageExportPath
|
|
17
|
+
| {
|
|
18
|
+
[path: PackageExportsEntry]: PackageExportsValue;
|
|
19
|
+
[condition: string]: PackageExportsValue;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Allows "." and "./{name}" */
|
|
23
|
+
export type PackageExportsEntry = `.${string}`;
|
|
24
|
+
|
|
25
|
+
/** Internal path */
|
|
26
|
+
export type PackageExportPath = `./${string}`;
|
|
27
|
+
|
|
28
|
+
export type PackageExportsValue =
|
|
29
|
+
| PackageExportPath
|
|
30
|
+
| null
|
|
31
|
+
| {
|
|
32
|
+
[condition: string]: PackageExportsValue;
|
|
33
|
+
}
|
|
34
|
+
| PackageExportsValue[];
|
|
35
|
+
|
|
36
|
+
/* End of `resolve.exports` adaptation */
|
|
37
|
+
|
|
13
38
|
/**
|
|
14
39
|
* The structure of a `package.json` file. This is a subset of the actual
|
|
15
40
|
* `package.json` file, if you need to access other fields you add them here.
|
|
@@ -22,6 +47,7 @@ export interface PackageJson {
|
|
|
22
47
|
engines?: {
|
|
23
48
|
node?: string;
|
|
24
49
|
};
|
|
50
|
+
exports?: PackageExports;
|
|
25
51
|
dependencies?: Record<string, string>;
|
|
26
52
|
devDependencies?: Record<string, string>;
|
|
27
53
|
peerDependencies?: Record<string, string>;
|
package/src/path.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function resolveFromRoot(root: string, target: string): string {
|
|
|
22
22
|
* Tries to return a shorter version of the path if its inside the given folder.
|
|
23
23
|
*
|
|
24
24
|
* This is useful for displaying paths in the terminal, as they can be shorter
|
|
25
|
-
* when they are
|
|
25
|
+
* when they are inside the current working directory. For example, if the
|
|
26
26
|
* current working directory is `/home/user/project`, and the path is
|
|
27
27
|
* `/home/user/project/contracts/File.sol`, the shorter path is
|
|
28
28
|
* `contracts/File.sol`.
|
|
@@ -31,11 +31,21 @@ export function resolveFromRoot(root: string, target: string): string {
|
|
|
31
31
|
* @param folder The absolute path to the folder.
|
|
32
32
|
* @returns The shorter path, if possible, or the original path.
|
|
33
33
|
*/
|
|
34
|
-
export function shortenPath(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
export function shortenPath(absolutePath: string): string {
|
|
35
|
+
const cwd = process.cwd();
|
|
36
|
+
let relativePath = path.relative(cwd, absolutePath);
|
|
37
|
+
|
|
38
|
+
if (relativePath === "..") {
|
|
39
|
+
return ".." + path.sep;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
!relativePath.startsWith(".." + path.sep) &&
|
|
44
|
+
!relativePath.startsWith("." + path.sep) &&
|
|
45
|
+
!path.isAbsolute(relativePath)
|
|
46
|
+
) {
|
|
47
|
+
relativePath = "." + path.sep + relativePath;
|
|
48
|
+
}
|
|
39
49
|
|
|
40
50
|
if (relativePath.length < absolutePath.length) {
|
|
41
51
|
return relativePath;
|
package/src/request.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
handleError,
|
|
24
24
|
} from "./internal/request.js";
|
|
25
25
|
|
|
26
|
-
export const DEFAULT_TIMEOUT_IN_MILLISECONDS =
|
|
26
|
+
export const DEFAULT_TIMEOUT_IN_MILLISECONDS = 300_000; // Aligned with unidici
|
|
27
27
|
export const DEFAULT_MAX_REDIRECTS = 10;
|
|
28
28
|
export const DEFAULT_POOL_MAX_CONNECTIONS = 128;
|
|
29
29
|
export const DEFAULT_USER_AGENT = "Hardhat";
|
|
@@ -62,13 +62,22 @@ export interface RequestOptions {
|
|
|
62
62
|
abortSignal?: AbortSignal | EventEmitter;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export interface HttpResponse {
|
|
66
|
+
statusCode: number;
|
|
67
|
+
body: {
|
|
68
|
+
json(): Promise<any>;
|
|
69
|
+
text(): Promise<string>;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
/**
|
|
66
74
|
* Performs a HTTP request.
|
|
67
75
|
*
|
|
68
76
|
* @param url The url to make the request to.
|
|
69
77
|
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
70
78
|
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
71
|
-
* @returns
|
|
79
|
+
* @returns An object containing the status code and the response body. The body can be accessed as JSON or text.
|
|
80
|
+
* `body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.
|
|
72
81
|
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
73
82
|
* @throws RequestTimeoutError If the request times out.
|
|
74
83
|
* @throws RequestError If the request fails for any other reason.
|
|
@@ -77,7 +86,7 @@ export async function getRequest(
|
|
|
77
86
|
url: string,
|
|
78
87
|
requestOptions: RequestOptions = {},
|
|
79
88
|
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
80
|
-
): Promise<
|
|
89
|
+
): Promise<HttpResponse> {
|
|
81
90
|
const { request } = await import("undici");
|
|
82
91
|
|
|
83
92
|
try {
|
|
@@ -106,7 +115,8 @@ export async function getRequest(
|
|
|
106
115
|
* @param body The body of the request, represented as an object.
|
|
107
116
|
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
108
117
|
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
109
|
-
* @returns
|
|
118
|
+
* @returns An object containing the status code and the response body. The body can be accessed as JSON or text.
|
|
119
|
+
* `body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.
|
|
110
120
|
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
111
121
|
* @throws RequestTimeoutError If the request times out.
|
|
112
122
|
* @throws RequestError If the request fails for any other reason.
|
|
@@ -116,7 +126,7 @@ export async function postJsonRequest(
|
|
|
116
126
|
body: unknown,
|
|
117
127
|
requestOptions: RequestOptions = {},
|
|
118
128
|
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
119
|
-
): Promise<
|
|
129
|
+
): Promise<HttpResponse> {
|
|
120
130
|
const { request } = await import("undici");
|
|
121
131
|
|
|
122
132
|
try {
|
|
@@ -150,7 +160,8 @@ export async function postJsonRequest(
|
|
|
150
160
|
* @param body The body of the request, represented as an object.
|
|
151
161
|
* @param requestOptions The options to configure the request. See {@link RequestOptions}.
|
|
152
162
|
* @param dispatcherOrDispatcherOptions Either a dispatcher or dispatcher options. See {@link DispatcherOptions}.
|
|
153
|
-
* @returns
|
|
163
|
+
* @returns An object containing the status code and the response body. The body can be accessed as JSON or text.
|
|
164
|
+
* `body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.
|
|
154
165
|
* @throws ConnectionRefusedError If the connection is refused by the server.
|
|
155
166
|
* @throws RequestTimeoutError If the request times out.
|
|
156
167
|
* @throws RequestError If the request fails for any other reason.
|
|
@@ -160,7 +171,7 @@ export async function postFormRequest(
|
|
|
160
171
|
body: unknown,
|
|
161
172
|
requestOptions: RequestOptions = {},
|
|
162
173
|
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
163
|
-
): Promise<
|
|
174
|
+
): Promise<HttpResponse> {
|
|
164
175
|
const { request } = await import("undici");
|
|
165
176
|
|
|
166
177
|
try {
|
|
@@ -208,11 +219,16 @@ export async function download(
|
|
|
208
219
|
let statusCode: number | undefined;
|
|
209
220
|
|
|
210
221
|
try {
|
|
211
|
-
|
|
222
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
223
|
+
-- We need the full Dispatcher.ResponseData here for stream.pipeline,
|
|
224
|
+
but HttpResponse doesn’t expose the raw ReadableStream.
|
|
225
|
+
TODO: wrap undici's request so we can keep the public API
|
|
226
|
+
strictly typed without falling back to Undici types. */
|
|
227
|
+
const response = (await getRequest(
|
|
212
228
|
url,
|
|
213
229
|
requestOptions,
|
|
214
230
|
dispatcherOrDispatcherOptions,
|
|
215
|
-
);
|
|
231
|
+
)) as UndiciT.Dispatcher.ResponseData;
|
|
216
232
|
const { body } = response;
|
|
217
233
|
statusCode = response.statusCode;
|
|
218
234
|
|