@jsenv/filesystem 4.11.0 → 4.13.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.11.0",
3
+ "version": "4.13.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -339,7 +339,8 @@ export const registerDirectoryLifecycle = (
339
339
  }
340
340
  };
341
341
 
342
- readdirSync(new URL(sourceUrl)).forEach((entry) => {
342
+ const entries = readdirSync(new URL(sourceUrl));
343
+ for (const entry of entries) {
343
344
  const entryUrl = new URL(entry, sourceUrl).href;
344
345
  const entryInfo = readEntryInfo(entryUrl);
345
346
  if (entryInfo.type !== null && entryInfo.patternValue) {
@@ -347,7 +348,7 @@ export const registerDirectoryLifecycle = (
347
348
  notify: notifyExistent,
348
349
  });
349
350
  }
350
- });
351
+ }
351
352
  if (debug) {
352
353
  const relativeUrls = Array.from(infoMap.keys());
353
354
  if (relativeUrls.length === 0) {
@@ -381,11 +382,11 @@ const shouldCallUpdated = (entryInfo) => {
381
382
  if (!stat.atimeMs) {
382
383
  return true;
383
384
  }
384
- if (stat.atimeMs < stat.mtimeMs) {
385
+ if (stat.atimeMs <= stat.mtimeMs) {
385
386
  return true;
386
387
  }
387
- if (isLinux && stat.mtimeMs <= previousInfo.stat.mtimeMs) {
388
- return false;
388
+ if (stat.mtimeMs !== previousInfo.stat.mtimeMs) {
389
+ return true;
389
390
  }
390
391
  return true;
391
392
  };
@@ -8,6 +8,10 @@ import { createWatcher } from "./create_watcher.js";
8
8
  import { guardTooFastSecondCall } from "./guard_second_call.js";
9
9
  import { trackResources } from "./track_resources.js";
10
10
 
11
+ const isMacos = process.platform === "darwin";
12
+ const isLinux = process.platform === "linux";
13
+ const isFreeBSD = process.platform === "freebsd";
14
+
11
15
  export const registerFileLifecycle = (
12
16
  source,
13
17
  {
@@ -171,16 +175,13 @@ const watchFileMutation = (
171
175
  { updated, removed, keepProcessAlive, stat },
172
176
  ) => {
173
177
  let prevStat = stat;
174
- let watcher = createWatcher(urlToFileSystemPath(sourceUrl), {
175
- persistent: keepProcessAlive,
176
- });
178
+ let watcher;
177
179
 
178
- watcher.on("change", () => {
180
+ const onChange = () => {
179
181
  const { type, stat } = readFileInfo(sourceUrl);
180
182
 
181
183
  if (type === null) {
182
- watcher.close();
183
- watcher = undefined;
184
+ stopWatching();
184
185
  if (removed) {
185
186
  removed();
186
187
  }
@@ -188,25 +189,38 @@ const watchFileMutation = (
188
189
  if (updated && shouldCallUpdated(stat, prevStat)) {
189
190
  updated();
190
191
  }
192
+ if ((isMacos || isLinux || isFreeBSD) && prevStat.ino !== stat.ino) {
193
+ stopWatching();
194
+ watch();
195
+ }
191
196
  }
192
197
  prevStat = stat;
193
- });
198
+ };
194
199
 
195
- return () => {
200
+ const watch = () => {
201
+ watcher = createWatcher(urlToFileSystemPath(sourceUrl), {
202
+ persistent: keepProcessAlive,
203
+ });
204
+ watcher.on("change", onChange);
205
+ };
206
+ const stopWatching = () => {
196
207
  if (watcher) {
197
208
  watcher.close();
209
+ watcher = undefined;
198
210
  }
199
211
  };
212
+ watch();
213
+ return stopWatching;
200
214
  };
201
215
 
202
216
  const shouldCallUpdated = (stat, prevStat) => {
203
217
  if (!stat.atimeMs) {
204
218
  return true;
205
219
  }
206
- if (stat.atimeMs < stat.mtimeMs) {
220
+ if (stat.atimeMs <= stat.mtimeMs) {
207
221
  return true;
208
222
  }
209
- if (stat.mtimeMs > prevStat.mtimeMs) {
223
+ if (stat.mtimeMs !== prevStat.mtimeMs) {
210
224
  return true;
211
225
  }
212
226
  return false;
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);