@nomicfoundation/hardhat-utils 3.0.0-next.0 → 3.0.0-next.10
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 +13 -0
- 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/error.d.ts +17 -0
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +20 -0
- package/dist/src/error.js.map +1 -1
- package/dist/src/fs.d.ts +33 -1
- package/dist/src/fs.d.ts.map +1 -1
- package/dist/src/fs.js +152 -22
- package/dist/src/fs.js.map +1 -1
- package/dist/src/global-dir.d.ts.map +1 -1
- package/dist/src/global-dir.js +1 -1
- package/dist/src/global-dir.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/request.d.ts +1 -1
- package/dist/src/request.d.ts.map +1 -1
- package/dist/src/request.js +1 -1
- package/dist/src/request.js.map +1 -1
- package/dist/src/synchronization.js +6 -6
- package/dist/src/synchronization.js.map +1 -1
- package/package.json +8 -12
- package/src/bigint.ts +1 -0
- package/src/error.ts +24 -0
- package/src/fs.ts +189 -23
- package/src/global-dir.ts +1 -2
- package/src/internal/lang.ts +38 -0
- package/src/lang.ts +26 -1
- package/src/request.ts +1 -1
- package/src/synchronization.ts +6 -6
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.
|
|
@@ -48,6 +57,7 @@ export async function getRealPath(absolutePath: string): Promise<string> {
|
|
|
48
57
|
export async function getAllFilesMatching(
|
|
49
58
|
dirFrom: string,
|
|
50
59
|
matches?: (absolutePathToFile: string) => boolean,
|
|
60
|
+
directoryFilter?: (absolutePathToDir: string) => boolean,
|
|
51
61
|
): Promise<string[]> {
|
|
52
62
|
const dirContent = await readdirOrEmpty(dirFrom);
|
|
53
63
|
|
|
@@ -55,7 +65,18 @@ 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
|
-
|
|
68
|
+
if (
|
|
69
|
+
directoryFilter === undefined ||
|
|
70
|
+
directoryFilter(absolutePathToFile)
|
|
71
|
+
) {
|
|
72
|
+
return getAllFilesMatching(
|
|
73
|
+
absolutePathToFile,
|
|
74
|
+
matches,
|
|
75
|
+
directoryFilter,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [];
|
|
59
80
|
} else if (matches === undefined || matches(absolutePathToFile)) {
|
|
60
81
|
return absolutePathToFile;
|
|
61
82
|
} else {
|
|
@@ -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,22 @@ 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 fsPromises.mkdtemp(path.join(tmpdir(), prefix));
|
|
544
|
+
} catch (e) {
|
|
545
|
+
ensureNodeErrnoExceptionError(e);
|
|
546
|
+
throw new FileSystemAccessError(e.message, e);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
387
550
|
/**
|
|
388
551
|
* Retrieves the last change time of a file or directory's properties.
|
|
389
552
|
* This includes changes to the file's metadata or contents.
|
|
@@ -398,7 +561,7 @@ export async function getChangeTime(absolutePath: string): Promise<Date> {
|
|
|
398
561
|
const stats = await fsPromises.stat(absolutePath);
|
|
399
562
|
return stats.ctime;
|
|
400
563
|
} catch (e) {
|
|
401
|
-
|
|
564
|
+
ensureNodeErrnoExceptionError(e);
|
|
402
565
|
if (e.code === "ENOENT") {
|
|
403
566
|
throw new FileNotFoundError(absolutePath, e);
|
|
404
567
|
}
|
|
@@ -420,7 +583,7 @@ export async function getAccessTime(absolutePath: string): Promise<Date> {
|
|
|
420
583
|
const stats = await fsPromises.stat(absolutePath);
|
|
421
584
|
return stats.atime;
|
|
422
585
|
} catch (e) {
|
|
423
|
-
|
|
586
|
+
ensureNodeErrnoExceptionError(e);
|
|
424
587
|
if (e.code === "ENOENT") {
|
|
425
588
|
throw new FileNotFoundError(absolutePath, e);
|
|
426
589
|
}
|
|
@@ -442,7 +605,7 @@ export async function getFileSize(absolutePath: string): Promise<number> {
|
|
|
442
605
|
const stats = await fsPromises.stat(absolutePath);
|
|
443
606
|
return stats.size;
|
|
444
607
|
} catch (e) {
|
|
445
|
-
|
|
608
|
+
ensureNodeErrnoExceptionError(e);
|
|
446
609
|
if (e.code === "ENOENT") {
|
|
447
610
|
throw new FileNotFoundError(absolutePath, e);
|
|
448
611
|
}
|
|
@@ -461,7 +624,7 @@ export async function exists(absolutePath: string): Promise<boolean> {
|
|
|
461
624
|
try {
|
|
462
625
|
await fsPromises.access(absolutePath);
|
|
463
626
|
return true;
|
|
464
|
-
} catch (
|
|
627
|
+
} catch (_error) {
|
|
465
628
|
return false;
|
|
466
629
|
}
|
|
467
630
|
}
|
|
@@ -480,7 +643,7 @@ export async function copy(source: string, destination: string): Promise<void> {
|
|
|
480
643
|
try {
|
|
481
644
|
await fsPromises.copyFile(source, destination);
|
|
482
645
|
} catch (e) {
|
|
483
|
-
|
|
646
|
+
ensureNodeErrnoExceptionError(e);
|
|
484
647
|
if (e.code === "ENOENT") {
|
|
485
648
|
if (!(await exists(source))) {
|
|
486
649
|
throw new FileNotFoundError(source, e);
|
|
@@ -524,7 +687,7 @@ export async function move(source: string, destination: string): Promise<void> {
|
|
|
524
687
|
try {
|
|
525
688
|
await fsPromises.rename(source, destination);
|
|
526
689
|
} catch (e) {
|
|
527
|
-
|
|
690
|
+
ensureNodeErrnoExceptionError(e);
|
|
528
691
|
if (e.code === "ENOENT") {
|
|
529
692
|
if (!(await exists(source))) {
|
|
530
693
|
throw new FileNotFoundError(source, e);
|
|
@@ -537,7 +700,9 @@ export async function move(source: string, destination: string): Promise<void> {
|
|
|
537
700
|
// On linux, trying to move a non-empty directory will throw ENOTEMPTY,
|
|
538
701
|
// while on Windows it will throw EPERM.
|
|
539
702
|
if (e.code === "ENOTEMPTY" || e.code === "EPERM") {
|
|
540
|
-
|
|
703
|
+
if (await isDirectory(source)) {
|
|
704
|
+
throw new DirectoryNotEmptyError(destination, e);
|
|
705
|
+
}
|
|
541
706
|
}
|
|
542
707
|
|
|
543
708
|
throw new FileSystemAccessError(e.message, e);
|
|
@@ -557,9 +722,10 @@ export async function remove(absolutePath: string): Promise<void> {
|
|
|
557
722
|
recursive: true,
|
|
558
723
|
force: true,
|
|
559
724
|
maxRetries: 3,
|
|
725
|
+
retryDelay: 300,
|
|
560
726
|
});
|
|
561
727
|
} catch (e) {
|
|
562
|
-
|
|
728
|
+
ensureNodeErrnoExceptionError(e);
|
|
563
729
|
throw new FileSystemAccessError(e.message, e);
|
|
564
730
|
}
|
|
565
731
|
}
|
|
@@ -579,7 +745,7 @@ export async function chmod(
|
|
|
579
745
|
try {
|
|
580
746
|
await fsPromises.chmod(absolutePath, mode);
|
|
581
747
|
} catch (e) {
|
|
582
|
-
|
|
748
|
+
ensureNodeErrnoExceptionError(e);
|
|
583
749
|
if (e.code === "ENOENT") {
|
|
584
750
|
throw new FileNotFoundError(absolutePath, e);
|
|
585
751
|
}
|
|
@@ -616,7 +782,7 @@ export async function emptyDir(absolutePath: string): Promise<void> {
|
|
|
616
782
|
isDir = stats.isDirectory();
|
|
617
783
|
mode = stats.mode;
|
|
618
784
|
} catch (e) {
|
|
619
|
-
|
|
785
|
+
ensureNodeErrnoExceptionError(e);
|
|
620
786
|
if (e.code === "ENOENT") {
|
|
621
787
|
await mkdir(absolutePath);
|
|
622
788
|
return;
|
|
@@ -631,7 +797,7 @@ export async function emptyDir(absolutePath: string): Promise<void> {
|
|
|
631
797
|
|
|
632
798
|
await remove(absolutePath);
|
|
633
799
|
await mkdir(absolutePath);
|
|
634
|
-
// eslint-disable-next-line no-bitwise -- Bitwise
|
|
800
|
+
// eslint-disable-next-line no-bitwise -- Bitwise is common in fs permissions
|
|
635
801
|
await chmod(absolutePath, mode & 0o777);
|
|
636
802
|
}
|
|
637
803
|
|
package/src/global-dir.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { ensureDir } from "./fs.js";
|
|
1
2
|
import { generatePaths, HARDHAT_PACKAGE_NAME } from "./internal/global-dir.js";
|
|
2
3
|
|
|
3
|
-
import { ensureDir } from "@nomicfoundation/hardhat-utils/fs";
|
|
4
|
-
|
|
5
4
|
/**
|
|
6
5
|
* Returns the configuration directory path for a given package (defaults to "hardhat").
|
|
7
6
|
* Ensures that the directory exists before returning the path.
|
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/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";
|
package/src/synchronization.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// ATTENTION: in the current implementation, there's still a risk of two processes running simultaneously.
|
|
5
5
|
// For example, if processA has locked the mutex and is running, processB will wait.
|
|
6
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
|
|
8
|
-
//
|
|
7
|
+
// If an excessive amount of time has passed, processB will assume ownership of the mutex to prevent stale locks, even if processA is still running.
|
|
8
|
+
// As a result, two processes will be running simultaneously in what is theoretically a mutex-locked section.
|
|
9
9
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import os from "node:os";
|
|
@@ -13,7 +13,7 @@ import path from "node:path";
|
|
|
13
13
|
|
|
14
14
|
import debug from "debug";
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { ensureNodeErrnoExceptionError } from "./error.js";
|
|
17
17
|
import { FileSystemAccessError } from "./errors/fs.js";
|
|
18
18
|
import { sleep } from "./lang.js";
|
|
19
19
|
|
|
@@ -62,7 +62,7 @@ export class MultiProcessMutex {
|
|
|
62
62
|
fs.writeFileSync(this.#mutexFilePath, "", { flag: "wx+" });
|
|
63
63
|
return true;
|
|
64
64
|
} catch (e) {
|
|
65
|
-
|
|
65
|
+
ensureNodeErrnoExceptionError(e);
|
|
66
66
|
|
|
67
67
|
if (e.code === "EEXIST") {
|
|
68
68
|
// File already exists, so the mutex is already acquired
|
|
@@ -91,7 +91,7 @@ export class MultiProcessMutex {
|
|
|
91
91
|
try {
|
|
92
92
|
fileStat = fs.statSync(this.#mutexFilePath);
|
|
93
93
|
} catch (e) {
|
|
94
|
-
|
|
94
|
+
ensureNodeErrnoExceptionError(e);
|
|
95
95
|
|
|
96
96
|
if (e.code === "ENOENT") {
|
|
97
97
|
// The file might have been deleted by another process while this function was trying to access it.
|
|
@@ -113,7 +113,7 @@ export class MultiProcessMutex {
|
|
|
113
113
|
log(`Deleting mutex file at path '${this.#mutexFilePath}'`);
|
|
114
114
|
fs.unlinkSync(this.#mutexFilePath);
|
|
115
115
|
} catch (e) {
|
|
116
|
-
|
|
116
|
+
ensureNodeErrnoExceptionError(e);
|
|
117
117
|
|
|
118
118
|
if (e.code === "ENOENT") {
|
|
119
119
|
// The file might have been deleted by another process while this function was trying to access it.
|