@pnpm/fs.graceful-fs 1100.2.2 → 1100.2.4
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 +19 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +66 -13
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @pnpm/fs.graceful-fs
|
|
2
2
|
|
|
3
|
+
## 1100.2.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fixed concurrent installs failing when replacing the same stale hoisted dependency link.
|
|
8
|
+
|
|
9
|
+
- `pnpm install` now works in StackBlitz WebContainers on projects without a lockfile. It used to fail there with `ENOENT ... pnpm-lock.yaml`, because pnpm did not recognize the errors that WebContainers return from asynchronous file system calls [#15649](https://github.com/pnpm/pnpm/issues/15649).
|
|
10
|
+
|
|
11
|
+
- Updated dependencies:
|
|
12
|
+
- @pnpm/error@1100.2.1
|
|
13
|
+
|
|
14
|
+
## 1100.2.3
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- `pnpm install` in WSL now waits out Windows file locks on a Windows drive such as `/mnt/c`, as it already does on Windows. Before, an antivirus or indexer scan holding a file open could fail the install with `EACCES` [pnpm/pnpm#6155](https://github.com/pnpm/pnpm/issues/6155).
|
|
19
|
+
|
|
20
|
+
- On Windows, pnpm now retries saving `pnpm-lock.yaml` for up to a minute while another process holds the file open. The save used to fail at once with `EPERM`, `EBUSY`, or "Access is denied" [#9461](https://github.com/pnpm/pnpm/issues/9461).
|
|
21
|
+
|
|
3
22
|
## 1100.2.2
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/lib/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ declare const _default: {
|
|
|
22
22
|
export default _default;
|
|
23
23
|
/**
|
|
24
24
|
* Renames `src` over `dest`, retrying Windows EBUSY errors for up to a minute.
|
|
25
|
+
* Windows drives mounted into WSL get the same retries.
|
|
25
26
|
* EPERM and EACCES have a one-second budget because they can also indicate
|
|
26
27
|
* permanent permission or destination conflicts. Other errors are thrown
|
|
27
28
|
* right away.
|
|
@@ -31,6 +32,11 @@ export default _default;
|
|
|
31
32
|
* that was there or the whole file replacing it.
|
|
32
33
|
*/
|
|
33
34
|
export declare function renameFileWithRetry(src: string, dest: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Asynchronous {@link renameFileWithRetry}, which waits between attempts
|
|
37
|
+
* without blocking the event loop.
|
|
38
|
+
*/
|
|
39
|
+
export declare function renameFileWithRetryAsync(src: string, dest: string): Promise<void>;
|
|
34
40
|
/**
|
|
35
41
|
* Reads `target`'s stats without following it, with the retry policy of
|
|
36
42
|
* {@link renameFileWithRetry}.
|
|
@@ -45,3 +51,14 @@ export declare function lstatWithRetry(target: string): fs.Stats;
|
|
|
45
51
|
* Removes a file, with the retry policy of {@link renameFileWithRetry}.
|
|
46
52
|
*/
|
|
47
53
|
export declare function unlinkWithRetry(target: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Runs a filesystem operation with the retry policy of
|
|
56
|
+
* {@link renameFileWithRetry}.
|
|
57
|
+
*/
|
|
58
|
+
export declare function withFileLockRetry<T>(operation: () => T): T;
|
|
59
|
+
/**
|
|
60
|
+
* Asynchronous {@link withFileLockRetry}, which waits between attempts
|
|
61
|
+
* without blocking the event loop.
|
|
62
|
+
*/
|
|
63
|
+
export declare function withFileLockRetryAsync<T>(operation: () => Promise<T>): Promise<T>;
|
|
64
|
+
export declare function isTransientFileLockError(err: unknown): err is NodeJS.ErrnoException;
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { isError } from '@pnpm/error';
|
|
3
5
|
import gfs from 'graceful-fs';
|
|
4
6
|
const FILE_LOCK_RETRY_BUDGET_MS = 60_000;
|
|
5
7
|
const PERMISSION_DENIED_RETRY_BUDGET_MS = 1_000;
|
|
@@ -33,7 +35,7 @@ function withEagainRetry(fn, maxRetries = 15) {
|
|
|
33
35
|
return fn(...args);
|
|
34
36
|
}
|
|
35
37
|
catch (err) {
|
|
36
|
-
if (
|
|
38
|
+
if (isError(err) && 'code' in err && err.code === 'EAGAIN' && attempts < maxRetries) {
|
|
37
39
|
attempts++;
|
|
38
40
|
// Exponential backoff: wait 2^attempts milliseconds, max 300ms
|
|
39
41
|
const delay = Math.min(Math.pow(2, attempts), 300);
|
|
@@ -48,6 +50,7 @@ function withEagainRetry(fn, maxRetries = 15) {
|
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
50
52
|
* Renames `src` over `dest`, retrying Windows EBUSY errors for up to a minute.
|
|
53
|
+
* Windows drives mounted into WSL get the same retries.
|
|
51
54
|
* EPERM and EACCES have a one-second budget because they can also indicate
|
|
52
55
|
* permanent permission or destination conflicts. Other errors are thrown
|
|
53
56
|
* right away.
|
|
@@ -61,6 +64,13 @@ export function renameFileWithRetry(src, dest) {
|
|
|
61
64
|
fs.renameSync(src, dest);
|
|
62
65
|
});
|
|
63
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Asynchronous {@link renameFileWithRetry}, which waits between attempts
|
|
69
|
+
* without blocking the event loop.
|
|
70
|
+
*/
|
|
71
|
+
export async function renameFileWithRetryAsync(src, dest) {
|
|
72
|
+
await withFileLockRetryAsync(() => fs.promises.rename(src, dest));
|
|
73
|
+
}
|
|
64
74
|
/**
|
|
65
75
|
* Reads `target`'s stats without following it, with the retry policy of
|
|
66
76
|
* {@link renameFileWithRetry}.
|
|
@@ -81,15 +91,51 @@ export function unlinkWithRetry(target) {
|
|
|
81
91
|
fs.unlinkSync(target);
|
|
82
92
|
});
|
|
83
93
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Runs a filesystem operation with the retry policy of
|
|
96
|
+
* {@link renameFileWithRetry}.
|
|
97
|
+
*/
|
|
98
|
+
export function withFileLockRetry(operation) {
|
|
99
|
+
const retry = createFileLockRetry();
|
|
88
100
|
for (;;) {
|
|
89
101
|
try {
|
|
90
102
|
return operation();
|
|
91
103
|
}
|
|
92
104
|
catch (err) {
|
|
105
|
+
const delayMs = retry.delayBeforeNextAttempt(err);
|
|
106
|
+
if (delayMs > 0)
|
|
107
|
+
Atomics.wait(fileLockRetrySleepBuffer, 0, 0, delayMs);
|
|
108
|
+
retry.checkBudgetAfterDelay(err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Asynchronous {@link withFileLockRetry}, which waits between attempts
|
|
114
|
+
* without blocking the event loop.
|
|
115
|
+
*/
|
|
116
|
+
export async function withFileLockRetryAsync(operation) {
|
|
117
|
+
const retry = createFileLockRetry();
|
|
118
|
+
for (;;) {
|
|
119
|
+
try {
|
|
120
|
+
// eslint-disable-next-line no-await-in-loop
|
|
121
|
+
return await operation();
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const delayMs = retry.delayBeforeNextAttempt(err);
|
|
125
|
+
if (delayMs > 0) {
|
|
126
|
+
// eslint-disable-next-line no-await-in-loop
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
128
|
+
}
|
|
129
|
+
retry.checkBudgetAfterDelay(err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function createFileLockRetry() {
|
|
134
|
+
const startedAt = Date.now();
|
|
135
|
+
let backoffMs = 0;
|
|
136
|
+
let budgetMs = FILE_LOCK_RETRY_BUDGET_MS;
|
|
137
|
+
return {
|
|
138
|
+
delayBeforeNextAttempt(err) {
|
|
93
139
|
if (!isTransientFileLockError(err))
|
|
94
140
|
throw err;
|
|
95
141
|
if (err.code === 'EPERM' || err.code === 'EACCES')
|
|
@@ -97,18 +143,25 @@ function withFileLockRetry(operation) {
|
|
|
97
143
|
const remainingMs = budgetMs - (Date.now() - startedAt);
|
|
98
144
|
if (remainingMs <= 0)
|
|
99
145
|
throw err;
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
return Math.min(backoffMs, remainingMs);
|
|
147
|
+
},
|
|
148
|
+
checkBudgetAfterDelay(err) {
|
|
102
149
|
if (Date.now() - startedAt >= budgetMs)
|
|
103
150
|
throw err;
|
|
104
151
|
backoffMs = Math.min(backoffMs + 10, FILE_LOCK_RETRY_BACKOFF_CAP_MS);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
107
154
|
}
|
|
108
|
-
function isTransientFileLockError(err) {
|
|
109
|
-
return process.platform === 'win32' &&
|
|
110
|
-
|
|
155
|
+
export function isTransientFileLockError(err) {
|
|
156
|
+
return (process.platform === 'win32' || isWsl()) &&
|
|
157
|
+
isError(err) &&
|
|
111
158
|
'code' in err &&
|
|
112
159
|
(err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'EBUSY');
|
|
113
160
|
}
|
|
161
|
+
// A Windows drive mounted into WSL (/mnt/c) keeps Windows file locking, so an
|
|
162
|
+
// antivirus or indexer handle fails a rename there with EACCES. WSL kernels
|
|
163
|
+
// carry "microsoft" in their release, e.g. 5.15.167.4-microsoft-standard-WSL2.
|
|
164
|
+
function isWsl() {
|
|
165
|
+
return process.platform === 'linux' && os.release().toLowerCase().includes('microsoft');
|
|
166
|
+
}
|
|
114
167
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/fs.graceful-fs",
|
|
3
|
-
"version": "1100.2.
|
|
3
|
+
"version": "1100.2.4",
|
|
4
4
|
"description": "Promisified graceful-fs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
"!*.map"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@pnpm/error": "1100.2.1",
|
|
30
31
|
"graceful-fs": "^4.2.11"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@jest/globals": "30.4.1",
|
|
34
|
-
"@pnpm/fs.graceful-fs": "1100.2.
|
|
35
|
+
"@pnpm/fs.graceful-fs": "1100.2.4",
|
|
35
36
|
"@types/graceful-fs": "^4.1.9"
|
|
36
37
|
},
|
|
37
38
|
"engines": {
|