@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.
- package/README.md +29 -38
- package/dist/commonjs/jsenv_filesystem.cjs +174 -26
- package/dist/commonjs/jsenv_filesystem.cjs.map +50 -36
- package/main.js +2 -0
- package/package.json +11 -10
- package/src/assertAndNormalizeDirectoryUrl.js +6 -2
- package/src/assertDirectoryPresence.js +3 -1
- package/src/assertFilePresence.js +5 -1
- package/src/collectDirectoryMatchReport.js +19 -4
- package/src/collectFiles.js +13 -3
- package/src/copyDirectoryContent.js +81 -0
- package/src/copyFileSystemNode.js +27 -8
- package/src/ensureEmptyDirectory.js +4 -1
- package/src/ensureParentDirectories.js +4 -1
- package/src/ensureWindowsDriveLetter.js +6 -2
- package/src/internal/getCommonPathname.js +4 -1
- package/src/internal/replaceBackSlashesWithSlashes.js +2 -1
- package/src/isFileSystemPath.js +3 -1
- package/src/listFilesMatching.js +34 -0
- package/src/moveDirectoryContent.js +15 -4
- package/src/moveFileSystemNode.js +11 -3
- package/src/readDirectory.js +8 -2
- package/src/readFileSystemNodeStat.js +8 -2
- package/src/registerDirectoryLifecycle.js +52 -27
- package/src/registerFileLifecycle.js +32 -10
- package/src/removeFileSystemNode.js +18 -7
- package/src/testFileSystemNodePermissions.js +6 -1
- package/src/urlToBasename.js +2 -1
- package/src/urlToFilename.js +3 -1
- package/src/urlToParentUrl.js +4 -1
- package/src/urlToPathname.js +3 -1
- package/src/urlToRelativeUrl.js +6 -2
- package/src/writeFileSystemNodeModificationTime.js +2 -1
- package/src/writeFileSystemNodePermissions.js +5 -1
- package/src/writeSymbolicLink.js +82 -26
package/README.md
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
# Jsenv filesystem
|
|
1
|
+
# Jsenv filesystem [](https://www.npmjs.com/package/@jsenv/filesystem) [](https://github.com/jsenv/filesystem/actions?workflow=main) [](https://codecov.io/gh/jsenv/filesystem)
|
|
2
2
|
|
|
3
|
-
Collection of functions to interact with filesystem in Node.js
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@jsenv/filesystem)
|
|
6
|
-
[](https://github.com/jsenv/filesystem/actions?workflow=main)
|
|
7
|
-
[](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)
|
|
12
|
+
const relativeUrl = urlToRelativeUrl(jsFileUrl, projectDirectoryUrl)
|
|
13
|
+
console.log(relativeUrl) // "src/file.js"
|
|
25
14
|
```
|
|
26
15
|
|
|
27
|
-
##
|
|
16
|
+
## List files using pattern matching
|
|
28
17
|
|
|
29
18
|
```js
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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(
|
|
44
|
+
packageJSON = JSON.parse(String(readFileSync(packageJSONFileUrl)))
|
|
58
45
|
},
|
|
59
46
|
updated: () => {
|
|
60
|
-
packageJSON = JSON.parse(String(readFileSync(
|
|
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
|
|
57
|
+
## Watch many files changes
|
|
71
58
|
|
|
72
59
|
```js
|
|
73
60
|
import { registerDirectoryLifecycle } from "@jsenv/filesystem"
|
|
74
61
|
|
|
75
|
-
const
|
|
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
|
-
|
|
69
|
+
directoryContentDescription[relativeUrl] = type
|
|
83
70
|
},
|
|
84
71
|
removed: ({ relativeUrl }) => {
|
|
85
|
-
delete
|
|
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 && ( //
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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;
|