@jsenv/filesystem 2.0.0 → 2.3.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 (35) hide show
  1. package/README.md +29 -38
  2. package/dist/commonjs/jsenv_filesystem.cjs +174 -26
  3. package/dist/commonjs/jsenv_filesystem.cjs.map +50 -36
  4. package/main.js +2 -0
  5. package/package.json +11 -10
  6. package/src/assertAndNormalizeDirectoryUrl.js +6 -2
  7. package/src/assertDirectoryPresence.js +3 -1
  8. package/src/assertFilePresence.js +5 -1
  9. package/src/collectDirectoryMatchReport.js +19 -4
  10. package/src/collectFiles.js +13 -3
  11. package/src/copyDirectoryContent.js +81 -0
  12. package/src/copyFileSystemNode.js +27 -8
  13. package/src/ensureEmptyDirectory.js +4 -1
  14. package/src/ensureParentDirectories.js +4 -1
  15. package/src/ensureWindowsDriveLetter.js +6 -2
  16. package/src/internal/getCommonPathname.js +4 -1
  17. package/src/internal/replaceBackSlashesWithSlashes.js +2 -1
  18. package/src/isFileSystemPath.js +3 -1
  19. package/src/listFilesMatching.js +34 -0
  20. package/src/moveDirectoryContent.js +15 -4
  21. package/src/moveFileSystemNode.js +11 -3
  22. package/src/readDirectory.js +8 -2
  23. package/src/readFileSystemNodeStat.js +8 -2
  24. package/src/registerDirectoryLifecycle.js +52 -27
  25. package/src/registerFileLifecycle.js +32 -10
  26. package/src/removeFileSystemNode.js +18 -7
  27. package/src/testFileSystemNodePermissions.js +6 -1
  28. package/src/urlToBasename.js +2 -1
  29. package/src/urlToFilename.js +3 -1
  30. package/src/urlToParentUrl.js +4 -1
  31. package/src/urlToPathname.js +3 -1
  32. package/src/urlToRelativeUrl.js +6 -2
  33. package/src/writeFileSystemNodeModificationTime.js +2 -1
  34. package/src/writeFileSystemNodePermissions.js +5 -1
  35. package/src/writeSymbolicLink.js +82 -26
package/README.md CHANGED
@@ -1,18 +1,6 @@
1
- # Jsenv filesystem
1
+ # Jsenv filesystem [![npm package](https://img.shields.io/npm/v/@jsenv/filesystem.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/filesystem) [![github main](https://github.com/jsenv/filesystem/workflows/main/badge.svg)](https://github.com/jsenv/filesystem/actions?workflow=main) [![codecov coverage](https://codecov.io/gh/jsenv/filesystem/branch/main/graph/badge.svg)](https://codecov.io/gh/jsenv/filesystem)
2
2
 
3
- Collection of functions to interact with filesystem in Node.js.
4
-
5
- [![npm package](https://img.shields.io/npm/v/@jsenv/filesystem.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/filesystem)
6
- [![github main](https://github.com/jsenv/filesystem/workflows/main/badge.svg)](https://github.com/jsenv/filesystem/actions?workflow=main)
7
- [![codecov coverage](https://codecov.io/gh/jsenv/filesystem/branch/main/graph/badge.svg)](https://codecov.io/gh/jsenv/filesystem)
8
-
9
- # Presentation
10
-
11
- `@jsenv/filesystem` provides functions needed to work with files. It has no external dependency.
12
-
13
- # Examples
14
-
15
- The whole list of functions exported by `@jsenv/filesystem` are documented in [./docs/API.md](./docs/API.md). This section demonstrates how `@jsenv/filesystem` can be used in common use cases.
3
+ Collection of functions to interact with filesystem in Node.js
16
4
 
17
5
  ## Get a relative url
18
6
 
@@ -21,30 +9,29 @@ import { urlToRelativeUrl } from "@jsenv/filesystem"
21
9
 
22
10
  const projectDirectoryUrl = "file:///project/"
23
11
  const jsFileUrl = "file:///project/src/file.js"
24
- const relativeUrl = urlToRelativeUrl(jsFileUrl, projectDirectoryUrl) // "src/file.js"
12
+ const relativeUrl = urlToRelativeUrl(jsFileUrl, projectDirectoryUrl)
13
+ console.log(relativeUrl) // "src/file.js"
25
14
  ```
26
15
 
27
- ## Collect files using pattern matching
16
+ ## List files using pattern matching
28
17
 
29
18
  ```js
30
- import { collectFiles } from "@jsenv/filesystem"
31
-
32
- const jsAndSourceMapFiles = await collectFiles({
33
- directoryUrl: "file:///project/directory/",
34
- structuredMetaMap: {
35
- isJsFile: {
36
- "./**/*.js": true,
37
- },
38
- isSourceMapFile: {
39
- "./**/*.js.map": true,
40
- },
41
- },
42
- predicate: ({ isJsFile, isSourceMapFile }) => isJsFile || isSourceMapFile,
43
- })
44
- console.log(jsAndSourceMapFiles.length)
19
+ import { listFilesMatching } from "@jsenv/filesystem"
20
+
21
+ const jsFiles = await listFilesMatching(
22
+ "./**/*.js",
23
+ new URL("./", import.meta.url),
24
+ )
25
+ ```
26
+
27
+ ```console
28
+ [
29
+ 'file:///Users/dmail/docs/demo/a.js',
30
+ 'file:///Users/dmail/docs/demo/b.js'
31
+ ]
45
32
  ```
46
33
 
47
- ## Watch a specific file change
34
+ ## Watch a specific file changes
48
35
 
49
36
  ```js
50
37
  import { readFileSync } from "node:fs"
@@ -54,10 +41,10 @@ const packageJSONFileUrl = new URL("./package.json", import.meta.url)
54
41
  let packageJSON = null
55
42
  const unregister = registerFileLifecycle(packageJSONFileUrl, {
56
43
  added: () => {
57
- packageJSON = JSON.parse(String(readFileSync(filePath)))
44
+ packageJSON = JSON.parse(String(readFileSync(packageJSONFileUrl)))
58
45
  },
59
46
  updated: () => {
60
- packageJSON = JSON.parse(String(readFileSync(filePath)))
47
+ packageJSON = JSON.parse(String(readFileSync(packageJSONFileUrl)))
61
48
  },
62
49
  removed: () => {
63
50
  packageJSON = null
@@ -67,27 +54,31 @@ const unregister = registerFileLifecycle(packageJSONFileUrl, {
67
54
  unregister() // stop watching the file changes
68
55
  ```
69
56
 
70
- ## Watch many file change
57
+ ## Watch many files changes
71
58
 
72
59
  ```js
73
60
  import { registerDirectoryLifecycle } from "@jsenv/filesystem"
74
61
 
75
- const contentMap = {}
62
+ const directoryContentDescription = {}
76
63
  const unregister = registerDirectoryLifecycle("file:///directory/", {
77
64
  watchDescription: {
78
65
  "./**/*": true,
79
66
  "./node_modules/": false,
80
67
  },
81
68
  added: ({ relativeUrl, type }) => {
82
- contentMap[relativeUrl] = type
69
+ directoryContentDescription[relativeUrl] = type
83
70
  },
84
71
  removed: ({ relativeUrl }) => {
85
- delete contentMap[relativeUrl]
72
+ delete directoryContentDescription[relativeUrl]
86
73
  },
87
74
  })
88
75
  unregister() // stop watching the directory changes
89
76
  ```
90
77
 
78
+ # API
79
+
80
+ [docs/API.md](./docs/API.md)
81
+
91
82
  # Installation
92
83
 
93
84
  ```console
@@ -785,6 +785,7 @@ const collectFiles = async ({
785
785
  })
786
786
  });
787
787
  matchingFileResultArray.push({
788
+ url: new URL(relativeUrl, directoryUrl).href,
788
789
  relativeUrl,
789
790
  meta,
790
791
  fileStats: directoryChildNodeStats,
@@ -1040,8 +1041,7 @@ const removeDirectoryNaive = (directoryPath, {
1040
1041
  resolve(handlePermissionError(error));
1041
1042
  } else if (error.code === "ENOENT") {
1042
1043
  resolve();
1043
- } else if (handleNotEmptyError && ( // linux os
1044
- error.code === "ENOTEMPTY" || // SunOS
1044
+ } else if (handleNotEmptyError && (error.code === "ENOTEMPTY" || // SunOS
1045
1045
  error.code === "EEXIST")) {
1046
1046
  resolve(handleNotEmptyError(error));
1047
1047
  } else {
@@ -1201,37 +1201,29 @@ const {
1201
1201
  symlink
1202
1202
  } = node_fs.promises;
1203
1203
  const isWindows$1 = process.platform === "win32";
1204
+ /**
1205
+ * Writes a symbolic link pointing from a filesystem node to an other
1206
+ * @param {string} from Where symlink is written
1207
+ * @param {string} to The symlink target
1208
+ * @param {"file" | "dir"} [type] Symlink type if you know it before hand
1209
+ * @param {boolean} [allowUseless=false] Prevent error when symlink already exists with the same target
1210
+ * @param {boolean} [allowOverwrite=false] Will replace any existing symlink
1211
+ */
1212
+
1204
1213
  const writeSymbolicLink = async ({
1205
1214
  from,
1206
1215
  to,
1207
- type
1216
+ type,
1217
+ allowUseless = false,
1218
+ allowOverwrite = false
1208
1219
  }) => {
1209
1220
  const fromUrl = assertAndNormalizeFileUrl(from);
1210
- let toValue;
1211
-
1212
- if (typeof to === "string") {
1213
- // absolute filesystem path
1214
- if (isFileSystemPath(to)) {
1215
- toValue = to;
1216
- } // relative url
1217
- else if (to.startsWith("./") || to.startsWith("../")) {
1218
- toValue = to;
1219
- } // absolute url
1220
- else {
1221
- const toUrl = String(new URL(to, fromUrl));
1222
- toValue = urlToFileSystemPath(toUrl);
1223
- }
1224
- } else if (to instanceof URL) {
1225
- toValue = urlToFileSystemPath(to);
1226
- } else {
1227
- throw new TypeError(`symbolic link to must be a string or an url, received ${to}`);
1228
- }
1221
+ const toInfo = getToInfo(to, fromUrl);
1229
1222
 
1230
1223
  if (isWindows$1 && typeof type === "undefined") {
1231
1224
  // without this if you write a symbolic link without specifying the type on windows
1232
1225
  // you later get EPERM when doing stat on the symlink
1233
- const toUrl = resolveUrl(toValue, fromUrl);
1234
- const toStats = await readFileSystemNodeStat(toUrl, {
1226
+ const toStats = await readFileSystemNodeStat(toInfo.url, {
1235
1227
  nullIfNotFound: true
1236
1228
  });
1237
1229
  type = toStats && toStats.isDirectory() ? "dir" : "file";
@@ -1240,18 +1232,77 @@ const writeSymbolicLink = async ({
1240
1232
  const symbolicLinkPath = urlToFileSystemPath(fromUrl);
1241
1233
 
1242
1234
  try {
1243
- await symlink(toValue, symbolicLinkPath, type);
1235
+ await symlink(toInfo.value, symbolicLinkPath, type);
1244
1236
  } catch (error) {
1245
1237
  if (error.code === "ENOENT") {
1246
1238
  await ensureParentDirectories(fromUrl);
1247
- await symlink(toValue, symbolicLinkPath, type);
1239
+ await symlink(toInfo.value, symbolicLinkPath, type);
1248
1240
  return;
1249
1241
  }
1250
1242
 
1243
+ if (error.code === "EEXIST") {
1244
+ if (allowUseless) {
1245
+ const existingSymbolicLinkUrl = await readSymbolicLink(fromUrl);
1246
+
1247
+ if (existingSymbolicLinkUrl === toInfo.url) {
1248
+ return;
1249
+ }
1250
+ }
1251
+
1252
+ if (allowOverwrite) {
1253
+ await removeFileSystemNode(fromUrl);
1254
+ await symlink(toInfo.value, symbolicLinkPath, type);
1255
+ return;
1256
+ }
1257
+ }
1258
+
1251
1259
  throw error;
1252
1260
  }
1253
1261
  };
1254
1262
 
1263
+ const getToInfo = (to, fromUrl) => {
1264
+ if (typeof to === "string") {
1265
+ // absolute filesystem path
1266
+ if (isFileSystemPath(to)) {
1267
+ const url = fileSystemPathToUrl(to);
1268
+ const value = to;
1269
+ return {
1270
+ url,
1271
+ value
1272
+ };
1273
+ } // relative url
1274
+
1275
+
1276
+ if (to.startsWith("./") || to.startsWith("../")) {
1277
+ const url = resolveUrl(to, fromUrl);
1278
+ const value = to;
1279
+ return {
1280
+ url,
1281
+ value
1282
+ };
1283
+ } // absolute url
1284
+
1285
+
1286
+ const url = resolveUrl(to, fromUrl);
1287
+ const value = urlToFileSystemPath(url);
1288
+ return {
1289
+ url,
1290
+ value
1291
+ };
1292
+ }
1293
+
1294
+ if (to instanceof URL) {
1295
+ const url = String(to);
1296
+ const value = urlToFileSystemPath(url);
1297
+ return {
1298
+ url,
1299
+ value
1300
+ };
1301
+ }
1302
+
1303
+ throw new TypeError(`symbolic link to must be a string or an url, received ${to}`);
1304
+ };
1305
+
1255
1306
  const urlIsInsideOf = (url, otherUrl) => {
1256
1307
  const urlObject = new URL(url);
1257
1308
  const otherUrlObject = new URL(otherUrl);
@@ -1447,6 +1498,72 @@ const copyFileContentNaive = (filePath, fileDestinationPath) => {
1447
1498
  });
1448
1499
  };
1449
1500
 
1501
+ const copyDirectoryContent = async ({
1502
+ from,
1503
+ to,
1504
+ overwrite,
1505
+ followLink = true
1506
+ } = {}) => {
1507
+ const fromUrl = assertAndNormalizeDirectoryUrl(from);
1508
+ const fromPath = urlToFileSystemPath(fromUrl);
1509
+ let toUrl = assertAndNormalizeDirectoryUrl(to);
1510
+ let toPath = urlToFileSystemPath(toUrl);
1511
+ const sourceStats = await readFileSystemNodeStat(fromUrl, {
1512
+ nullIfNotFound: true,
1513
+ followLink: false
1514
+ });
1515
+
1516
+ if (!sourceStats) {
1517
+ throw new Error(`no directory to copy content from at ${fromPath}`);
1518
+ }
1519
+
1520
+ if (!sourceStats.isDirectory()) {
1521
+ const sourceType = statsToType(sourceStats);
1522
+ throw new Error(`found a ${sourceType} instead of a directory at ${fromPath}`);
1523
+ }
1524
+
1525
+ let destinationStats = await readFileSystemNodeStat(toUrl, {
1526
+ nullIfNotFound: true,
1527
+ // we force false here but in fact we will follow the destination link
1528
+ // to know where we will actually move and detect useless move overrite etc..
1529
+ followLink: false
1530
+ });
1531
+
1532
+ if (followLink && destinationStats && destinationStats.isSymbolicLink()) {
1533
+ const linkTarget = await readSymbolicLink(toUrl);
1534
+ toUrl = resolveUrl(linkTarget, toUrl);
1535
+ toPath = urlToFileSystemPath(toUrl);
1536
+ destinationStats = await readFileSystemNodeStat(toUrl, {
1537
+ nullIfNotFound: true
1538
+ });
1539
+ }
1540
+
1541
+ if (destinationStats === null) {
1542
+ throw new Error(`no directory to copy content into at ${toPath}`);
1543
+ }
1544
+
1545
+ if (!destinationStats.isDirectory()) {
1546
+ const destinationType = statsToType(destinationStats);
1547
+ throw new Error(`destination leads to a ${destinationType} instead of a directory at ${toPath}`);
1548
+ }
1549
+
1550
+ if (urlTargetsSameFileSystemPath(fromUrl, toUrl)) {
1551
+ throw new Error(`cannot copy directory content, source and destination are the same (${fromPath})`);
1552
+ }
1553
+
1554
+ const directoryEntries = await readDirectory(fromUrl);
1555
+ await Promise.all(directoryEntries.map(async directoryEntry => {
1556
+ const from = resolveUrl(directoryEntry, fromUrl);
1557
+ const to = resolveUrl(directoryEntry, toUrl);
1558
+ await copyFileSystemNode({
1559
+ from,
1560
+ to,
1561
+ overwrite,
1562
+ followLink
1563
+ });
1564
+ }));
1565
+ };
1566
+
1450
1567
  /*
1451
1568
  * - file modes documentation on Node.js
1452
1569
  * https://nodejs.org/docs/latest-v13.x/api/fs.html#fs_file_modes
@@ -1492,6 +1609,35 @@ const grantPermissionsOnFileSystemNode = async (source, {
1492
1609
  };
1493
1610
  };
1494
1611
 
1612
+ const listFilesMatching = async (patterns, directoryUrl = process.cwd()) => {
1613
+ if (typeof patterns === "string") {
1614
+ patterns = [patterns];
1615
+ } else if (Array.isArray(patterns)) {
1616
+ throw new TypeError(`patterns must be a string or an array of strings, got ${patterns}`);
1617
+ }
1618
+
1619
+ const patternMap = {};
1620
+ patterns.forEach((pattern, index) => {
1621
+ if (typeof pattern !== "string") {
1622
+ throw new TypeError(`every pattern must be a string, found ${pattern} at index ${index}`);
1623
+ }
1624
+
1625
+ patternMap[pattern] = true;
1626
+ });
1627
+ const fileDatas = await collectFiles({
1628
+ directoryUrl,
1629
+ structuredMetaMap: {
1630
+ matches: patternMap
1631
+ },
1632
+ predicate: ({
1633
+ matches
1634
+ }) => matches
1635
+ });
1636
+ return fileDatas.map(({
1637
+ url
1638
+ }) => url);
1639
+ };
1640
+
1495
1641
  const memoize = compute => {
1496
1642
  let memoized = false;
1497
1643
  let memoizedValue;
@@ -2464,6 +2610,7 @@ exports.bufferToEtag = bufferToEtag;
2464
2610
  exports.collectDirectoryMatchReport = collectDirectoryMatchReport;
2465
2611
  exports.collectFiles = collectFiles;
2466
2612
  exports.comparePathnames = comparePathnames;
2613
+ exports.copyDirectoryContent = copyDirectoryContent;
2467
2614
  exports.copyFileSystemNode = copyFileSystemNode;
2468
2615
  exports.ensureEmptyDirectory = ensureEmptyDirectory;
2469
2616
  exports.ensureParentDirectories = ensureParentDirectories;
@@ -2471,6 +2618,7 @@ exports.ensureWindowsDriveLetter = ensureWindowsDriveLetter;
2471
2618
  exports.fileSystemPathToUrl = fileSystemPathToUrl;
2472
2619
  exports.grantPermissionsOnFileSystemNode = grantPermissionsOnFileSystemNode;
2473
2620
  exports.isFileSystemPath = isFileSystemPath;
2621
+ exports.listFilesMatching = listFilesMatching;
2474
2622
  exports.memoize = memoize;
2475
2623
  exports.moveDirectoryContent = moveDirectoryContent;
2476
2624
  exports.moveFileSystemNode = moveFileSystemNode;