@nomicfoundation/hardhat-utils 3.0.5 → 4.0.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/CHANGELOG.md +17 -0
- package/dist/src/errors/fs.d.ts +1 -1
- package/dist/src/errors/fs.d.ts.map +1 -1
- package/dist/src/errors/fs.js.map +1 -1
- package/dist/src/errors/synchronization.d.ts +29 -0
- package/dist/src/errors/synchronization.d.ts.map +1 -0
- package/dist/src/errors/synchronization.js +48 -0
- package/dist/src/errors/synchronization.js.map +1 -0
- package/dist/src/format.d.ts +7 -41
- package/dist/src/format.d.ts.map +1 -1
- package/dist/src/format.js +2 -72
- package/dist/src/format.js.map +1 -1
- package/dist/src/fs.d.ts.map +1 -1
- package/dist/src/fs.js +8 -0
- package/dist/src/fs.js.map +1 -1
- package/dist/src/global-dir.d.ts +16 -0
- package/dist/src/global-dir.d.ts.map +1 -1
- package/dist/src/global-dir.js +27 -0
- package/dist/src/global-dir.js.map +1 -1
- package/dist/src/internal/format.d.ts +3 -3
- package/dist/src/internal/format.d.ts.map +1 -1
- package/dist/src/internal/format.js.map +1 -1
- package/dist/src/internal/lang.d.ts +8 -0
- package/dist/src/internal/lang.d.ts.map +1 -1
- package/dist/src/internal/lang.js +30 -0
- package/dist/src/internal/lang.js.map +1 -1
- package/dist/src/internal/request.d.ts.map +1 -1
- package/dist/src/internal/request.js +2 -1
- package/dist/src/internal/request.js.map +1 -1
- package/dist/src/lang.d.ts.map +1 -1
- package/dist/src/lang.js +2 -3
- package/dist/src/lang.js.map +1 -1
- package/dist/src/request.d.ts.map +1 -1
- package/dist/src/request.js +24 -6
- package/dist/src/request.js.map +1 -1
- package/dist/src/synchronization.d.ts +99 -1
- package/dist/src/synchronization.d.ts.map +1 -1
- package/dist/src/synchronization.js +407 -89
- package/dist/src/synchronization.js.map +1 -1
- package/package.json +3 -3
- package/src/errors/fs.ts +1 -1
- package/src/errors/synchronization.ts +75 -0
- package/src/format.ts +11 -94
- package/src/fs.ts +9 -0
- package/src/global-dir.ts +31 -0
- package/src/internal/format.ts +3 -3
- package/src/internal/lang.ts +36 -0
- package/src/internal/request.ts +2 -1
- package/src/lang.ts +6 -4
- package/src/request.ts +29 -6
- package/src/synchronization.ts +504 -95
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { CustomError } from "../error.js";
|
|
2
|
+
|
|
3
|
+
export class BaseMultiProcessMutexError extends CustomError {
|
|
4
|
+
constructor(message: string, cause?: Error) {
|
|
5
|
+
super(message, cause);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class InvalidMultiProcessMutexPathError extends BaseMultiProcessMutexError {
|
|
10
|
+
constructor(mutexPath: string) {
|
|
11
|
+
super(`The path ${mutexPath} is not a valid absolute path.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class MultiProcessMutexError extends BaseMultiProcessMutexError {
|
|
16
|
+
constructor(lockPath: string, cause: Error) {
|
|
17
|
+
super(`Unexpected error with lock at ${lockPath}: ${cause.message}`, cause);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class MultiProcessMutexTimeoutError extends BaseMultiProcessMutexError {
|
|
22
|
+
constructor(lockPath: string, timeoutMs: number) {
|
|
23
|
+
super(
|
|
24
|
+
`Timed out waiting to acquire lock at ${lockPath} after ${timeoutMs}ms`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class StaleMultiProcessMutexError extends BaseMultiProcessMutexError {
|
|
30
|
+
constructor(lockPath: string, ownerUid: number | undefined, cause: Error) {
|
|
31
|
+
const uidInfo = ownerUid !== undefined ? ` (uid: ${ownerUid})` : "";
|
|
32
|
+
super(
|
|
33
|
+
`Lock at ${lockPath} appears stale but cannot be removed due to insufficient permissions${uidInfo}. Please remove it manually: ${lockPath}`,
|
|
34
|
+
cause,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class IncompatibleMultiProcessMutexError extends BaseMultiProcessMutexError {
|
|
40
|
+
constructor(message: string) {
|
|
41
|
+
super(message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class IncompatibleHostnameMultiProcessMutexError extends IncompatibleMultiProcessMutexError {
|
|
46
|
+
constructor(
|
|
47
|
+
lockPath: string,
|
|
48
|
+
foreignHostname: string,
|
|
49
|
+
currentHostname: string,
|
|
50
|
+
) {
|
|
51
|
+
super(
|
|
52
|
+
`Lock at ${lockPath} was created by a different host (${foreignHostname}, current: ${currentHostname}). It cannot be verified or removed automatically. Please remove it manually: ${lockPath}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class IncompatiblePlatformMultiProcessMutexError extends IncompatibleMultiProcessMutexError {
|
|
58
|
+
constructor(
|
|
59
|
+
lockPath: string,
|
|
60
|
+
foreignPlatform: string,
|
|
61
|
+
currentPlatform: string,
|
|
62
|
+
) {
|
|
63
|
+
super(
|
|
64
|
+
`Lock at ${lockPath} was created on a different platform (${foreignPlatform}, current: ${currentPlatform}). It cannot be verified or removed automatically. Please remove it manually: ${lockPath}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class IncompatibleUidMultiProcessMutexError extends IncompatibleMultiProcessMutexError {
|
|
70
|
+
constructor(lockPath: string, foreignUid: number, currentUid: number) {
|
|
71
|
+
super(
|
|
72
|
+
`Lock at ${lockPath} is owned by a different user (uid: ${foreignUid}, current: ${currentUid}). It cannot be removed automatically. Please remove it manually: ${lockPath}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/format.ts
CHANGED
|
@@ -9,114 +9,31 @@ import {
|
|
|
9
9
|
renderSectionClose,
|
|
10
10
|
} from "./internal/format.js";
|
|
11
11
|
|
|
12
|
-
export
|
|
13
|
-
export interface TableDivider {
|
|
14
|
-
type: "divider";
|
|
15
|
-
}
|
|
16
|
-
export type TableItem = TableRow | TableDivider;
|
|
17
|
-
|
|
18
|
-
export const divider: TableDivider = { type: "divider" };
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Formats an array of rows and dividers into a table string.
|
|
22
|
-
*
|
|
23
|
-
* @param items An array of table rows (string arrays) and dividers.
|
|
24
|
-
* Dividers are objects with type: "divider" and will be rendered as table dividers.
|
|
25
|
-
* @returns The formatted table as a string, ready to be rendered.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* formatTable([
|
|
30
|
-
* ["Name", "Age"],
|
|
31
|
-
* divider,
|
|
32
|
-
* ["Alice", "30"],
|
|
33
|
-
* ["Bob", "25"],
|
|
34
|
-
* divider,
|
|
35
|
-
* ["Average", "27.5"]
|
|
36
|
-
* ]);
|
|
37
|
-
*
|
|
38
|
-
* // =>
|
|
39
|
-
* // | Name | Age |
|
|
40
|
-
* // | ------- | ---- |
|
|
41
|
-
* // | Alice | 30 |
|
|
42
|
-
* // | Bob | 25 |
|
|
43
|
-
* // | ------- | ---- |
|
|
44
|
-
* // | Average | 27.5 |
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
export function formatTable(items: TableItem[]): string {
|
|
48
|
-
const widths: number[] = [];
|
|
49
|
-
const dataRows: string[][] = [];
|
|
50
|
-
|
|
51
|
-
for (const item of items) {
|
|
52
|
-
if (Array.isArray(item)) {
|
|
53
|
-
dataRows.push([...item]);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Calculate maximum width for each column
|
|
58
|
-
for (const row of dataRows) {
|
|
59
|
-
for (let i = 0; i < row.length; i++) {
|
|
60
|
-
while (i >= widths.length) {
|
|
61
|
-
widths.push(0);
|
|
62
|
-
}
|
|
63
|
-
widths[i] = Math.max(widths[i], getStringWidth(row[i]));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const dividerRow = widths.map((width) => "-".repeat(width));
|
|
68
|
-
const outputRows: string[][] = [];
|
|
69
|
-
|
|
70
|
-
for (const item of items) {
|
|
71
|
-
if (Array.isArray(item)) {
|
|
72
|
-
const row = [...item];
|
|
73
|
-
// Pad the row to match the number of columns
|
|
74
|
-
while (row.length < widths.length) {
|
|
75
|
-
row.push("");
|
|
76
|
-
}
|
|
77
|
-
outputRows.push(row);
|
|
78
|
-
} else {
|
|
79
|
-
outputRows.push([...dividerRow]);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
outputRows.forEach((row) => {
|
|
84
|
-
for (let i = 0; i < row.length; i++) {
|
|
85
|
-
const displayWidth = getStringWidth(row[i]);
|
|
86
|
-
const actualLength = row[i].length;
|
|
87
|
-
// Adjust padding to account for difference between display width and actual length
|
|
88
|
-
row[i] = row[i].padEnd(widths[i] + actualLength - displayWidth);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
return outputRows.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface TableTitleV2 {
|
|
12
|
+
export interface TableTitle {
|
|
96
13
|
type: "title";
|
|
97
14
|
text: string;
|
|
98
15
|
}
|
|
99
16
|
|
|
100
|
-
export interface
|
|
17
|
+
export interface TableSectionHeader {
|
|
101
18
|
type: "section-header";
|
|
102
19
|
text: string;
|
|
103
20
|
}
|
|
104
21
|
|
|
105
|
-
export interface
|
|
22
|
+
export interface TableHeader {
|
|
106
23
|
type: "header";
|
|
107
24
|
cells: string[];
|
|
108
25
|
}
|
|
109
26
|
|
|
110
|
-
export interface
|
|
27
|
+
export interface TableRow {
|
|
111
28
|
type: "row";
|
|
112
29
|
cells: string[];
|
|
113
30
|
}
|
|
114
31
|
|
|
115
|
-
export type
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
32
|
+
export type TableItem =
|
|
33
|
+
| TableTitle
|
|
34
|
+
| TableSectionHeader
|
|
35
|
+
| TableHeader
|
|
36
|
+
| TableRow;
|
|
120
37
|
|
|
121
38
|
/**
|
|
122
39
|
* Formats an array of titles, section headers, headers, and rows into a table
|
|
@@ -135,7 +52,7 @@ export type TableItemV2 =
|
|
|
135
52
|
*
|
|
136
53
|
* @example
|
|
137
54
|
* ```ts
|
|
138
|
-
*
|
|
55
|
+
* formatTable([
|
|
139
56
|
* { type: "title", text: "My Table" },
|
|
140
57
|
* { type: "section-header", text: "User Data" },
|
|
141
58
|
* { type: "header", cells: ["Name", "Age", "City"] },
|
|
@@ -168,7 +85,7 @@ export type TableItemV2 =
|
|
|
168
85
|
* // ╚═══════╧═══════════╝
|
|
169
86
|
* ```
|
|
170
87
|
*/
|
|
171
|
-
export function
|
|
88
|
+
export function formatTable(items: TableItem[]): string {
|
|
172
89
|
if (items.length === 0) {
|
|
173
90
|
return "";
|
|
174
91
|
}
|
package/src/fs.ts
CHANGED
|
@@ -642,6 +642,15 @@ export async function exists(absolutePath: string): Promise<boolean> {
|
|
|
642
642
|
* @throws FileSystemAccessError for any other error.
|
|
643
643
|
*/
|
|
644
644
|
export async function copy(source: string, destination: string): Promise<void> {
|
|
645
|
+
// We must proactively check if the source is a directory.
|
|
646
|
+
// On modern Linux kernels (6.x+), the `copy_file_range` system call used by
|
|
647
|
+
// Node.js may return success (0 bytes copied) when the source is a directory
|
|
648
|
+
// instead of throwing EISDIR. Node.js interprets this 0-byte success as a
|
|
649
|
+
// completed operation, resulting in no error being thrown.
|
|
650
|
+
if (await isDirectory(source)) {
|
|
651
|
+
throw new IsDirectoryError(source, undefined);
|
|
652
|
+
}
|
|
653
|
+
|
|
645
654
|
try {
|
|
646
655
|
await fsPromises.copyFile(source, destination);
|
|
647
656
|
} catch (e) {
|
package/src/global-dir.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { ensureDir } from "./fs.js";
|
|
2
2
|
import { generatePaths, HARDHAT_PACKAGE_NAME } from "./internal/global-dir.js";
|
|
3
3
|
|
|
4
|
+
// Internal override for testing purposes
|
|
5
|
+
let _cacheDirOverride: string | undefined;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sets a mock cache directory for getCacheDir. This is intended for testing
|
|
9
|
+
* purposes only, to isolate tests from the real global cache.
|
|
10
|
+
*
|
|
11
|
+
* @param dir The directory path to use as the mock cache directory.
|
|
12
|
+
*/
|
|
13
|
+
export function setMockCacheDir(dir: string): void {
|
|
14
|
+
_cacheDirOverride = dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resets the mock cache directory set by setMockCacheDir.
|
|
19
|
+
* Call this in test cleanup to restore normal behavior.
|
|
20
|
+
*/
|
|
21
|
+
export function resetMockCacheDir(): void {
|
|
22
|
+
_cacheDirOverride = undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
4
25
|
/**
|
|
5
26
|
* Returns the configuration directory path for a given package (defaults to "hardhat").
|
|
6
27
|
* Ensures that the directory exists before returning the path.
|
|
@@ -21,6 +42,10 @@ export async function getConfigDir(
|
|
|
21
42
|
* Returns the cache directory path for a given package (defaults to "hardhat").
|
|
22
43
|
* Ensures that the directory exists before returning the path.
|
|
23
44
|
*
|
|
45
|
+
* For testing purposes, the cache directory can be overridden using
|
|
46
|
+
* setMockCacheDir(). This is intended to isolate tests from the real
|
|
47
|
+
* global cache.
|
|
48
|
+
*
|
|
24
49
|
* @param packageName The name of the package for which to generate paths. Defaults to "hardhat" if no package name is provided.
|
|
25
50
|
* @returns The path to the hardhat cache directory.
|
|
26
51
|
* @throws FileSystemAccessError for any error.
|
|
@@ -28,6 +53,12 @@ export async function getConfigDir(
|
|
|
28
53
|
export async function getCacheDir(
|
|
29
54
|
packageName: string = HARDHAT_PACKAGE_NAME,
|
|
30
55
|
): Promise<string> {
|
|
56
|
+
// Allow override for testing purposes
|
|
57
|
+
if (_cacheDirOverride !== undefined) {
|
|
58
|
+
await ensureDir(_cacheDirOverride);
|
|
59
|
+
return _cacheDirOverride;
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
const { cache } = await generatePaths(packageName);
|
|
32
63
|
await ensureDir(cache);
|
|
33
64
|
return cache;
|
package/src/internal/format.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TableItem } from "../format.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Calculate the display width of a string by removing ANSI escape codes.
|
|
@@ -17,7 +17,7 @@ export function getStringWidth(str: string): number {
|
|
|
17
17
|
* Calculates the minimum width needed by each column in the table
|
|
18
18
|
* to fit its content (accounting for ANSI color codes).
|
|
19
19
|
*/
|
|
20
|
-
export function getColumnWidths(items:
|
|
20
|
+
export function getColumnWidths(items: TableItem[]): number[] {
|
|
21
21
|
const columnWidths: number[] = [];
|
|
22
22
|
|
|
23
23
|
for (const item of items) {
|
|
@@ -53,7 +53,7 @@ export function getContentWidth(columnWidths: number[]): number {
|
|
|
53
53
|
* Each title/header is padded by 1 space on each side.
|
|
54
54
|
* Accounts for ANSI color codes.
|
|
55
55
|
*/
|
|
56
|
-
export function getHeadingWidth(items:
|
|
56
|
+
export function getHeadingWidth(items: TableItem[]): number {
|
|
57
57
|
let headingWidth = 0;
|
|
58
58
|
for (const item of items) {
|
|
59
59
|
if (item.type === "section-header" || item.type === "title") {
|
package/src/internal/lang.ts
CHANGED
|
@@ -53,3 +53,39 @@ export function deepMergeImpl<T extends object, S extends object>(
|
|
|
53
53
|
|
|
54
54
|
return result;
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
let cachedCustomEqual: ((a: unknown, b: unknown) => boolean) | undefined;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Performs a custom deep equality check using `fast-equals` with specific overrides.
|
|
61
|
+
*
|
|
62
|
+
* @param x The first value to compare.
|
|
63
|
+
* @param y The second value to compare.
|
|
64
|
+
* @returns A promise that resolves to true if the values are deeply equal, false otherwise.
|
|
65
|
+
*/
|
|
66
|
+
export async function customFastEqual<T>(x: T, y: T): Promise<boolean> {
|
|
67
|
+
if (cachedCustomEqual !== undefined) {
|
|
68
|
+
return cachedCustomEqual(x, y);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { createCustomEqual } = await import("fast-equals");
|
|
72
|
+
|
|
73
|
+
cachedCustomEqual = createCustomEqual({
|
|
74
|
+
createCustomConfig: (defaultConfig) => ({
|
|
75
|
+
areTypedArraysEqual: (a, b, state) => {
|
|
76
|
+
// Node.js uses an internal pool for small Buffers, so multiple Buffers can
|
|
77
|
+
// share the same underlying ArrayBuffer while having different byteOffsets.
|
|
78
|
+
// Structural equality checks (e.g. deep equality) consider offset and length
|
|
79
|
+
// and may fail even if the contents are identical.
|
|
80
|
+
// We use Buffer.equals() to compare content only.
|
|
81
|
+
if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) {
|
|
82
|
+
return a.equals(b);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return defaultConfig.areTypedArraysEqual(a, b, state);
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return cachedCustomEqual(x, y);
|
|
91
|
+
}
|
package/src/internal/request.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DispatcherOptions, RequestOptions } from "../request.js";
|
|
|
2
2
|
import type EventEmitter from "node:events";
|
|
3
3
|
import type UndiciT from "undici";
|
|
4
4
|
|
|
5
|
+
import crypto from "node:crypto";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
|
|
7
8
|
import { mkdir } from "../fs.js";
|
|
@@ -24,7 +25,7 @@ export async function generateTempFilePath(filePath: string): Promise<string> {
|
|
|
24
25
|
return path.format({
|
|
25
26
|
dir,
|
|
26
27
|
ext,
|
|
27
|
-
name: `tmp-${name}`,
|
|
28
|
+
name: `tmp-${name}-${crypto.randomBytes(8).toString("hex")}`,
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
31
|
|
package/src/lang.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
customFastEqual,
|
|
3
|
+
deepMergeImpl,
|
|
4
|
+
getDeepCloneFunction,
|
|
5
|
+
} from "./internal/lang.js";
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Creates a deep clone of the provided value.
|
|
@@ -20,9 +24,7 @@ export async function deepClone<T>(value: T): Promise<T> {
|
|
|
20
24
|
* @returns True if the values are deeply equal, false otherwise.
|
|
21
25
|
*/
|
|
22
26
|
export async function deepEqual<T>(x: T, y: T): Promise<boolean> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return _deepEqual(x, y);
|
|
27
|
+
return customFastEqual(x, y);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/**
|
package/src/request.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type EventEmitter from "node:events";
|
|
2
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
3
|
import type { ParsedUrlQueryInput } from "node:querystring";
|
|
3
4
|
import type UndiciT from "undici";
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
+
import { open } from "node:fs/promises";
|
|
6
7
|
import querystring from "node:querystring";
|
|
7
8
|
import stream from "node:stream/promises";
|
|
8
9
|
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
RequestError,
|
|
13
14
|
DispatcherError,
|
|
14
15
|
} from "./errors/request.js";
|
|
15
|
-
import { move } from "./fs.js";
|
|
16
|
+
import { move, remove } from "./fs.js";
|
|
16
17
|
import {
|
|
17
18
|
generateTempFilePath,
|
|
18
19
|
getBaseDispatcherOptions,
|
|
@@ -217,11 +218,12 @@ export async function download(
|
|
|
217
218
|
dispatcherOrDispatcherOptions?: UndiciT.Dispatcher | DispatcherOptions,
|
|
218
219
|
): Promise<void> {
|
|
219
220
|
let statusCode: number | undefined;
|
|
221
|
+
let tempFilePath: string | undefined;
|
|
220
222
|
|
|
221
223
|
try {
|
|
222
224
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
223
225
|
-- We need the full Dispatcher.ResponseData here for stream.pipeline,
|
|
224
|
-
but HttpResponse doesn
|
|
226
|
+
but HttpResponse doesn't expose the raw ReadableStream.
|
|
225
227
|
TODO: wrap undici's request so we can keep the public API
|
|
226
228
|
strictly typed without falling back to Undici types. */
|
|
227
229
|
const response = (await getRequest(
|
|
@@ -236,13 +238,34 @@ export async function download(
|
|
|
236
238
|
throw new Error(await body.text());
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
tempFilePath = await generateTempFilePath(destination);
|
|
242
|
+
|
|
243
|
+
let fileHandle: FileHandle | undefined;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
fileHandle = await open(tempFilePath, "w");
|
|
247
|
+
|
|
248
|
+
const fileStream = fileHandle.createWriteStream();
|
|
249
|
+
|
|
250
|
+
await stream.pipeline(body, fileStream);
|
|
251
|
+
} finally {
|
|
252
|
+
// NOTE: Historically, not closing the file handle caused issues on Windows,
|
|
253
|
+
// for example, when trying to move the file previously written to by this function
|
|
254
|
+
await fileHandle?.close();
|
|
255
|
+
}
|
|
256
|
+
|
|
242
257
|
await move(tempFilePath, destination);
|
|
243
258
|
} catch (e) {
|
|
244
259
|
ensureError(e);
|
|
245
260
|
|
|
261
|
+
if (tempFilePath !== undefined) {
|
|
262
|
+
try {
|
|
263
|
+
await remove(tempFilePath);
|
|
264
|
+
} catch {
|
|
265
|
+
// Best-effort: file may not exist or may have already been moved
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
246
269
|
handleError(e, url);
|
|
247
270
|
|
|
248
271
|
throw new DownloadError(url, e);
|