@williamthorsen/toolbelt.filesystem 0.8.3 → 0.9.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/.readyup/kits/default.js +514 -0
- package/.readyup/manifest.json +100 -0
- package/CHANGELOG.md +78 -14
- package/README.md +96 -147
- package/dist/esm/1-proposed/index.d.ts +0 -1
- package/dist/esm/1-proposed/index.js +0 -1
- package/dist/esm/3-candidate/index.d.ts +1 -1
- package/dist/esm/3-candidate/index.js +1 -1
- package/dist/esm/{1-proposed → 3-candidate}/writeAtomic.d.ts +3 -3
- package/dist/esm/4-release/listDirectoryChain.d.ts +1 -1
- package/dist/esm/4-release/loadConfigCascade.d.ts +1 -1
- package/dist/esm/4-release/reconcileFile.d.ts +1 -1
- package/dist/esm/4-release/reconcileFileFromFile.d.ts +1 -1
- package/package.json +10 -2
- package/dist/esm/3-candidate/createTempTree.d.ts +0 -85
- package/dist/esm/3-candidate/createTempTree.js +0 -148
- /package/dist/esm/{1-proposed → 3-candidate}/writeAtomic.js +0 -0
|
@@ -5,7 +5,7 @@ import { type FileReconciliation, type ReconcileFileOptions } from './reconcileF
|
|
|
5
5
|
* Everything past the read is `reconcileFile`: The comparison, the conflict policy, the created parent directories,
|
|
6
6
|
* and the outcome vocabulary are its. A source that cannot be read reports `failed`, and a missing source is not
|
|
7
7
|
* distinguished from an unreadable one. The reason names `sourcePath` and the cause; the path is interpolated
|
|
8
|
-
* because a read-stage failure
|
|
8
|
+
* because a read-stage failure has none of its own (`EISDIR: illegal operation on a directory, read`) and the
|
|
9
9
|
* result's `filePath` is the destination.
|
|
10
10
|
*
|
|
11
11
|
* The source is read even under `isDryRun`, because the outcome depends on comparing its content, so a dry run can
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/toolbelt.filesystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Filesystem utilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config-cascade",
|
|
@@ -40,11 +40,19 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
|
+
".readyup/kits/*.js",
|
|
44
|
+
".readyup/manifest.json",
|
|
43
45
|
"dist/*",
|
|
44
46
|
"CHANGELOG.md"
|
|
45
47
|
],
|
|
46
48
|
"dependencies": {
|
|
47
|
-
"@williamthorsen/toolbelt.errors": "0.
|
|
49
|
+
"@williamthorsen/toolbelt.errors": "0.8.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@williamthorsen/toolbelt.adoption": "0.1.0",
|
|
53
|
+
"@williamthorsen/toolbelt.testing": "0.6.0",
|
|
54
|
+
"esbuild": "0.28.2",
|
|
55
|
+
"readyup": "0.36.0"
|
|
48
56
|
},
|
|
49
57
|
"engines": {
|
|
50
58
|
"node": ">=24.0.0"
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a throwaway directory tree and returns a handle that removes it on disposal. Each key of `entries` is a
|
|
3
|
-
* path relative to the tree root: One ending in `/` becomes a directory, and any other becomes a file holding the
|
|
4
|
-
* mapped contents, given as text or as the bytes themselves. A key resolving outside the root is rejected, and a
|
|
5
|
-
* call that throws leaves nothing on disk.
|
|
6
|
-
*
|
|
7
|
-
* The handle writes into the tree after it is built, through `mkdir`, `symlink`, `write`, `writeAll`, and
|
|
8
|
-
* `writeJson`. Each creates the parent directories that it needs and takes its entry path through the containment
|
|
9
|
-
* check applied by `resolve`; `symlink`'s target is the exception, stored verbatim. Every one but `writeAll`,
|
|
10
|
-
* which takes a map, returns the absolute path that it wrote. It reads the tree back through `exists`, `list`,
|
|
11
|
-
* `listFiles`, `read`, and `readJson`, and removes an entry through `rm`.
|
|
12
|
-
*
|
|
13
|
-
* `prefix` names the directory, so a tree outliving a crashed run still shows what made it.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* using tree = createTempTree({ '.git/': '', 'src/main.ts': 'export {};\n' });
|
|
17
|
-
* tree.resolve('src/main.ts'); // '/private/var/folders/…/toolbelt-a1b2c3/src/main.ts'
|
|
18
|
-
*
|
|
19
|
-
* @category Filesystem
|
|
20
|
-
* @experimental
|
|
21
|
-
* @stage candidate
|
|
22
|
-
*/
|
|
23
|
-
export declare function createTempTree(entries: Record<string, string | Uint8Array>, options?: CreateTempTreeOptions): TempTree;
|
|
24
|
-
export interface CreateTempTreeOptions {
|
|
25
|
-
/** Leading text of the generated directory's name, to which a random suffix is appended. Defaults to `toolbelt-`. */
|
|
26
|
-
prefix?: string;
|
|
27
|
-
}
|
|
28
|
-
export interface TempTree extends Disposable {
|
|
29
|
-
/** Realpath of the tree root, resolved because `os.tmpdir()` is a symlink on macOS. */
|
|
30
|
-
readonly dir: string;
|
|
31
|
-
/** Reports whether a tree-relative path exists, following a symlink, so a dangling one yields `false`. */
|
|
32
|
-
exists(entryPath: string): boolean;
|
|
33
|
-
/** Lists the names directly inside a tree-relative directory, sorted, defaulting to the tree root. */
|
|
34
|
-
list(entryPath?: string): string[];
|
|
35
|
-
/**
|
|
36
|
-
* Lists every file below a tree-relative directory, at any depth, as `/`-separated paths relative to it, sorted,
|
|
37
|
-
* defaulting to the tree root. A symlink below that directory is neither listed nor descended, so every path in
|
|
38
|
-
* the result names a file held inside the tree. The directory given as the argument is the exception, followed as
|
|
39
|
-
* `list`, `read`, and `exists` follow theirs: One naming a link out of the tree lists the target's files. A path
|
|
40
|
-
* that does not exist yields an empty array, where `list` throws; one that exists as a file raises `ENOTDIR`, as
|
|
41
|
-
* `list` does.
|
|
42
|
-
*/
|
|
43
|
-
listFiles(entryPath?: string): string[];
|
|
44
|
-
/** Creates the directory at a tree-relative path, along with its parents, leaving an existing one as it is. */
|
|
45
|
-
mkdir(entryPath: string): string;
|
|
46
|
-
/** Reads the file at a tree-relative path as UTF-8 text. */
|
|
47
|
-
read(entryPath: string): string;
|
|
48
|
-
/**
|
|
49
|
-
* Reads the file at a tree-relative path as JSON. The result is `unknown`, for the caller to narrow; contents
|
|
50
|
-
* that do not parse raise an error naming the entry, which the parse error alone does not.
|
|
51
|
-
*/
|
|
52
|
-
readJson(entryPath: string): unknown;
|
|
53
|
-
/**
|
|
54
|
-
* Resolves `segments` against the tree root, throwing when the result falls outside it. An absolute segment
|
|
55
|
-
* landing inside the root is returned. The containment test is lexical, so it does not follow a symlink within
|
|
56
|
-
* the tree that points out of it.
|
|
57
|
-
*/
|
|
58
|
-
resolve(...segments: string[]): string;
|
|
59
|
-
/** Removes a tree-relative entry, along with its contents where it is a directory, and a missing one silently. */
|
|
60
|
-
rm(entryPath: string): void;
|
|
61
|
-
/**
|
|
62
|
-
* Links a tree-relative path to `targetPath`, taking the link first and so inverting `fs.symlinkSync`. The target
|
|
63
|
-
* is stored verbatim and is not containment-checked, being a string held by the link rather than a location to
|
|
64
|
-
* which the tree writes: It may be absolute or relative, name something outside the tree, or dangle. A relative one
|
|
65
|
-
* resolves against the link's own directory, as POSIX resolves it. An occupied link path raises `EEXIST`.
|
|
66
|
-
*
|
|
67
|
-
* The link type is the one portability difference. An absolute directory target is linked as a junction, which
|
|
68
|
-
* Windows creates without the elevation needed by a directory symlink; a relative directory target is linked as a
|
|
69
|
-
* directory, which needs that elevation; every other target, a missing one included, is linked as a file.
|
|
70
|
-
*/
|
|
71
|
-
symlink(linkPath: string, targetPath: string): string;
|
|
72
|
-
/** Writes `contents` at a tree-relative path, replacing an existing file. */
|
|
73
|
-
write(entryPath: string, contents: string | Uint8Array): string;
|
|
74
|
-
/**
|
|
75
|
-
* Writes a map of entries in the shape taken by the constructor, so a fixture built in one call there can be
|
|
76
|
-
* added to in one call here. Unlike the constructor, a failure part-way leaves the entries already written in
|
|
77
|
-
* place, there being no whole tree to discard.
|
|
78
|
-
*/
|
|
79
|
-
writeAll(entries: Record<string, string | Uint8Array>): void;
|
|
80
|
-
/**
|
|
81
|
-
* Writes `value` at a tree-relative path as two-space-indented JSON ending in a newline. A value with no JSON
|
|
82
|
-
* representation -- `undefined`, a function, a symbol -- is refused rather than written.
|
|
83
|
-
*/
|
|
84
|
-
writeJson(entryPath: string, value: unknown): string;
|
|
85
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
export function createTempTree(entries, options = {}) {
|
|
5
|
-
const { prefix = 'toolbelt-' } = options;
|
|
6
|
-
assertNamesDirectChild(prefix);
|
|
7
|
-
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), prefix)));
|
|
8
|
-
try {
|
|
9
|
-
writeAll(entries);
|
|
10
|
-
}
|
|
11
|
-
catch (error) {
|
|
12
|
-
fs.rmSync(dir, { force: true, recursive: true });
|
|
13
|
-
throw error;
|
|
14
|
-
}
|
|
15
|
-
function exists(entryPath) {
|
|
16
|
-
return fs.existsSync(resolveWithinTree(dir, [entryPath]));
|
|
17
|
-
}
|
|
18
|
-
function list(entryPath = '') {
|
|
19
|
-
return fs.readdirSync(resolveWithinTree(dir, [entryPath])).toSorted();
|
|
20
|
-
}
|
|
21
|
-
function listFiles(entryPath = '') {
|
|
22
|
-
const rootPath = resolveWithinTree(dir, [entryPath]);
|
|
23
|
-
if (!fs.existsSync(rootPath))
|
|
24
|
-
return [];
|
|
25
|
-
return listFilesBelow(rootPath, '').toSorted();
|
|
26
|
-
}
|
|
27
|
-
function mkdir(entryPath) {
|
|
28
|
-
const absolutePath = resolveWithinTree(dir, [entryPath]);
|
|
29
|
-
fs.mkdirSync(absolutePath, { recursive: true });
|
|
30
|
-
return absolutePath;
|
|
31
|
-
}
|
|
32
|
-
function read(entryPath) {
|
|
33
|
-
return fs.readFileSync(resolveWithinTree(dir, [entryPath]), 'utf8');
|
|
34
|
-
}
|
|
35
|
-
function readJson(entryPath) {
|
|
36
|
-
const contents = read(entryPath);
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(contents);
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
throw new Error(`Entry "${entryPath}" is not readable as JSON`, { cause: error });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
function resolve(...segments) {
|
|
45
|
-
return resolveWithinTree(dir, segments);
|
|
46
|
-
}
|
|
47
|
-
function rm(entryPath) {
|
|
48
|
-
fs.rmSync(resolveWithinTree(dir, [entryPath]), { force: true, recursive: true });
|
|
49
|
-
}
|
|
50
|
-
function symlink(linkPath, targetPath) {
|
|
51
|
-
const absoluteLink = resolveWithinTree(dir, [linkPath]);
|
|
52
|
-
fs.mkdirSync(path.dirname(absoluteLink), { recursive: true });
|
|
53
|
-
fs.symlinkSync(targetPath, absoluteLink, chooseLinkType(absoluteLink, targetPath));
|
|
54
|
-
return absoluteLink;
|
|
55
|
-
}
|
|
56
|
-
function write(entryPath, contents) {
|
|
57
|
-
const absolutePath = resolveWithinTree(dir, [entryPath]);
|
|
58
|
-
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
59
|
-
fs.writeFileSync(absolutePath, contents);
|
|
60
|
-
return absolutePath;
|
|
61
|
-
}
|
|
62
|
-
function writeAll(newEntries) {
|
|
63
|
-
for (const [entry, contents] of Object.entries(newEntries)) {
|
|
64
|
-
if (entry.endsWith('/')) {
|
|
65
|
-
mkdir(entry);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
write(entry, contents);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function writeJson(entryPath, value) {
|
|
73
|
-
const json = JSON.stringify(value, null, 2);
|
|
74
|
-
if (json === undefined) {
|
|
75
|
-
throw new Error(`Value of type "${typeof value}" for "${entryPath}" has no JSON representation`);
|
|
76
|
-
}
|
|
77
|
-
return write(entryPath, `${json}\n`);
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
dir,
|
|
81
|
-
exists,
|
|
82
|
-
list,
|
|
83
|
-
listFiles,
|
|
84
|
-
mkdir,
|
|
85
|
-
read,
|
|
86
|
-
readJson,
|
|
87
|
-
resolve,
|
|
88
|
-
rm,
|
|
89
|
-
symlink,
|
|
90
|
-
write,
|
|
91
|
-
writeAll,
|
|
92
|
-
writeJson,
|
|
93
|
-
[Symbol.dispose]() {
|
|
94
|
-
try {
|
|
95
|
-
fs.rmSync(dir, { force: true, recursive: true });
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
restoreDirectoryPermissions(dir);
|
|
99
|
-
fs.rmSync(dir, { force: true, recursive: true });
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function assertNamesDirectChild(prefix) {
|
|
105
|
-
if (prefix.includes('/') || prefix.includes('\\')) {
|
|
106
|
-
throw new Error(`Temporary-directory prefix "${prefix}" contains a path separator`);
|
|
107
|
-
}
|
|
108
|
-
if (['', '.', '..'].includes(prefix)) {
|
|
109
|
-
throw new Error(`Temporary-directory prefix "${prefix}" names no new directory`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function chooseLinkType(absoluteLinkPath, targetPath) {
|
|
113
|
-
const resolvedTarget = path.resolve(path.dirname(absoluteLinkPath), targetPath);
|
|
114
|
-
if (fs.statSync(resolvedTarget, { throwIfNoEntry: false })?.isDirectory() !== true) {
|
|
115
|
-
return 'file';
|
|
116
|
-
}
|
|
117
|
-
return path.isAbsolute(targetPath) ? 'junction' : 'dir';
|
|
118
|
-
}
|
|
119
|
-
function listFilesBelow(dir, prefix) {
|
|
120
|
-
const paths = [];
|
|
121
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
const relativePath = `${prefix}${entry.name}`;
|
|
124
|
-
if (entry.isFile()) {
|
|
125
|
-
paths.push(relativePath);
|
|
126
|
-
}
|
|
127
|
-
else if (entry.isDirectory()) {
|
|
128
|
-
paths.push(...listFilesBelow(path.join(dir, entry.name), `${relativePath}/`));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return paths;
|
|
132
|
-
}
|
|
133
|
-
function restoreDirectoryPermissions(dir) {
|
|
134
|
-
fs.chmodSync(dir, 0o700);
|
|
135
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
136
|
-
for (const entry of entries) {
|
|
137
|
-
if (entry.isDirectory()) {
|
|
138
|
-
restoreDirectoryPermissions(path.join(dir, entry.name));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
function resolveWithinTree(dir, segments) {
|
|
143
|
-
const target = path.resolve(dir, ...segments);
|
|
144
|
-
if (target !== dir && !target.startsWith(dir + path.sep)) {
|
|
145
|
-
throw new Error(`Path "${target}" falls outside the temporary tree at "${dir}"`);
|
|
146
|
-
}
|
|
147
|
-
return target;
|
|
148
|
-
}
|
|
File without changes
|