@jsenv/filesystem 4.10.13 → 4.12.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/package.json +1 -1
- package/src/lifecycle/call_once_idle.js +32 -0
- package/src/lifecycle/register_directory_lifecycle.js +7 -0
- package/src/lifecycle/register_file_lifecycle.js +7 -0
- package/src/main.js +2 -0
- package/src/path_and_url/find_ancestor_directory_url.js +12 -0
- package/src/path_and_url/get_parent_directory_url.js +24 -0
- package/src/read_write/write_directory_sync.js +44 -7
- package/src/read_write/write_file_sync.js +16 -8
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const callOnceIdle = (callback, idleMs) => {
|
|
2
|
+
let timeoutId;
|
|
3
|
+
return (...args) => {
|
|
4
|
+
if (timeoutId) {
|
|
5
|
+
clearTimeout(timeoutId);
|
|
6
|
+
}
|
|
7
|
+
timeoutId = setTimeout(() => {
|
|
8
|
+
callback(...args);
|
|
9
|
+
}, idleMs);
|
|
10
|
+
if (timeoutId.unref) {
|
|
11
|
+
timeoutId.unref();
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const callOnceIdlePerFile = (callback, idleMs) => {
|
|
17
|
+
const timeoutIdMap = new Map();
|
|
18
|
+
return (fileEvent) => {
|
|
19
|
+
const { relativeUrl } = fileEvent;
|
|
20
|
+
let timeoutId = timeoutIdMap.get(relativeUrl);
|
|
21
|
+
if (timeoutId) {
|
|
22
|
+
clearTimeout(timeoutId);
|
|
23
|
+
}
|
|
24
|
+
timeoutId = setTimeout(() => {
|
|
25
|
+
callback(fileEvent);
|
|
26
|
+
}, idleMs);
|
|
27
|
+
if (timeoutId.unref) {
|
|
28
|
+
timeoutId.unref();
|
|
29
|
+
}
|
|
30
|
+
timeoutIdMap.set(relativeUrl, timeoutId);
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -4,6 +4,7 @@ import { readdirSync } from "node:fs";
|
|
|
4
4
|
import { assertAndNormalizeDirectoryUrl } from "../path_and_url/directory_url_validation.js";
|
|
5
5
|
import { readEntryStatSync } from "../read_write/stat/read_entry_stat_sync.js";
|
|
6
6
|
import { statsToType } from "../read_write/stat/stats_to_type.js";
|
|
7
|
+
import { callOnceIdlePerFile } from "./call_once_idle.js";
|
|
7
8
|
import { createWatcher } from "./create_watcher.js";
|
|
8
9
|
import { guardTooFastSecondCallPerFile } from "./guard_second_call.js";
|
|
9
10
|
import { trackResources } from "./track_resources.js";
|
|
@@ -30,6 +31,7 @@ export const registerDirectoryLifecycle = (
|
|
|
30
31
|
// For this reason"cooldownBetweenFileEvents" should be reserved to scenarios
|
|
31
32
|
// like unit tests
|
|
32
33
|
cooldownBetweenFileEvents = 0,
|
|
34
|
+
idleMs = 50,
|
|
33
35
|
},
|
|
34
36
|
) => {
|
|
35
37
|
const sourceUrl = assertAndNormalizeDirectoryUrl(source);
|
|
@@ -46,6 +48,11 @@ export const registerDirectoryLifecycle = (
|
|
|
46
48
|
`removed must be a function or undefined, got ${removed}`,
|
|
47
49
|
);
|
|
48
50
|
}
|
|
51
|
+
if (idleMs) {
|
|
52
|
+
if (updated) {
|
|
53
|
+
updated = callOnceIdlePerFile(updated, idleMs);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
49
56
|
if (cooldownBetweenFileEvents) {
|
|
50
57
|
if (added) {
|
|
51
58
|
added = guardTooFastSecondCallPerFile(added, cooldownBetweenFileEvents);
|
|
@@ -3,6 +3,7 @@ import { basename, dirname } from "node:path";
|
|
|
3
3
|
import { assertAndNormalizeFileUrl } from "../path_and_url/file_url_validation.js";
|
|
4
4
|
import { readEntryStatSync } from "../read_write/stat/read_entry_stat_sync.js";
|
|
5
5
|
import { statsToType } from "../read_write/stat/stats_to_type.js";
|
|
6
|
+
import { callOnceIdle } from "./call_once_idle.js";
|
|
6
7
|
import { createWatcher } from "./create_watcher.js";
|
|
7
8
|
import { guardTooFastSecondCall } from "./guard_second_call.js";
|
|
8
9
|
import { trackResources } from "./track_resources.js";
|
|
@@ -16,6 +17,7 @@ export const registerFileLifecycle = (
|
|
|
16
17
|
notifyExistent = false,
|
|
17
18
|
keepProcessAlive = true,
|
|
18
19
|
cooldownBetweenFileEvents = 0,
|
|
20
|
+
idleMs = 50,
|
|
19
21
|
},
|
|
20
22
|
) => {
|
|
21
23
|
const sourceUrl = assertAndNormalizeFileUrl(source);
|
|
@@ -32,6 +34,11 @@ export const registerFileLifecycle = (
|
|
|
32
34
|
`removed must be a function or undefined, got ${removed}`,
|
|
33
35
|
);
|
|
34
36
|
}
|
|
37
|
+
if (idleMs) {
|
|
38
|
+
if (updated) {
|
|
39
|
+
updated = callOnceIdle(updated, idleMs);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
35
42
|
if (cooldownBetweenFileEvents) {
|
|
36
43
|
if (added) {
|
|
37
44
|
added = guardTooFastSecondCall(added, cooldownBetweenFileEvents);
|
package/src/main.js
CHANGED
|
@@ -17,6 +17,8 @@ export {
|
|
|
17
17
|
} from "./path_and_url/file_url_validation.js";
|
|
18
18
|
export { comparePathnames } from "./path_and_url/compare_pathnames.js";
|
|
19
19
|
export { ensureWindowsDriveLetter } from "./path_and_url/ensure_windows_drive_letter.js";
|
|
20
|
+
export { getParentDirectoryUrl } from "./path_and_url/get_parent_directory_url.js";
|
|
21
|
+
export { findAncestorDirectoryUrl } from "./path_and_url/find_ancestor_directory_url.js";
|
|
20
22
|
|
|
21
23
|
// list
|
|
22
24
|
export { collectDirectoryMatchReport } from "./list/collect_directory_match_report.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getParentDirectoryUrl } from "./get_parent_directory_url.js";
|
|
2
|
+
|
|
3
|
+
export const findAncestorDirectoryUrl = (url, callback) => {
|
|
4
|
+
url = String(url);
|
|
5
|
+
while (url !== "file:///") {
|
|
6
|
+
if (callback(url)) {
|
|
7
|
+
return url;
|
|
8
|
+
}
|
|
9
|
+
url = getParentDirectoryUrl(url);
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const getParentDirectoryUrl = (url) => {
|
|
2
|
+
if (url.startsWith("file://")) {
|
|
3
|
+
// With node.js new URL('../', 'file:///C:/').href
|
|
4
|
+
// returns "file:///C:/" instead of "file:///"
|
|
5
|
+
const resource = url.slice("file://".length);
|
|
6
|
+
const slashLastIndex = resource.lastIndexOf("/");
|
|
7
|
+
if (slashLastIndex === -1) {
|
|
8
|
+
return url;
|
|
9
|
+
}
|
|
10
|
+
const lastCharIndex = resource.length - 1;
|
|
11
|
+
if (slashLastIndex === lastCharIndex) {
|
|
12
|
+
const slashBeforeLastIndex = resource.lastIndexOf(
|
|
13
|
+
"/",
|
|
14
|
+
slashLastIndex - 1,
|
|
15
|
+
);
|
|
16
|
+
if (slashBeforeLastIndex === -1) {
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
return `file://${resource.slice(0, slashBeforeLastIndex + 1)}`;
|
|
20
|
+
}
|
|
21
|
+
return `file://${resource.slice(0, slashLastIndex + 1)}`;
|
|
22
|
+
}
|
|
23
|
+
return new URL(url.endsWith("/") ? "../" : "./", url).href;
|
|
24
|
+
};
|
|
@@ -1,21 +1,59 @@
|
|
|
1
1
|
import { urlToFileSystemPath } from "@jsenv/urls";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, statSync, unlinkSync } from "node:fs";
|
|
3
3
|
|
|
4
4
|
import { assertAndNormalizeDirectoryUrl } from "../path_and_url/directory_url_validation.js";
|
|
5
|
+
import { findAncestorDirectoryUrl } from "../path_and_url/find_ancestor_directory_url.js";
|
|
5
6
|
import { readEntryStatSync } from "./stat/read_entry_stat_sync.js";
|
|
6
7
|
import { statsToType } from "./stat/stats_to_type.js";
|
|
7
8
|
|
|
8
9
|
export const writeDirectorySync = (
|
|
9
10
|
destination,
|
|
10
|
-
{ recursive = true, allowUseless = false } = {},
|
|
11
|
+
{ recursive = true, allowUseless = false, force } = {},
|
|
11
12
|
) => {
|
|
12
13
|
const destinationUrl = assertAndNormalizeDirectoryUrl(destination);
|
|
13
14
|
const destinationPath = urlToFileSystemPath(destinationUrl);
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
let destinationStats;
|
|
17
|
+
try {
|
|
18
|
+
destinationStats = readEntryStatSync(destinationUrl, {
|
|
19
|
+
nullIfNotFound: true,
|
|
20
|
+
followLink: false,
|
|
21
|
+
});
|
|
22
|
+
} catch (e) {
|
|
23
|
+
if (e.code === "ENOTDIR") {
|
|
24
|
+
let previousNonDirUrl = destinationUrl;
|
|
25
|
+
// we must try all parent directories as long as it fails with ENOTDIR
|
|
26
|
+
findAncestorDirectoryUrl(destinationUrl, (ancestorUrl) => {
|
|
27
|
+
try {
|
|
28
|
+
statSync(new URL(ancestorUrl));
|
|
29
|
+
return true;
|
|
30
|
+
} catch (e) {
|
|
31
|
+
if (e.code === "ENOTDIR") {
|
|
32
|
+
previousNonDirUrl = ancestorUrl;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
if (force) {
|
|
39
|
+
unlinkSync(
|
|
40
|
+
new URL(
|
|
41
|
+
previousNonDirUrl
|
|
42
|
+
// remove trailing slash
|
|
43
|
+
.slice(0, -1),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`cannot write directory at ${destinationPath} because there is a file at ${urlToFileSystemPath(
|
|
49
|
+
previousNonDirUrl,
|
|
50
|
+
)}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
19
57
|
|
|
20
58
|
if (destinationStats) {
|
|
21
59
|
if (destinationStats.isDirectory()) {
|
|
@@ -24,7 +62,6 @@ export const writeDirectorySync = (
|
|
|
24
62
|
}
|
|
25
63
|
throw new Error(`directory already exists at ${destinationPath}`);
|
|
26
64
|
}
|
|
27
|
-
|
|
28
65
|
const destinationType = statsToType(destinationStats);
|
|
29
66
|
throw new Error(
|
|
30
67
|
`cannot write directory at ${destinationPath} because there is a ${destinationType}`,
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
mkdirSync,
|
|
3
|
-
readFileSync,
|
|
4
|
-
writeFileSync as writeFileSyncNode,
|
|
5
|
-
} from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync as writeFileSyncNode } from "node:fs";
|
|
6
2
|
|
|
7
3
|
import { assertAndNormalizeFileUrl } from "../path_and_url/file_url_validation.js";
|
|
4
|
+
import { removeDirectorySync } from "../remove/remove_directory_sync.js";
|
|
5
|
+
import { writeDirectorySync } from "./write_directory_sync.js";
|
|
8
6
|
|
|
9
|
-
export const writeFileSync = (destination, content = "") => {
|
|
7
|
+
export const writeFileSync = (destination, content = "", { force } = {}) => {
|
|
10
8
|
const destinationUrl = assertAndNormalizeFileUrl(destination);
|
|
11
9
|
const destinationUrlObject = new URL(destinationUrl);
|
|
12
10
|
if (content && content instanceof URL) {
|
|
@@ -15,8 +13,18 @@ export const writeFileSync = (destination, content = "") => {
|
|
|
15
13
|
try {
|
|
16
14
|
writeFileSyncNode(destinationUrlObject, content);
|
|
17
15
|
} catch (error) {
|
|
18
|
-
if (error.code === "
|
|
19
|
-
|
|
16
|
+
if (error.code === "EISDIR") {
|
|
17
|
+
// happens when directory existed but got deleted and now it's a file
|
|
18
|
+
if (force) {
|
|
19
|
+
removeDirectorySync(destinationUrlObject);
|
|
20
|
+
writeFileSyncNode(destinationUrlObject, content);
|
|
21
|
+
} else {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") {
|
|
26
|
+
writeDirectorySync(new URL("./", destinationUrlObject), {
|
|
27
|
+
force,
|
|
20
28
|
recursive: true,
|
|
21
29
|
});
|
|
22
30
|
writeFileSyncNode(destinationUrlObject, content);
|