@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/filesystem",
3
- "version": "4.10.13",
3
+ "version": "4.12.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- const destinationStats = readEntryStatSync(destinationUrl, {
16
- nullIfNotFound: true,
17
- followLink: false,
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 === "ENOENT") {
19
- mkdirSync(new URL("./", destinationUrlObject), {
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);