@williamthorsen/toolbelt.filesystem 0.8.3 → 0.9.1

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.
@@ -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
- }