@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.
Files changed (49) hide show
  1. package/dist/src/bigint.d.ts.map +1 -1
  2. package/dist/src/bigint.js +1 -0
  3. package/dist/src/bigint.js.map +1 -1
  4. package/dist/src/debug.d.ts.map +1 -1
  5. package/dist/src/error.d.ts +19 -2
  6. package/dist/src/error.d.ts.map +1 -1
  7. package/dist/src/error.js +22 -2
  8. package/dist/src/error.js.map +1 -1
  9. package/dist/src/fs.d.ts +35 -3
  10. package/dist/src/fs.d.ts.map +1 -1
  11. package/dist/src/fs.js +155 -25
  12. package/dist/src/fs.js.map +1 -1
  13. package/dist/src/internal/lang.d.ts +1 -0
  14. package/dist/src/internal/lang.d.ts.map +1 -1
  15. package/dist/src/internal/lang.js +28 -0
  16. package/dist/src/internal/lang.js.map +1 -1
  17. package/dist/src/lang.d.ts +19 -0
  18. package/dist/src/lang.d.ts.map +1 -1
  19. package/dist/src/lang.js +22 -1
  20. package/dist/src/lang.js.map +1 -1
  21. package/dist/src/package.d.ts +12 -0
  22. package/dist/src/package.d.ts.map +1 -1
  23. package/dist/src/package.js.map +1 -1
  24. package/dist/src/path.d.ts +2 -2
  25. package/dist/src/path.d.ts.map +1 -1
  26. package/dist/src/path.js +12 -3
  27. package/dist/src/path.js.map +1 -1
  28. package/dist/src/request.d.ts +17 -7
  29. package/dist/src/request.d.ts.map +1 -1
  30. package/dist/src/request.js +13 -5
  31. package/dist/src/request.js.map +1 -1
  32. package/dist/src/stream.d.ts +1 -1
  33. package/dist/src/stream.js +1 -1
  34. package/dist/src/subprocess.js +1 -1
  35. package/dist/src/subprocess.js.map +1 -1
  36. package/dist/src/synchronization.js +4 -4
  37. package/dist/src/synchronization.js.map +1 -1
  38. package/package.json +9 -13
  39. package/src/bigint.ts +1 -0
  40. package/src/error.ts +26 -2
  41. package/src/fs.ts +196 -28
  42. package/src/internal/lang.ts +38 -0
  43. package/src/lang.ts +26 -1
  44. package/src/package.ts +26 -0
  45. package/src/path.ts +16 -6
  46. package/src/request.ts +25 -9
  47. package/src/stream.ts +1 -1
  48. package/src/subprocess.ts +1 -1
  49. 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
- ensureError<NodeJS.ErrnoException>(e);
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
- return getAllFilesMatching(absolutePathToFile, matches);
59
- } else if (matches === undefined || matches(absolutePathToFile)) {
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` is called on it.
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
- ensureError<NodeJS.ErrnoException>(e);
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<NodeJS.ErrnoException>(e);
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<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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 (err) {}
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
- ensureError<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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 (e) {
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
- ensureError<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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
- throw new DirectoryNotEmptyError(destination, e);
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
- ensureError<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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
- ensureError<NodeJS.ErrnoException>(e);
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 as common in fs permissions
802
+ // eslint-disable-next-line no-bitwise -- Bitwise is common in fs permissions
635
803
  await chmod(absolutePath, mode & 0o777);
636
804
  }
637
805
 
@@ -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 insidde the current working directory. For example, if the
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
- absolutePath: string,
36
- folder: string = process.cwd(),
37
- ): string {
38
- const relativePath = path.relative(folder, absolutePath);
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 = 30_000;
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 The response data object. See {@link https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-responsedata}.
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<UndiciT.Dispatcher.ResponseData> {
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 The response data object. See {@link https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-responsedata}.
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<UndiciT.Dispatcher.ResponseData> {
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 The response data object. See {@link https://undici.nodejs.org/#/docs/api/Dispatcher?id=parameter-responsedata}.
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<UndiciT.Dispatcher.ResponseData> {
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
- const response = await getRequest(
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