@nomicfoundation/hardhat-utils 4.0.4 → 4.1.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/src/debug.d.ts +40 -8
  3. package/dist/src/debug.d.ts.map +1 -1
  4. package/dist/src/debug.js +54 -18
  5. package/dist/src/debug.js.map +1 -1
  6. package/dist/src/fast-semver.d.ts +49 -0
  7. package/dist/src/fast-semver.d.ts.map +1 -0
  8. package/dist/src/fast-semver.js +73 -0
  9. package/dist/src/fast-semver.js.map +1 -0
  10. package/dist/src/fs.d.ts +57 -0
  11. package/dist/src/fs.d.ts.map +1 -1
  12. package/dist/src/fs.js +200 -25
  13. package/dist/src/fs.js.map +1 -1
  14. package/dist/src/internal/debug.d.ts +35 -0
  15. package/dist/src/internal/debug.d.ts.map +1 -0
  16. package/dist/src/internal/debug.js +91 -0
  17. package/dist/src/internal/debug.js.map +1 -0
  18. package/dist/src/internal/fs.d.ts +12 -0
  19. package/dist/src/internal/fs.d.ts.map +1 -0
  20. package/dist/src/internal/fs.js +42 -0
  21. package/dist/src/internal/fs.js.map +1 -0
  22. package/dist/src/runtime.d.ts +13 -0
  23. package/dist/src/runtime.d.ts.map +1 -0
  24. package/dist/src/runtime.js +34 -0
  25. package/dist/src/runtime.js.map +1 -0
  26. package/dist/src/subprocess.d.ts +2 -0
  27. package/dist/src/subprocess.d.ts.map +1 -1
  28. package/dist/src/subprocess.js +1 -0
  29. package/dist/src/subprocess.js.map +1 -1
  30. package/dist/src/synchronization.d.ts +97 -0
  31. package/dist/src/synchronization.d.ts.map +1 -1
  32. package/dist/src/synchronization.js +171 -2
  33. package/dist/src/synchronization.js.map +1 -1
  34. package/package.json +4 -4
  35. package/src/debug.ts +73 -28
  36. package/src/fast-semver.ts +95 -0
  37. package/src/fs.ts +272 -35
  38. package/src/internal/debug.ts +119 -0
  39. package/src/internal/fs.ts +57 -0
  40. package/src/runtime.ts +45 -0
  41. package/src/subprocess.ts +2 -0
  42. package/src/synchronization.ts +211 -3
package/src/fs.ts CHANGED
@@ -20,6 +20,257 @@ import {
20
20
  IsDirectoryError,
21
21
  DirectoryNotEmptyError,
22
22
  } from "./errors/fs.js";
23
+ import {
24
+ isDirectoryDirentAware,
25
+ readdirWithFileTypesOrEmpty,
26
+ } from "./internal/fs.js";
27
+
28
+ const AMBIGUOUS_CASING_DIR_ENTRY = Symbol("ambiguous");
29
+
30
+ type CaseFoldedEntry = string | typeof AMBIGUOUS_CASING_DIR_ENTRY;
31
+
32
+ /**
33
+ * The entries in a directory, which stores their exact name, and also a
34
+ * case-folded mapping to support case-insensitive lookups.
35
+ */
36
+ interface DirEntries {
37
+ /**
38
+ * The exact names present in the directory, as returned by `readdir`.
39
+ */
40
+ readonly exactNames: Set<string>;
41
+
42
+ /**
43
+ * Case-folded key -> the actual on-disk spelling, or
44
+ * AMBIGUOUS_CASING_DIR_ENTRY when multiple entries in the directory fold to
45
+ * the same key.
46
+ */
47
+ readonly caseFoldedNames: Map<string, CaseFoldedEntry>;
48
+ }
49
+
50
+ /**
51
+ * Resolves paths to their true (on-disk) casing, caching directory listings
52
+ * and resolutions so repeated lookups against the same directories don't re-hit
53
+ * the filesystem.
54
+ *
55
+ * Intended to be used in hot paths where the same `from` directories are seen
56
+ * over and over.
57
+ *
58
+ * Does not resolve symbolic links.
59
+ *
60
+ * This class caches successful resolutions internally, and may do some
61
+ * duplicate work when multiple concurrent lookups for the same path are made
62
+ * before the first one finishes. It does not cache failed resolutions as
63
+ * negative result entries, but it does cache directory listings, so filesystem
64
+ * changes may not be observed until `clear()` is called. After `clear()`,
65
+ * previously cached directory and resolution data is discarded. If profiling
66
+ * shows that this work duplication is a problem, we can either cache in-flight
67
+ * operations, or add a mutex.
68
+ */
69
+ export class TrueCasePathResolver {
70
+ /**
71
+ * A cache of DirEntries for the directories we've seen, keyed by their
72
+ * normalized absolute path as read. For example, if the same physical
73
+ * directory is read as `/a/foo` and `/a/Foo`, each path gets its own entry.
74
+ */
75
+ readonly #dirCache = new Map<string, DirEntries>();
76
+
77
+ /**
78
+ * A cache of successful resolutions, grouped by their `from` trusted starting
79
+ * directory.
80
+ *
81
+ * The outer key is the normalized absolute `from` path, and the inner key is
82
+ * the normalized `relativePath`.
83
+ *
84
+ * This keeps paths like `/a/B` + `foo.ts` distinct from `/a` + `B/foo.ts`,
85
+ * even if they point to the same location.
86
+ */
87
+ readonly #resultCache = new Map<string, Map<string, string>>();
88
+
89
+ /**
90
+ * Determines the true-case path of a given relative path from a specified
91
+ * directory, without resolving symbolic links.
92
+ *
93
+ * Note that the casing of the `from` path is not checked against the
94
+ * filesystem, and is trusted as-is. This avoids unnecessary directory
95
+ * listings for every ancestor of `from`, which can result in permission
96
+ * errors for directories that are otherwise accessible.
97
+ *
98
+ * @param from The absolute path of the directory to start the search from.
99
+ * @param relativePath The relative path to get the true case of.
100
+ * @returns The true case of the relative path. Returns an empty string if
101
+ * relativePath points to from.
102
+ * @throws FileNotFoundError if the starting directory or the relative path
103
+ * doesn't exist or is ambiguous.
104
+ * @throws NotADirectoryError if the starting directory, or an intermediate
105
+ * segment, is not a directory.
106
+ * @throws FileSystemAccessError for any other error.
107
+ */
108
+ public async getFileTrueCase(
109
+ from: string,
110
+ relativePath: string,
111
+ ): Promise<string> {
112
+ const absoluteFrom = path.resolve(from);
113
+
114
+ if (path.normalize(relativePath) === ".") {
115
+ // There's no casing to resolve, but we still read `from` so that callers
116
+ // get the documented FileNotFoundError / NotADirectoryError if it
117
+ // doesn't exist or isn't a directory.
118
+ await this.#getDirEntries(absoluteFrom);
119
+ return "";
120
+ }
121
+
122
+ const resolved = await this.#resolveFrom(absoluteFrom, relativePath);
123
+ const resolvedRelativePath = path.relative(absoluteFrom, resolved);
124
+
125
+ if (
126
+ resolvedRelativePath === ".." ||
127
+ resolvedRelativePath.startsWith(`..${path.sep}`) ||
128
+ path.isAbsolute(resolvedRelativePath)
129
+ ) {
130
+ throw new FileNotFoundError(path.resolve(absoluteFrom, relativePath));
131
+ }
132
+
133
+ return resolvedRelativePath;
134
+ }
135
+
136
+ /**
137
+ * Clears all cached directory listings and resolutions.
138
+ */
139
+ public clear(): void {
140
+ this.#dirCache.clear();
141
+ this.#resultCache.clear();
142
+ }
143
+
144
+ async #resolveFrom(from: string, relativePath: string): Promise<string> {
145
+ const fromCacheKey = this.#getResultFromCacheKey(from);
146
+ const relativePathCacheKey =
147
+ this.#getResultRelativePathCacheKey(relativePath);
148
+
149
+ const cached = this.#resultCache
150
+ .get(fromCacheKey)
151
+ ?.get(relativePathCacheKey);
152
+
153
+ if (cached !== undefined) {
154
+ return cached;
155
+ }
156
+
157
+ const resolved = await this.#doResolveFrom(from, relativePath);
158
+
159
+ let resultsFromCache = this.#resultCache.get(fromCacheKey);
160
+ if (resultsFromCache === undefined) {
161
+ resultsFromCache = new Map<string, string>();
162
+ this.#resultCache.set(fromCacheKey, resultsFromCache);
163
+ }
164
+
165
+ resultsFromCache.set(relativePathCacheKey, resolved);
166
+
167
+ return resolved;
168
+ }
169
+
170
+ async #doResolveFrom(from: string, relativePath: string): Promise<string> {
171
+ let currentPath = from;
172
+
173
+ const segments = path
174
+ .normalize(relativePath)
175
+ .split(path.sep)
176
+ .filter((s) => s.length > 0 || s === ".");
177
+
178
+ for (const requestedName of segments) {
179
+ if (requestedName === "..") {
180
+ currentPath = path.join(currentPath, requestedName);
181
+ continue;
182
+ }
183
+
184
+ const entries = await this.#getDirEntries(currentPath);
185
+ const actualName = this.#lookupChild(entries, requestedName);
186
+
187
+ if (actualName === undefined) {
188
+ throw new FileNotFoundError(path.resolve(from, relativePath));
189
+ }
190
+
191
+ currentPath = path.join(currentPath, actualName);
192
+ }
193
+
194
+ return currentPath;
195
+ }
196
+
197
+ async #getDirEntries(dirPath: string): Promise<DirEntries> {
198
+ const cacheKey = this.#getDirCacheKey(dirPath);
199
+ const cached = this.#dirCache.get(cacheKey);
200
+ if (cached !== undefined) {
201
+ return cached;
202
+ }
203
+
204
+ const entries = await this.#readDirEntries(dirPath);
205
+ this.#dirCache.set(cacheKey, entries);
206
+
207
+ return entries;
208
+ }
209
+
210
+ async #readDirEntries(dirPath: string): Promise<DirEntries> {
211
+ const names = await readdir(dirPath);
212
+
213
+ const exactNames = new Set<string>();
214
+ const caseFoldedNames = new Map<string, CaseFoldedEntry>();
215
+
216
+ for (const name of names) {
217
+ exactNames.add(name);
218
+
219
+ const folded = this.#caseFold(name);
220
+ const previous = caseFoldedNames.get(folded);
221
+ if (previous === undefined) {
222
+ caseFoldedNames.set(folded, name);
223
+ } else if (previous !== name) {
224
+ caseFoldedNames.set(folded, AMBIGUOUS_CASING_DIR_ENTRY);
225
+ }
226
+ }
227
+
228
+ return { exactNames, caseFoldedNames };
229
+ }
230
+
231
+ #lookupChild(entries: DirEntries, requestedName: string): string | undefined {
232
+ if (entries.exactNames.has(requestedName)) {
233
+ return requestedName;
234
+ }
235
+
236
+ const candidate = entries.caseFoldedNames.get(
237
+ this.#caseFold(requestedName),
238
+ );
239
+
240
+ if (candidate === undefined || candidate === AMBIGUOUS_CASING_DIR_ENTRY) {
241
+ return undefined;
242
+ }
243
+
244
+ return candidate;
245
+ }
246
+
247
+ #getResultFromCacheKey(from: string): string {
248
+ return path.normalize(from);
249
+ }
250
+
251
+ #getResultRelativePathCacheKey(relativePath: string): string {
252
+ return path.normalize(relativePath);
253
+ }
254
+
255
+ #getDirCacheKey(dirPath: string): string {
256
+ return path.normalize(dirPath);
257
+ }
258
+
259
+ /**
260
+ * Returns a case-folded version of the given name, which can be thought of as
261
+ * a "normalized uppercase" form. This is used to implement case-insensitive
262
+ * comparisons.
263
+ *
264
+ * This is not an exact match with what every filesystem would do, but a good
265
+ * enough approximation for our purposes.
266
+ *
267
+ * @param name The name to fold.
268
+ * @returns The case-folded version of the name.
269
+ */
270
+ #caseFold(name: string): string {
271
+ return name.normalize("NFC").toUpperCase();
272
+ }
273
+ }
23
274
 
24
275
  /**
25
276
  * Determines the canonical pathname for a given path, resolving any symbolic
@@ -59,12 +310,12 @@ export async function getAllFilesMatching(
59
310
  matches?: (absolutePathToFile: string) => Promise<boolean> | boolean,
60
311
  directoryFilter?: (absolutePathToDir: string) => Promise<boolean> | boolean,
61
312
  ): Promise<string[]> {
62
- const dirContent = await readdirOrEmpty(dirFrom);
313
+ const dirContent = await readdirWithFileTypesOrEmpty(dirFrom);
63
314
 
64
315
  const results = await Promise.all(
65
- dirContent.map(async (file) => {
66
- const absolutePathToFile = path.join(dirFrom, file);
67
- if (await isDirectory(absolutePathToFile)) {
316
+ dirContent.map(async (dirent) => {
317
+ const absolutePathToFile = path.join(dirFrom, dirent.name);
318
+ if (await isDirectoryDirentAware(absolutePathToFile, dirent)) {
68
319
  if (
69
320
  directoryFilter === undefined ||
70
321
  (await directoryFilter(absolutePathToFile))
@@ -107,12 +358,12 @@ export async function getAllDirectoriesMatching(
107
358
  dirFrom: string,
108
359
  matches?: (absolutePathToDir: string) => Promise<boolean> | boolean,
109
360
  ): Promise<string[]> {
110
- const dirContent = await readdirOrEmpty(dirFrom);
361
+ const dirContent = await readdirWithFileTypesOrEmpty(dirFrom);
111
362
 
112
363
  const results = await Promise.all(
113
- dirContent.map(async (file) => {
114
- const absolutePathToFile = path.join(dirFrom, file);
115
- if (!(await isDirectory(absolutePathToFile))) {
364
+ dirContent.map(async (dirent) => {
365
+ const absolutePathToFile = path.join(dirFrom, dirent.name);
366
+ if (!(await isDirectoryDirentAware(absolutePathToFile, dirent))) {
116
367
  return [];
117
368
  }
118
369
 
@@ -137,34 +388,13 @@ export async function getAllDirectoriesMatching(
137
388
  * @throws FileNotFoundError if the starting directory or the relative path doesn't exist.
138
389
  * @throws NotADirectoryError if the starting directory is not a directory.
139
390
  * @throws FileSystemAccessError for any other error.
391
+ * @deprecated Use {@link TrueCasePathResolver} instead.
140
392
  */
141
393
  export async function getFileTrueCase(
142
394
  from: string,
143
395
  relativePath: string,
144
396
  ): Promise<string> {
145
- const dirEntries = await readdirOrEmpty(from);
146
-
147
- const segments = relativePath.split(path.sep);
148
- const nextDir = segments[0];
149
- const nextDirLowerCase = nextDir.toLowerCase();
150
-
151
- for (const dirEntry of dirEntries) {
152
- if (dirEntry.toLowerCase() === nextDirLowerCase) {
153
- if (segments.length === 1) {
154
- return dirEntry;
155
- }
156
-
157
- return path.join(
158
- dirEntry,
159
- await getFileTrueCase(
160
- path.join(from, dirEntry),
161
- path.relative(nextDir, relativePath),
162
- ),
163
- );
164
- }
165
- }
166
-
167
- throw new FileNotFoundError(path.join(from, relativePath));
397
+ return await new TrueCasePathResolver().getFileTrueCase(from, relativePath);
168
398
  }
169
399
 
170
400
  /**
@@ -494,17 +724,24 @@ export async function readdir(absolutePathToDir: string): Promise<string[]> {
494
724
  }
495
725
 
496
726
  /**
497
- * Wrapper around `readdir` that returns an empty array if the directory doesn't exist.
727
+ * Reads a directory and returns its content as an array of strings, returning
728
+ * an empty array if the directory doesn't exist.
498
729
  *
499
- * @see readdir
730
+ * @param absolutePathToDir The path to the directory.
731
+ * @returns An array of strings with the names of the files and directories in the directory, and an empty array if the directory doesn't exist.
732
+ * @throws NotADirectoryError if the path is not a directory.
733
+ * @throws FileSystemAccessError for any other error.
500
734
  */
501
- async function readdirOrEmpty(dirFrom: string): Promise<string[]> {
735
+ export async function readdirOrEmpty(
736
+ absolutePathToDir: string,
737
+ ): Promise<string[]> {
502
738
  try {
503
- return await readdir(dirFrom);
739
+ return await readdir(absolutePathToDir);
504
740
  } catch (error) {
505
741
  if (error instanceof FileNotFoundError) {
506
742
  return [];
507
743
  }
744
+
508
745
  throw error;
509
746
  }
510
747
  }
@@ -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
+ }
@@ -0,0 +1,57 @@
1
+ import type { Dirent } from "node:fs";
2
+
3
+ import fsPromises from "node:fs/promises";
4
+
5
+ import { ensureNodeErrnoExceptionError } from "../error.js";
6
+ import { FileSystemAccessError, NotADirectoryError } from "../errors/fs.js";
7
+ import { isDirectory } from "../fs.js";
8
+
9
+ /**
10
+ * Like `readdirOrEmpty`, but returns `Dirent` entries to know if an entry is a
11
+ * directory or not without an extra `lstat` syscall.
12
+ */
13
+ export async function readdirWithFileTypesOrEmpty(
14
+ dirFrom: string,
15
+ ): Promise<Dirent[]> {
16
+ try {
17
+ return await fsPromises.readdir(dirFrom, { withFileTypes: true });
18
+ } catch (e) {
19
+ ensureNodeErrnoExceptionError(e);
20
+
21
+ if (e.code === "ENOENT") {
22
+ return [];
23
+ }
24
+
25
+ if (e.code === "ENOTDIR") {
26
+ throw new NotADirectoryError(dirFrom, e);
27
+ }
28
+
29
+ throw new FileSystemAccessError(e.message, e);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Determines if a dirent refers to a directory, falling back to `lstat` only
35
+ * when the dirent type is unknown.
36
+ */
37
+ export async function isDirectoryDirentAware(
38
+ absolutePath: string,
39
+ dirent: Dirent,
40
+ ): Promise<boolean> {
41
+ if (dirent.isDirectory()) {
42
+ return true;
43
+ }
44
+
45
+ if (
46
+ dirent.isFile() ||
47
+ dirent.isSymbolicLink() ||
48
+ dirent.isBlockDevice() ||
49
+ dirent.isCharacterDevice() ||
50
+ dirent.isFIFO() ||
51
+ dirent.isSocket()
52
+ ) {
53
+ return false;
54
+ }
55
+
56
+ return await isDirectory(absolutePath);
57
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,45 @@
1
+ export interface RuntimeInfo {
2
+ runtime: "bun" | "deno" | "node";
3
+ version: string;
4
+ }
5
+
6
+ declare const globalThis: {
7
+ Deno?: { version?: { deno?: string } };
8
+ };
9
+
10
+ /**
11
+ * Detects the JavaScript runtime environment (Node.js, Deno, Bun, or unknown)
12
+ * and its version.
13
+ *
14
+ * @returns An object containing the runtime type and version, or `undefined`
15
+ * if the runtime cannot be detected.
16
+ */
17
+ export function getRuntimeInfo(): RuntimeInfo | undefined {
18
+ // Deno
19
+ const deno = globalThis.Deno;
20
+ if (typeof deno === "object" && deno?.version?.deno !== undefined) {
21
+ return {
22
+ runtime: "deno",
23
+ version: deno.version.deno,
24
+ };
25
+ }
26
+
27
+ // Bun: this should be checked before Node.js since Bun also defines
28
+ // `process.versions.node`
29
+ if (typeof process !== "undefined" && process.versions?.bun !== undefined) {
30
+ return {
31
+ runtime: "bun",
32
+ version: process.versions.bun,
33
+ };
34
+ }
35
+
36
+ // Node
37
+ if (typeof process !== "undefined" && process.versions?.node !== undefined) {
38
+ return {
39
+ runtime: "node",
40
+ version: process.versions.node,
41
+ };
42
+ }
43
+
44
+ return undefined;
45
+ }
package/src/subprocess.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  } from "./errors/subprocess.js";
7
7
  import { exists, isDirectory } from "./fs.js";
8
8
 
9
+ export { SubprocessFileNotFoundError, SubprocessPathIsDirectoryError };
10
+
9
11
  /**
10
12
  * Spawns a detached subprocess to execute a given file with optional arguments.
11
13
  *