@nomicfoundation/hardhat-utils 3.0.6 → 4.0.1
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/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/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/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 +1 -0
- package/dist/src/lang.d.ts.map +1 -1
- package/dist/src/lang.js +13 -0
- 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 +1 -1
- package/src/errors/synchronization.ts +75 -0
- package/src/format.ts +11 -94
- package/src/global-dir.ts +31 -0
- package/src/internal/format.ts +3 -3
- package/src/internal/request.ts +2 -1
- package/src/lang.ts +15 -0
- 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/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/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
|
@@ -118,3 +118,18 @@ export function bindAllMethods<ObjectT extends object>(obj: ObjectT): void {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
export function findDuplicates<T>(arr: T[]): Set<T> {
|
|
123
|
+
const seen = new Set<T>();
|
|
124
|
+
const duplicates = new Set<T>();
|
|
125
|
+
|
|
126
|
+
for (const item of arr) {
|
|
127
|
+
if (seen.has(item)) {
|
|
128
|
+
duplicates.add(item);
|
|
129
|
+
} else {
|
|
130
|
+
seen.add(item);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return duplicates;
|
|
135
|
+
}
|
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);
|