@nomicfoundation/hardhat-utils 4.0.5 → 4.1.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/src/bytecode.d.ts +4 -0
  3. package/dist/src/bytecode.d.ts.map +1 -1
  4. package/dist/src/bytecode.js +12 -7
  5. package/dist/src/bytecode.js.map +1 -1
  6. package/dist/src/crypto.d.ts.map +1 -1
  7. package/dist/src/crypto.js +10 -2
  8. package/dist/src/crypto.js.map +1 -1
  9. package/dist/src/debug.d.ts +40 -8
  10. package/dist/src/debug.d.ts.map +1 -1
  11. package/dist/src/debug.js +54 -18
  12. package/dist/src/debug.js.map +1 -1
  13. package/dist/src/fast-semver.d.ts +49 -0
  14. package/dist/src/fast-semver.d.ts.map +1 -0
  15. package/dist/src/fast-semver.js +73 -0
  16. package/dist/src/fast-semver.js.map +1 -0
  17. package/dist/src/fs.d.ts +47 -0
  18. package/dist/src/fs.d.ts.map +1 -1
  19. package/dist/src/fs.js +204 -48
  20. package/dist/src/fs.js.map +1 -1
  21. package/dist/src/internal/bytecode.d.ts +11 -0
  22. package/dist/src/internal/bytecode.d.ts.map +1 -1
  23. package/dist/src/internal/bytecode.js +39 -2
  24. package/dist/src/internal/bytecode.js.map +1 -1
  25. package/dist/src/internal/debug.d.ts +35 -0
  26. package/dist/src/internal/debug.d.ts.map +1 -0
  27. package/dist/src/internal/debug.js +91 -0
  28. package/dist/src/internal/debug.js.map +1 -0
  29. package/dist/src/internal/fs.d.ts +15 -0
  30. package/dist/src/internal/fs.d.ts.map +1 -1
  31. package/dist/src/internal/fs.js +44 -0
  32. package/dist/src/internal/fs.js.map +1 -1
  33. package/dist/src/internal/global-dir.d.ts +2 -2
  34. package/dist/src/internal/global-dir.d.ts.map +1 -1
  35. package/dist/src/internal/global-dir.js +7 -1
  36. package/dist/src/internal/global-dir.js.map +1 -1
  37. package/dist/src/internal/lang.d.ts +3 -2
  38. package/dist/src/internal/lang.d.ts.map +1 -1
  39. package/dist/src/internal/lang.js +12 -6
  40. package/dist/src/internal/lang.js.map +1 -1
  41. package/dist/src/lang.js +2 -2
  42. package/dist/src/lang.js.map +1 -1
  43. package/dist/src/request.d.ts +11 -11
  44. package/dist/src/request.d.ts.map +1 -1
  45. package/dist/src/request.js +6 -6
  46. package/dist/src/request.js.map +1 -1
  47. package/dist/src/subprocess.d.ts +2 -0
  48. package/dist/src/subprocess.d.ts.map +1 -1
  49. package/dist/src/subprocess.js +1 -0
  50. package/dist/src/subprocess.js.map +1 -1
  51. package/dist/src/synchronization.d.ts +97 -0
  52. package/dist/src/synchronization.d.ts.map +1 -1
  53. package/dist/src/synchronization.js +177 -8
  54. package/dist/src/synchronization.js.map +1 -1
  55. package/package.json +3 -4
  56. package/src/bytecode.ts +15 -6
  57. package/src/crypto.ts +15 -2
  58. package/src/debug.ts +73 -28
  59. package/src/fast-semver.ts +95 -0
  60. package/src/fs.ts +282 -76
  61. package/src/internal/bytecode.ts +64 -5
  62. package/src/internal/debug.ts +119 -0
  63. package/src/internal/fs.ts +70 -0
  64. package/src/internal/global-dir.ts +10 -4
  65. package/src/internal/lang.ts +15 -6
  66. package/src/lang.ts +2 -2
  67. package/src/request.ts +16 -16
  68. package/src/subprocess.ts +2 -0
  69. package/src/synchronization.ts +217 -9
package/src/fs.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { JsonTypes, ParsedElementInfo } from "@streamparser/json-node";
1
+ import type * as StreamParserJson from "@streamparser/json-node";
2
+ import type * as JsonStreamStringify from "json-stream-stringify";
2
3
  import type { FileHandle } from "node:fs/promises";
3
4
 
4
5
  import fsPromises from "node:fs/promises";
@@ -6,9 +7,6 @@ import { tmpdir } from "node:os";
6
7
  import path from "node:path";
7
8
  import { pipeline } from "node:stream/promises";
8
9
 
9
- import { JSONParser } from "@streamparser/json-node";
10
- import { JsonStreamStringify } from "json-stream-stringify";
11
-
12
10
  import { ensureError, ensureNodeErrnoExceptionError } from "./error.js";
13
11
  import {
14
12
  FileNotFoundError,
@@ -21,10 +19,265 @@ import {
21
19
  DirectoryNotEmptyError,
22
20
  } from "./errors/fs.js";
23
21
  import {
24
- isDirectoryDirentAware,
25
- readdirWithFileTypesOrEmpty,
22
+ collectAllDirectoriesMatching,
23
+ collectAllFilesMatching,
26
24
  } from "./internal/fs.js";
27
25
 
26
+ // We don't load @streamparser/json-node on startup because it's only
27
+ // used by readJsonFileAsStream for very large JSON files.
28
+ let streamParserJson: typeof StreamParserJson | undefined;
29
+
30
+ // We don't load json-stream-stringify on startup because it's only
31
+ // used by writeJsonFileAsStream for very large JSON objects.
32
+ let jsonStreamStringify: typeof JsonStreamStringify | undefined;
33
+
34
+ const AMBIGUOUS_CASING_DIR_ENTRY = Symbol("ambiguous");
35
+
36
+ type CaseFoldedEntry = string | typeof AMBIGUOUS_CASING_DIR_ENTRY;
37
+
38
+ /**
39
+ * The entries in a directory, which stores their exact name, and also a
40
+ * case-folded mapping to support case-insensitive lookups.
41
+ */
42
+ interface DirEntries {
43
+ /**
44
+ * The exact names present in the directory, as returned by `readdir`.
45
+ */
46
+ readonly exactNames: Set<string>;
47
+
48
+ /**
49
+ * Case-folded key -> the actual on-disk spelling, or
50
+ * AMBIGUOUS_CASING_DIR_ENTRY when multiple entries in the directory fold to
51
+ * the same key.
52
+ */
53
+ readonly caseFoldedNames: Map<string, CaseFoldedEntry>;
54
+ }
55
+
56
+ /**
57
+ * Resolves paths to their true (on-disk) casing, caching directory listings
58
+ * and resolutions so repeated lookups against the same directories don't re-hit
59
+ * the filesystem.
60
+ *
61
+ * Intended to be used in hot paths where the same `from` directories are seen
62
+ * over and over.
63
+ *
64
+ * Does not resolve symbolic links.
65
+ *
66
+ * This class caches successful resolutions internally, and may do some
67
+ * duplicate work when multiple concurrent lookups for the same path are made
68
+ * before the first one finishes. It does not cache failed resolutions as
69
+ * negative result entries, but it does cache directory listings, so filesystem
70
+ * changes may not be observed until `clear()` is called. After `clear()`,
71
+ * previously cached directory and resolution data is discarded. If profiling
72
+ * shows that this work duplication is a problem, we can either cache in-flight
73
+ * operations, or add a mutex.
74
+ */
75
+ export class TrueCasePathResolver {
76
+ /**
77
+ * A cache of DirEntries for the directories we've seen, keyed by their
78
+ * normalized absolute path as read. For example, if the same physical
79
+ * directory is read as `/a/foo` and `/a/Foo`, each path gets its own entry.
80
+ */
81
+ readonly #dirCache = new Map<string, DirEntries>();
82
+
83
+ /**
84
+ * A cache of successful resolutions, grouped by their `from` trusted starting
85
+ * directory.
86
+ *
87
+ * The outer key is the normalized absolute `from` path, and the inner key is
88
+ * the normalized `relativePath`.
89
+ *
90
+ * This keeps paths like `/a/B` + `foo.ts` distinct from `/a` + `B/foo.ts`,
91
+ * even if they point to the same location.
92
+ */
93
+ readonly #resultCache = new Map<string, Map<string, string>>();
94
+
95
+ /**
96
+ * Determines the true-case path of a given relative path from a specified
97
+ * directory, without resolving symbolic links.
98
+ *
99
+ * Note that the casing of the `from` path is not checked against the
100
+ * filesystem, and is trusted as-is. This avoids unnecessary directory
101
+ * listings for every ancestor of `from`, which can result in permission
102
+ * errors for directories that are otherwise accessible.
103
+ *
104
+ * @param from The absolute path of the directory to start the search from.
105
+ * @param relativePath The relative path to get the true case of.
106
+ * @returns The true case of the relative path. Returns an empty string if
107
+ * relativePath points to from.
108
+ * @throws FileNotFoundError if the starting directory or the relative path
109
+ * doesn't exist or is ambiguous.
110
+ * @throws NotADirectoryError if the starting directory, or an intermediate
111
+ * segment, is not a directory.
112
+ * @throws FileSystemAccessError for any other error.
113
+ */
114
+ public async getFileTrueCase(
115
+ from: string,
116
+ relativePath: string,
117
+ ): Promise<string> {
118
+ const absoluteFrom = path.resolve(from);
119
+
120
+ if (path.normalize(relativePath) === ".") {
121
+ // There's no casing to resolve, but we still read `from` so that callers
122
+ // get the documented FileNotFoundError / NotADirectoryError if it
123
+ // doesn't exist or isn't a directory.
124
+ await this.#getDirEntries(absoluteFrom);
125
+ return "";
126
+ }
127
+
128
+ const resolved = await this.#resolveFrom(absoluteFrom, relativePath);
129
+ const resolvedRelativePath = path.relative(absoluteFrom, resolved);
130
+
131
+ if (
132
+ resolvedRelativePath === ".." ||
133
+ resolvedRelativePath.startsWith(`..${path.sep}`) ||
134
+ path.isAbsolute(resolvedRelativePath)
135
+ ) {
136
+ throw new FileNotFoundError(path.resolve(absoluteFrom, relativePath));
137
+ }
138
+
139
+ return resolvedRelativePath;
140
+ }
141
+
142
+ /**
143
+ * Clears all cached directory listings and resolutions.
144
+ */
145
+ public clear(): void {
146
+ this.#dirCache.clear();
147
+ this.#resultCache.clear();
148
+ }
149
+
150
+ async #resolveFrom(from: string, relativePath: string): Promise<string> {
151
+ const fromCacheKey = this.#getResultFromCacheKey(from);
152
+ const relativePathCacheKey =
153
+ this.#getResultRelativePathCacheKey(relativePath);
154
+
155
+ const cached = this.#resultCache
156
+ .get(fromCacheKey)
157
+ ?.get(relativePathCacheKey);
158
+
159
+ if (cached !== undefined) {
160
+ return cached;
161
+ }
162
+
163
+ const resolved = await this.#doResolveFrom(from, relativePath);
164
+
165
+ let resultsFromCache = this.#resultCache.get(fromCacheKey);
166
+ if (resultsFromCache === undefined) {
167
+ resultsFromCache = new Map<string, string>();
168
+ this.#resultCache.set(fromCacheKey, resultsFromCache);
169
+ }
170
+
171
+ resultsFromCache.set(relativePathCacheKey, resolved);
172
+
173
+ return resolved;
174
+ }
175
+
176
+ async #doResolveFrom(from: string, relativePath: string): Promise<string> {
177
+ let currentPath = from;
178
+
179
+ const segments = path
180
+ .normalize(relativePath)
181
+ .split(path.sep)
182
+ .filter((s) => s.length > 0 || s === ".");
183
+
184
+ for (const requestedName of segments) {
185
+ if (requestedName === "..") {
186
+ currentPath = path.join(currentPath, requestedName);
187
+ continue;
188
+ }
189
+
190
+ const entries = await this.#getDirEntries(currentPath);
191
+ const actualName = this.#lookupChild(entries, requestedName);
192
+
193
+ if (actualName === undefined) {
194
+ throw new FileNotFoundError(path.resolve(from, relativePath));
195
+ }
196
+
197
+ currentPath = path.join(currentPath, actualName);
198
+ }
199
+
200
+ return currentPath;
201
+ }
202
+
203
+ async #getDirEntries(dirPath: string): Promise<DirEntries> {
204
+ const cacheKey = this.#getDirCacheKey(dirPath);
205
+ const cached = this.#dirCache.get(cacheKey);
206
+ if (cached !== undefined) {
207
+ return cached;
208
+ }
209
+
210
+ const entries = await this.#readDirEntries(dirPath);
211
+ this.#dirCache.set(cacheKey, entries);
212
+
213
+ return entries;
214
+ }
215
+
216
+ async #readDirEntries(dirPath: string): Promise<DirEntries> {
217
+ const names = await readdir(dirPath);
218
+
219
+ const exactNames = new Set<string>();
220
+ const caseFoldedNames = new Map<string, CaseFoldedEntry>();
221
+
222
+ for (const name of names) {
223
+ exactNames.add(name);
224
+
225
+ const folded = this.#caseFold(name);
226
+ const previous = caseFoldedNames.get(folded);
227
+ if (previous === undefined) {
228
+ caseFoldedNames.set(folded, name);
229
+ } else if (previous !== name) {
230
+ caseFoldedNames.set(folded, AMBIGUOUS_CASING_DIR_ENTRY);
231
+ }
232
+ }
233
+
234
+ return { exactNames, caseFoldedNames };
235
+ }
236
+
237
+ #lookupChild(entries: DirEntries, requestedName: string): string | undefined {
238
+ if (entries.exactNames.has(requestedName)) {
239
+ return requestedName;
240
+ }
241
+
242
+ const candidate = entries.caseFoldedNames.get(
243
+ this.#caseFold(requestedName),
244
+ );
245
+
246
+ if (candidate === undefined || candidate === AMBIGUOUS_CASING_DIR_ENTRY) {
247
+ return undefined;
248
+ }
249
+
250
+ return candidate;
251
+ }
252
+
253
+ #getResultFromCacheKey(from: string): string {
254
+ return path.normalize(from);
255
+ }
256
+
257
+ #getResultRelativePathCacheKey(relativePath: string): string {
258
+ return path.normalize(relativePath);
259
+ }
260
+
261
+ #getDirCacheKey(dirPath: string): string {
262
+ return path.normalize(dirPath);
263
+ }
264
+
265
+ /**
266
+ * Returns a case-folded version of the given name, which can be thought of as
267
+ * a "normalized uppercase" form. This is used to implement case-insensitive
268
+ * comparisons.
269
+ *
270
+ * This is not an exact match with what every filesystem would do, but a good
271
+ * enough approximation for our purposes.
272
+ *
273
+ * @param name The name to fold.
274
+ * @returns The case-folded version of the name.
275
+ */
276
+ #caseFold(name: string): string {
277
+ return name.normalize("NFC").toUpperCase();
278
+ }
279
+ }
280
+
28
281
  /**
29
282
  * Determines the canonical pathname for a given path, resolving any symbolic
30
283
  * links, and returns it.
@@ -63,33 +316,10 @@ export async function getAllFilesMatching(
63
316
  matches?: (absolutePathToFile: string) => Promise<boolean> | boolean,
64
317
  directoryFilter?: (absolutePathToDir: string) => Promise<boolean> | boolean,
65
318
  ): Promise<string[]> {
66
- const dirContent = await readdirWithFileTypesOrEmpty(dirFrom);
67
-
68
- const results = await Promise.all(
69
- dirContent.map(async (dirent) => {
70
- const absolutePathToFile = path.join(dirFrom, dirent.name);
71
- if (await isDirectoryDirentAware(absolutePathToFile, dirent)) {
72
- if (
73
- directoryFilter === undefined ||
74
- (await directoryFilter(absolutePathToFile))
75
- ) {
76
- return await getAllFilesMatching(
77
- absolutePathToFile,
78
- matches,
79
- directoryFilter,
80
- );
81
- }
319
+ const results: string[] = [];
320
+ await collectAllFilesMatching(dirFrom, results, matches, directoryFilter);
82
321
 
83
- return [];
84
- } else if (matches === undefined || (await matches(absolutePathToFile))) {
85
- return absolutePathToFile;
86
- } else {
87
- return [];
88
- }
89
- }),
90
- );
91
-
92
- return results.flat();
322
+ return results;
93
323
  }
94
324
 
95
325
  /**
@@ -111,24 +341,10 @@ export async function getAllDirectoriesMatching(
111
341
  dirFrom: string,
112
342
  matches?: (absolutePathToDir: string) => Promise<boolean> | boolean,
113
343
  ): Promise<string[]> {
114
- const dirContent = await readdirWithFileTypesOrEmpty(dirFrom);
115
-
116
- const results = await Promise.all(
117
- dirContent.map(async (dirent) => {
118
- const absolutePathToFile = path.join(dirFrom, dirent.name);
119
- if (!(await isDirectoryDirentAware(absolutePathToFile, dirent))) {
120
- return [];
121
- }
122
-
123
- if (matches === undefined || (await matches(absolutePathToFile))) {
124
- return absolutePathToFile;
125
- }
344
+ const results: string[] = [];
345
+ await collectAllDirectoriesMatching(dirFrom, results, matches);
126
346
 
127
- return await getAllDirectoriesMatching(absolutePathToFile, matches);
128
- }),
129
- );
130
-
131
- return results.flat();
347
+ return results;
132
348
  }
133
349
 
134
350
  /**
@@ -141,34 +357,13 @@ export async function getAllDirectoriesMatching(
141
357
  * @throws FileNotFoundError if the starting directory or the relative path doesn't exist.
142
358
  * @throws NotADirectoryError if the starting directory is not a directory.
143
359
  * @throws FileSystemAccessError for any other error.
360
+ * @deprecated Use {@link TrueCasePathResolver} instead.
144
361
  */
145
362
  export async function getFileTrueCase(
146
363
  from: string,
147
364
  relativePath: string,
148
365
  ): Promise<string> {
149
- const dirEntries = await readdirOrEmpty(from);
150
-
151
- const segments = relativePath.split(path.sep);
152
- const nextDir = segments[0];
153
- const nextDirLowerCase = nextDir.toLowerCase();
154
-
155
- for (const dirEntry of dirEntries) {
156
- if (dirEntry.toLowerCase() === nextDirLowerCase) {
157
- if (segments.length === 1) {
158
- return dirEntry;
159
- }
160
-
161
- return path.join(
162
- dirEntry,
163
- await getFileTrueCase(
164
- path.join(from, dirEntry),
165
- path.relative(nextDir, relativePath),
166
- ),
167
- );
168
- }
169
- }
170
-
171
- throw new FileNotFoundError(path.join(from, relativePath));
366
+ return await new TrueCasePathResolver().getFileTrueCase(from, relativePath);
172
367
  }
173
368
 
174
369
  /**
@@ -233,9 +428,13 @@ export async function readJsonFileAsStream<T>(
233
428
 
234
429
  const fileReadStream = fileHandle.createReadStream();
235
430
 
431
+ if (streamParserJson === undefined) {
432
+ streamParserJson = await import("@streamparser/json-node");
433
+ }
434
+
236
435
  // NOTE: We set a separator to disable self-closing to be able to use the parser
237
436
  // in the stream.pipeline context; see https://github.com/juanjoDiaz/streamparser-json/issues/47
238
- const jsonParser = new JSONParser({
437
+ const jsonParser = new streamParserJson.JSONParser({
239
438
  separator: "",
240
439
  });
241
440
 
@@ -243,9 +442,12 @@ export async function readJsonFileAsStream<T>(
243
442
  fileReadStream,
244
443
  jsonParser,
245
444
  async (
246
- elements: AsyncIterable<ParsedElementInfo.ParsedElementInfo>,
445
+ elements: AsyncIterable<StreamParserJson.ParsedElementInfo.ParsedElementInfo>,
247
446
  ): Promise<any | undefined> => {
248
- let value: JsonTypes.JsonPrimitive | JsonTypes.JsonStruct | undefined;
447
+ let value:
448
+ | StreamParserJson.JsonTypes.JsonPrimitive
449
+ | StreamParserJson.JsonTypes.JsonStruct
450
+ | undefined;
249
451
  for await (const element of elements) {
250
452
  value = element.value;
251
453
  }
@@ -334,7 +536,11 @@ export async function writeJsonFileAsStream<T>(
334
536
  try {
335
537
  fileHandle = await fsPromises.open(absolutePathToFile, "w");
336
538
 
337
- const jsonStream = new JsonStreamStringify(object);
539
+ if (jsonStreamStringify === undefined) {
540
+ jsonStreamStringify = await import("json-stream-stringify");
541
+ }
542
+
543
+ const jsonStream = new jsonStreamStringify.JsonStreamStringify(object);
338
544
  const fileWriteStream = fileHandle.createWriteStream();
339
545
 
340
546
  await pipeline(jsonStream, fileWriteStream);
@@ -29,6 +29,17 @@ export interface LibraryAddresses {
29
29
  [contractName: string]: PrefixedHexString;
30
30
  }
31
31
 
32
+ export interface BytecodeReplacement {
33
+ start: number;
34
+ length: number;
35
+ value: string;
36
+ }
37
+
38
+ interface LibraryLinksIndex {
39
+ byName: Map<string, LibraryLink[]>;
40
+ byFqn: Map<string, LibraryLink>;
41
+ }
42
+
32
43
  /**
33
44
  * Check that the provided library addresses are valid Ethereum addresses.
34
45
  * If any of them are not, an InvalidLibraryAddressError is thrown.
@@ -61,13 +72,15 @@ export function checkAmbiguousOrUnnecessaryLinks(
61
72
  ): void {
62
73
  const ambiguousLibraries: Record<string, LibraryLink[]> = {};
63
74
  const unnecessaryLibraries: string[] = [];
75
+ const neededLibrariesIndex = indexLibraryLinks(neededLibraries);
64
76
 
65
77
  for (const providedLibraryName of Object.keys(providedLibraries)) {
66
- const matchingLibraries = neededLibraries.filter(
67
- ({ libraryName, libraryFqn }) =>
68
- libraryName === providedLibraryName ||
69
- libraryFqn === providedLibraryName,
70
- );
78
+ const matchingLibraryByFqn =
79
+ neededLibrariesIndex.byFqn.get(providedLibraryName);
80
+ const matchingLibraries =
81
+ matchingLibraryByFqn !== undefined
82
+ ? [matchingLibraryByFqn]
83
+ : neededLibrariesIndex.byName.get(providedLibraryName) ?? [];
71
84
 
72
85
  if (matchingLibraries.length > 1) {
73
86
  ambiguousLibraries[providedLibraryName] = matchingLibraries;
@@ -85,6 +98,24 @@ export function checkAmbiguousOrUnnecessaryLinks(
85
98
  }
86
99
  }
87
100
 
101
+ function indexLibraryLinks(neededLibraries: LibraryLink[]): LibraryLinksIndex {
102
+ const byName = new Map<string, LibraryLink[]>();
103
+ const byFqn = new Map<string, LibraryLink>();
104
+
105
+ for (const neededLibrary of neededLibraries) {
106
+ byFqn.set(neededLibrary.libraryFqn, neededLibrary);
107
+
108
+ const sameNameLibraries = byName.get(neededLibrary.libraryName);
109
+ if (sameNameLibraries === undefined) {
110
+ byName.set(neededLibrary.libraryName, [neededLibrary]);
111
+ } else {
112
+ sameNameLibraries.push(neededLibrary);
113
+ }
114
+ }
115
+
116
+ return { byName, byFqn };
117
+ }
118
+
88
119
  /**
89
120
  * Check that each library is only provided once, either by its name or its
90
121
  * fully qualified name. If a library is provided more than once, an
@@ -127,3 +158,31 @@ export function checkMissingLibraryAddresses(
127
158
 
128
159
  throw new MissingLibrariesError(missingLibraries);
129
160
  }
161
+
162
+ /**
163
+ * Apply a set of replacements to a bytecode string, returning the resulting
164
+ * bytecode. Each replacement overwrites `length` characters starting at
165
+ * `start` with `value`. Replacements must not overlap.
166
+ */
167
+ export function applyBytecodeReplacements(
168
+ bytecode: string,
169
+ replacements: BytecodeReplacement[],
170
+ ): string {
171
+ if (replacements.length === 0) {
172
+ return bytecode;
173
+ }
174
+
175
+ replacements.sort((a, b) => a.start - b.start);
176
+
177
+ const parts: string[] = [];
178
+ let position = 0;
179
+
180
+ for (const { start, length, value } of replacements) {
181
+ parts.push(bytecode.slice(position, start), value);
182
+ position = start + length;
183
+ }
184
+
185
+ parts.push(bytecode.slice(position));
186
+
187
+ return parts.join("");
188
+ }
@@ -0,0 +1,119 @@
1
+ import type { DebugLogger } from "../debug.js";
2
+
3
+ export const NOOP: DebugLogger = Object.assign(
4
+ (_format: unknown, ..._args: unknown[]): void => {},
5
+ { enabled: false },
6
+ );
7
+
8
+ interface ParsedPatterns {
9
+ include: RegExp[];
10
+ exclude: RegExp[];
11
+ }
12
+
13
+ let cached: { env: string; parsed: ParsedPatterns } | undefined;
14
+
15
+ /**
16
+ * Parses a `DEBUG`-style pattern string into include/exclude regex lists.
17
+ * Patterns prefixed with `-` go into `exclude`; everything else into `include`.
18
+ *
19
+ * Results are memoized against the last-seen `env` string, since
20
+ * `process.env.DEBUG` is effectively constant within a process and
21
+ * `createDebug` is called many times at startup.
22
+ */
23
+ export function parsePatterns(env: string): ParsedPatterns {
24
+ if (cached !== undefined && cached.env === env) {
25
+ return cached.parsed;
26
+ }
27
+
28
+ const include: RegExp[] = [];
29
+ const exclude: RegExp[] = [];
30
+
31
+ for (const raw of env.split(/[\s,]+/)) {
32
+ if (raw === "") {
33
+ continue;
34
+ }
35
+ if (raw.startsWith("-")) {
36
+ exclude.push(namespaceToRegExp(raw.slice(1)));
37
+ } else {
38
+ include.push(namespaceToRegExp(raw));
39
+ }
40
+ }
41
+
42
+ const parsed: ParsedPatterns = { include, exclude };
43
+ cached = { env, parsed };
44
+ return parsed;
45
+ }
46
+
47
+ /**
48
+ * Converts a `DEBUG`-style namespace pattern into an anchored regex.
49
+ * Regex metacharacters are escaped to match literally, and `*` is translated
50
+ * into `.*?` so it behaves as a glob wildcard.
51
+ */
52
+ function namespaceToRegExp(namespace: string): RegExp {
53
+ const escaped = namespace
54
+ .replace(/[.+?^${}()|[\]\\]/g, "\\$&")
55
+ .replace(/\*/g, ".*?");
56
+
57
+ return new RegExp(`^${escaped}$`);
58
+ }
59
+
60
+ /**
61
+ * Checks whether a namespace is enabled under the given `DEBUG` pattern string.
62
+ */
63
+ export function isEnabled(namespace: string, env: string): boolean {
64
+ if (env === "") {
65
+ return false;
66
+ }
67
+
68
+ const { include, exclude } = parsePatterns(env);
69
+
70
+ return (
71
+ !exclude.some((p) => p.test(namespace)) &&
72
+ include.some((p) => p.test(namespace))
73
+ );
74
+ }
75
+
76
+ // `debug@4`'s 76-colour 256-palette with the red and red-orange families
77
+ // stripped, so red stays reserved for error output.
78
+ export const COLORS: readonly number[] = [
79
+ 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68,
80
+ 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 128, 129, 134,
81
+ 135, 148, 149, 178, 179, 184, 185, 201, 214, 215, 220, 221,
82
+ ];
83
+
84
+ /**
85
+ * Picks an ANSI 256-colour code for a namespace, deterministic from its
86
+ * characters.
87
+ *
88
+ * Uses a 32-bit FNV-1a hash followed by an xorshift finalizer. The finalizer
89
+ * improves bit avalanche so the low bits used by `% COLORS.length`
90
+ * distribute Hardhat's namespaces more evenly across the palette.
91
+ */
92
+ export function selectColor(namespace: string): number {
93
+ let hash = 0x811c9dc5;
94
+
95
+ for (let i = 0; i < namespace.length; i++) {
96
+ // eslint-disable-next-line no-bitwise -- FNV-1a hash
97
+ hash ^= namespace.charCodeAt(i);
98
+ hash = Math.imul(hash, 0x01000193);
99
+ }
100
+
101
+ // xorshift finalizer for better avalanche on the low bits
102
+ /* eslint-disable no-bitwise -- xorshift finalizer */
103
+ hash ^= hash >>> 16;
104
+ hash = Math.imul(hash, 0x85ebca6b);
105
+ hash ^= hash >>> 13;
106
+ /* eslint-enable no-bitwise */
107
+
108
+ return COLORS[Math.abs(hash) % COLORS.length];
109
+ }
110
+
111
+ /**
112
+ * Reports whether ANSI colours should be used, honouring `DEBUG_COLORS` and TTY.
113
+ */
114
+ export function useColors(): boolean {
115
+ const isDisabled =
116
+ process.env.DEBUG_COLORS === "no" || process.env.DEBUG_COLORS === "false";
117
+
118
+ return !isDisabled && process.stderr.isTTY === true;
119
+ }