@williamthorsen/toolbelt.filesystem 0.5.0 → 0.6.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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.6.0 — 2026-08-16
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Add mkdir, symlink, write, and writeJson methods to `TempTree` (#176)
|
|
10
|
+
|
|
11
|
+
Adds four write methods to `TempTree` in `@williamthorsen/toolbelt.filesystem`: `mkdir`, `symlink`, `write`, and `writeJson`. Each takes a tree-relative path, creates the parent directories it needs, resolves through the same containment check `resolve` applies, and returns the absolute path, so a suite writing into a built temporary tree reaches it through the handle rather than through `node:fs`. `symlink` accepts a link path and a target, and picks the link type from the target.
|
|
12
|
+
|
|
13
|
+
### Tests
|
|
14
|
+
|
|
15
|
+
- Drop expect-type in favor of expectTypeOf (#175)
|
|
16
|
+
|
|
17
|
+
Replaces all imports of `expectTypeOf` from `expect-type` with the same import from `vitest`. Previously there had been imports from both libraries. `expect-type` is removed as a dependency.
|
|
18
|
+
|
|
5
19
|
## 0.5.0 — 2026-08-15
|
|
6
20
|
|
|
7
21
|
### Features
|
package/README.md
CHANGED
|
@@ -3,19 +3,13 @@
|
|
|
3
3
|
Filesystem utilities for TypeScript and JavaScript.
|
|
4
4
|
|
|
5
5
|
<!-- section:release-notes -->
|
|
6
|
-
## Release notes — v0.
|
|
6
|
+
## Release notes — v0.6.0 (2026-08-16)
|
|
7
7
|
|
|
8
8
|
### Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Add mkdir, symlink, write, and writeJson methods to `TempTree` (#176)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Migration: `createTempTree` and `TempTree` are imported from `@williamthorsen/toolbelt.filesystem/candidate` rather than `/proposed`.
|
|
15
|
-
|
|
16
|
-
- Add `writeAtomic`, an atomic file-write utility (#154)
|
|
17
|
-
|
|
18
|
-
Adds `writeAtomic` to `@williamthorsen/toolbelt.filesystem`, exported from the `/proposed` subpath. It stages content in a temp file beside the target and renames over it, so a concurrent reader sees either the previous file or the complete new one.
|
|
12
|
+
Adds four write methods to `TempTree` in `@williamthorsen/toolbelt.filesystem`: `mkdir`, `symlink`, `write`, and `writeJson`. Each takes a tree-relative path, creates the parent directories it needs, resolves through the same containment check `resolve` applies, and returns the absolute path, so a suite writing into a built temporary tree reaches it through the handle rather than through `node:fs`. `symlink` accepts a link path and a target, and picks the link type from the target.
|
|
19
13
|
<!-- /section:release-notes -->
|
|
20
14
|
|
|
21
15
|
## Installation
|
|
@@ -298,7 +292,11 @@ A prefix that would place the tree anywhere but directly inside the system tempo
|
|
|
298
292
|
```ts
|
|
299
293
|
interface TempTree extends Disposable {
|
|
300
294
|
readonly dir: string;
|
|
295
|
+
mkdir(entryPath: string): string;
|
|
301
296
|
resolve(...segments: string[]): string;
|
|
297
|
+
symlink(linkPath: string, targetPath: string): string;
|
|
298
|
+
write(entryPath: string, contents: string | Uint8Array): string;
|
|
299
|
+
writeJson(entryPath: string, value: unknown): string;
|
|
302
300
|
}
|
|
303
301
|
```
|
|
304
302
|
|
|
@@ -306,6 +304,32 @@ interface TempTree extends Disposable {
|
|
|
306
304
|
|
|
307
305
|
`resolve` joins `segments` against the root and throws when the result would fall outside it, so a stray `..` fails loudly rather than reaching into the enclosing directory. An absolute segment landing inside the root is returned unchanged. The containment test is lexical, so it does not follow a symlink inside the tree that points out of it.
|
|
308
306
|
|
|
307
|
+
`mkdir`, `symlink`, `write`, and `writeJson` write into the tree after it is built, for a fixture that varies per test or a file created to trigger a re-read:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
using tree = createTempTree({ 'packages/app/package.json': '{ "name": "app" }' });
|
|
311
|
+
|
|
312
|
+
tree.write('packages/app/src/main.ts', 'export {};\n'); // '/private/var/folders/.../packages/app/src/main.ts'
|
|
313
|
+
tree.writeJson('tsconfig.json', { include: ['src'] });
|
|
314
|
+
tree.mkdir('packages/empty');
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Each creates the parent directories it needs, resolves through the same containment check as `resolve`, and returns the absolute path of what it wrote. Both of `symlink`'s paths are checked, so an escaping target is refused as well as an escaping link.
|
|
318
|
+
|
|
319
|
+
They part company on an entry that already exists: `write` replaces it, `mkdir` leaves it and its contents alone, and `symlink` raises `EEXIST`.
|
|
320
|
+
|
|
321
|
+
`symlink` takes the link first and the target second, inverting `fs.symlinkSync`, so that it reads like the other methods: the path being created leads. The link type is chosen from the target, which is where the one portability difference lives. A directory is linked as a junction, which Windows creates without the elevation a directory symlink needs; every other target, one that does not exist included, is linked as a file. That last case matches what Node falls back to when no type is given, and it leaves the link dangling until the target appears.
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
using tree = createTempTree({ 'store/kit/package.json': '{ "name": "kit" }' });
|
|
325
|
+
|
|
326
|
+
tree.symlink('node_modules/kit', 'store/kit'); // a junction, so Windows needs no elevation
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The link stores an absolute path, which a junction requires. Code under test that reads a link rather than following it therefore sees a path under the tree root, where a package manager would have written a relative one; a fixture reproducing that shape reaches for `fs.symlinkSync` directly.
|
|
330
|
+
|
|
331
|
+
`writeJson` writes two-space-indented JSON ending in a newline, so a tree outliving a crashed run reads as a real config file would. A fixture needing exact bytes goes through `write` instead. A value `JSON.stringify` cannot represent -- `undefined`, a function, a symbol -- is refused rather than written, so an optional binding that arrived empty fails at the call that passed it instead of surfacing later as a parse error.
|
|
332
|
+
|
|
309
333
|
Disposal is idempotent.
|
|
310
334
|
|
|
311
335
|
`Disposable` is declared in `lib.esnext.disposable.d.ts` alone, so consuming this export requires `ESNext.Disposable` in your `lib`.
|
|
@@ -4,5 +4,9 @@ export interface CreateTempTreeOptions {
|
|
|
4
4
|
}
|
|
5
5
|
export interface TempTree extends Disposable {
|
|
6
6
|
readonly dir: string;
|
|
7
|
+
mkdir(entryPath: string): string;
|
|
7
8
|
resolve(...segments: string[]): string;
|
|
9
|
+
symlink(linkPath: string, targetPath: string): string;
|
|
10
|
+
write(entryPath: string, contents: string | Uint8Array): string;
|
|
11
|
+
writeJson(entryPath: string, value: unknown): string;
|
|
8
12
|
}
|
|
@@ -7,13 +7,11 @@ export function createTempTree(entries, options = {}) {
|
|
|
7
7
|
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), prefix)));
|
|
8
8
|
try {
|
|
9
9
|
for (const [entry, contents] of Object.entries(entries)) {
|
|
10
|
-
const entryPath = resolveWithinTree(dir, [entry]);
|
|
11
10
|
if (entry.endsWith('/')) {
|
|
12
|
-
|
|
11
|
+
mkdir(entry);
|
|
13
12
|
}
|
|
14
13
|
else {
|
|
15
|
-
|
|
16
|
-
fs.writeFileSync(entryPath, contents);
|
|
14
|
+
write(entry, contents);
|
|
17
15
|
}
|
|
18
16
|
}
|
|
19
17
|
}
|
|
@@ -21,11 +19,41 @@ export function createTempTree(entries, options = {}) {
|
|
|
21
19
|
fs.rmSync(dir, { force: true, recursive: true });
|
|
22
20
|
throw error;
|
|
23
21
|
}
|
|
22
|
+
function mkdir(entryPath) {
|
|
23
|
+
const absolutePath = resolveWithinTree(dir, [entryPath]);
|
|
24
|
+
fs.mkdirSync(absolutePath, { recursive: true });
|
|
25
|
+
return absolutePath;
|
|
26
|
+
}
|
|
27
|
+
function resolve(...segments) {
|
|
28
|
+
return resolveWithinTree(dir, segments);
|
|
29
|
+
}
|
|
30
|
+
function symlink(linkPath, targetPath) {
|
|
31
|
+
const absoluteLink = resolveWithinTree(dir, [linkPath]);
|
|
32
|
+
const absoluteTarget = resolveWithinTree(dir, [targetPath]);
|
|
33
|
+
fs.mkdirSync(path.dirname(absoluteLink), { recursive: true });
|
|
34
|
+
fs.symlinkSync(absoluteTarget, absoluteLink, chooseLinkType(absoluteTarget));
|
|
35
|
+
return absoluteLink;
|
|
36
|
+
}
|
|
37
|
+
function write(entryPath, contents) {
|
|
38
|
+
const absolutePath = resolveWithinTree(dir, [entryPath]);
|
|
39
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
40
|
+
fs.writeFileSync(absolutePath, contents);
|
|
41
|
+
return absolutePath;
|
|
42
|
+
}
|
|
43
|
+
function writeJson(entryPath, value) {
|
|
44
|
+
const json = JSON.stringify(value, null, 2);
|
|
45
|
+
if (json === undefined) {
|
|
46
|
+
throw new Error(`Value of type "${typeof value}" for "${entryPath}" has no JSON representation`);
|
|
47
|
+
}
|
|
48
|
+
return write(entryPath, `${json}\n`);
|
|
49
|
+
}
|
|
24
50
|
return {
|
|
25
51
|
dir,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
52
|
+
mkdir,
|
|
53
|
+
resolve,
|
|
54
|
+
symlink,
|
|
55
|
+
write,
|
|
56
|
+
writeJson,
|
|
29
57
|
[Symbol.dispose]() {
|
|
30
58
|
fs.rmSync(dir, { force: true, recursive: true });
|
|
31
59
|
},
|
|
@@ -39,6 +67,9 @@ function assertNamesDirectChild(prefix) {
|
|
|
39
67
|
throw new Error(`Temporary-directory prefix "${prefix}" names no new directory`);
|
|
40
68
|
}
|
|
41
69
|
}
|
|
70
|
+
function chooseLinkType(targetPath) {
|
|
71
|
+
return fs.statSync(targetPath, { throwIfNoEntry: false })?.isDirectory() === true ? 'junction' : 'file';
|
|
72
|
+
}
|
|
42
73
|
function resolveWithinTree(dir, segments) {
|
|
43
74
|
const target = path.resolve(dir, ...segments);
|
|
44
75
|
if (target !== dir && !target.startsWith(dir + path.sep)) {
|