@williamthorsen/toolbelt.filesystem 0.4.1 → 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 +28 -0
- package/README.md +87 -6
- package/dist/esm/1-proposed/index.d.ts +1 -1
- package/dist/esm/1-proposed/index.js +1 -1
- package/dist/esm/1-proposed/writeAtomic.d.ts +1 -0
- package/dist/esm/1-proposed/writeAtomic.js +32 -0
- package/dist/esm/3-candidate/createTempTree.d.ts +12 -0
- package/dist/esm/3-candidate/createTempTree.js +79 -0
- package/dist/esm/3-candidate/index.d.ts +1 -1
- package/dist/esm/3-candidate/index.js +1 -1
- package/package.json +2 -2
- package/dist/esm/1-proposed/createTempTree.d.ts +0 -5
- package/dist/esm/1-proposed/createTempTree.js +0 -38
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.5.0 — 2026-08-15
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
- 🚨 **Breaking:** Promote createTempTree to the candidate tier with a caller-chosen prefix and binary entries (#148)
|
|
24
|
+
|
|
25
|
+
Promotes `createTempTree` and `TempTree` to `@williamthorsen/toolbelt.filesystem/candidate`, adding two capabilities as they move: a caller-chosen prefix for the temporary directory's name, and file contents given as bytes. A prefix that would place the tree anywhere but directly inside the system temporary directory is rejected before anything is created.
|
|
26
|
+
|
|
27
|
+
Migration: `createTempTree` and `TempTree` are imported from `@williamthorsen/toolbelt.filesystem/candidate` rather than `/proposed`.
|
|
28
|
+
|
|
29
|
+
- Add `writeAtomic`, an atomic file-write utility (#154)
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
5
33
|
## 0.4.1 — 2026-08-13
|
|
6
34
|
|
|
7
35
|
### Tooling
|
package/README.md
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Filesystem utilities for TypeScript and JavaScript.
|
|
4
4
|
|
|
5
|
-
<!-- section:release-notes
|
|
5
|
+
<!-- section:release-notes -->
|
|
6
|
+
## Release notes — v0.6.0 (2026-08-16)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- Add mkdir, symlink, write, and writeJson methods to `TempTree` (#176)
|
|
11
|
+
|
|
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.
|
|
13
|
+
<!-- /section:release-notes -->
|
|
6
14
|
|
|
7
15
|
## Installation
|
|
8
16
|
|
|
@@ -12,7 +20,7 @@ pnpm add @williamthorsen/toolbelt.filesystem
|
|
|
12
20
|
|
|
13
21
|
## Runtime requirements
|
|
14
22
|
|
|
15
|
-
`createTempTree`, `findDirectoryChainMatch`, `listDirectoryChainMatches`, `loadConfigCascade`, `reconcileFile`, and `
|
|
23
|
+
`createTempTree`, `findDirectoryChainMatch`, `listDirectoryChainMatches`, `loadConfigCascade`, `reconcileFile`, `reconcileFileFromFile`, and `writeAtomic` reach the filesystem through `node:` builtins, so they run under Node.js 24 or later, Bun, and Deno. They do not run in browsers, nor in edge runtimes that expose no filesystem. `listDirectoryChain` and `replaceFileExtension` touch no filesystem, so an edge runtime that exposes none runs them; they still import `node:path`, which a browser bundle has to supply.
|
|
16
24
|
|
|
17
25
|
`loadConfigCascade` imports each config through the host runtime, so a `.ts` config is subject to whatever that runtime does with TypeScript. Node strips types rather than compiling them, which admits erasable syntax alone: an `enum`, a `namespace`, or a parameter property in a config file fails to parse. A `.mjs` or `.js` config sidesteps the question.
|
|
18
26
|
|
|
@@ -98,7 +106,7 @@ findDirectoryChainMatch('/home/dev/app/src', ['.git']);
|
|
|
98
106
|
// { dir: '/home/dev/app', entryName: '.git', entryPath: '/home/dev/app/.git' }
|
|
99
107
|
```
|
|
100
108
|
|
|
101
|
-
Probing stops at the first level that matches, so no level beyond it is touched
|
|
109
|
+
Probing stops at the first level that matches, so no level beyond it is touched -- the reason to reach for this rather than read element zero off `listDirectoryChainMatches`, which probes to the ceiling regardless. The nullable return type is the other reason: a result that may be absent says so, where an array leaves the caller to narrow.
|
|
102
110
|
|
|
103
111
|
## `loadConfigCascade`
|
|
104
112
|
|
|
@@ -241,16 +249,16 @@ The source is read even under `isDryRun`, because the outcome depends on compari
|
|
|
241
249
|
|
|
242
250
|
## `createTempTree`
|
|
243
251
|
|
|
244
|
-
|
|
252
|
+
Candidate tier: imported from `@williamthorsen/toolbelt.filesystem/candidate` rather than the package root, and subject to change.
|
|
245
253
|
|
|
246
254
|
```ts
|
|
247
|
-
createTempTree(entries: Record<string, string
|
|
255
|
+
createTempTree(entries: Record<string, string | Uint8Array>, options?: { prefix?: string }): TempTree;
|
|
248
256
|
```
|
|
249
257
|
|
|
250
258
|
Builds a throwaway directory tree and returns a handle that removes it when the binding leaves scope:
|
|
251
259
|
|
|
252
260
|
```ts
|
|
253
|
-
import { createTempTree } from '@williamthorsen/toolbelt.filesystem/
|
|
261
|
+
import { createTempTree } from '@williamthorsen/toolbelt.filesystem/candidate';
|
|
254
262
|
|
|
255
263
|
{
|
|
256
264
|
using tree = createTempTree({
|
|
@@ -266,10 +274,29 @@ import { createTempTree } from '@williamthorsen/toolbelt.filesystem/proposed';
|
|
|
266
274
|
|
|
267
275
|
Each key of `entries` is a path relative to the tree root. One ending in `/` becomes a directory; any other becomes a file holding the mapped contents, with its intermediate directories created for it. A key resolving outside the root is rejected, and a call that throws leaves nothing on disk.
|
|
268
276
|
|
|
277
|
+
A value is text or the bytes themselves, so a body no UTF-8 round trip survives is as writable as a string:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
using tree = createTempTree({ 'logo.png': pngBytes });
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`prefix` names the directory built under the system temporary directory, defaulting to `toolbelt-`. Set it to whatever is doing the building, so a tree outliving a crashed run says what made it:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
using tree = createTempTree({}, { prefix: 'rdy-tsconfig-' });
|
|
287
|
+
tree.dir; // '/private/var/folders/.../rdy-tsconfig-a1b2c3'
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
A prefix that would place the tree anywhere but directly inside the system temporary directory is rejected before anything is created. `mkdtemp` appends its random suffix to the joined path as given, so a prefix holding `/` or `\` targets a nested directory that has to already exist, or, where it ascends, a directory outside the temporary one; and a prefix that normalizes away (`''`, `'.'`, or `'..'`) lands the suffix beside the temporary directory rather than within it.
|
|
291
|
+
|
|
269
292
|
```ts
|
|
270
293
|
interface TempTree extends Disposable {
|
|
271
294
|
readonly dir: string;
|
|
295
|
+
mkdir(entryPath: string): string;
|
|
272
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;
|
|
273
300
|
}
|
|
274
301
|
```
|
|
275
302
|
|
|
@@ -277,6 +304,32 @@ interface TempTree extends Disposable {
|
|
|
277
304
|
|
|
278
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.
|
|
279
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
|
+
|
|
280
333
|
Disposal is idempotent.
|
|
281
334
|
|
|
282
335
|
`Disposable` is declared in `lib.esnext.disposable.d.ts` alone, so consuming this export requires `ESNext.Disposable` in your `lib`.
|
|
@@ -308,3 +361,31 @@ replaceFileExtension('src/main.d.ts', '.js', { oldExtension: '.d.ts' }); // 'src
|
|
|
308
361
|
Which extension is meant is genuinely ambiguous, since `archive.tar.gz` could reasonably end in `.gz` or in `.tar.gz`, so the caller declares it rather than the function guessing.
|
|
309
362
|
|
|
310
363
|
Two inputs throw rather than returning a path that would quietly be wrong: a `filePath` ending in a separator, which names a directory rather than a file, and a `filePath` that does not end with a declared `oldExtension`.
|
|
364
|
+
|
|
365
|
+
## `writeAtomic`
|
|
366
|
+
|
|
367
|
+
Proposed tier: imported from `@williamthorsen/toolbelt.filesystem/proposed` rather than the package root, and subject to change.
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
writeAtomic(filePath: string, content: string | Uint8Array): Promise<void>;
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Writes `content` to `filePath` through a temp file and a rename, so a concurrent reader sees either the previous file or the complete new one, never a partial write:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import { writeAtomic } from '@williamthorsen/toolbelt.filesystem/proposed';
|
|
377
|
+
|
|
378
|
+
await writeAtomic('.agents/manifest.json', `${JSON.stringify(manifest, null, 2)}\n`);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The temp file is a sibling of the target, which is the part a hand-rolled copy most often gets wrong: `rename` is atomic only within one filesystem, so a temp file staged under the system temporary directory fails with `EXDEV` the moment the target lives on another volume. Its name is dot-prefixed and carries a random component, so it stays out of `*` globs and two processes writing the same target do not collide.
|
|
382
|
+
|
|
383
|
+
Missing parent directories are created, as they are for [`reconcileFile`](#reconcilefile).
|
|
384
|
+
|
|
385
|
+
An existing target's permission bits are carried onto the replacement. A plain `writeFile` truncates the file in place and so preserves its mode, while a rename replaces the inode and would otherwise reset it to the platform default; without this, swapping a plain write for an atomic one would silently widen a `0o600` file to world-readable. A target that does not exist yet gets the platform default, exactly as a plain write would.
|
|
386
|
+
|
|
387
|
+
Three behaviors are worth knowing before they surprise you:
|
|
388
|
+
|
|
389
|
+
- Nothing is fsynced. "Atomic" here means no torn reads, not survives-power-loss: a write this function has returned from can still be lost to a power failure. A durability option is additive if a caller ever needs one.
|
|
390
|
+
- A symlink at `filePath` is replaced by a regular file rather than written through, because the rename replaces the target's directory entry. The link's former target is left untouched.
|
|
391
|
+
- A failure removes the temp file best-effort and rethrows the error that caused it, never the cleanup's own. Where the cleanup also fails, the temp file survives beside the target under its dot-prefixed name ending in `.tmp`, which is where to look for one.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeAtomic(filePath: string, content: string | Uint8Array): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export async function writeAtomic(filePath, content) {
|
|
5
|
+
const dir = path.dirname(filePath);
|
|
6
|
+
await fs.mkdir(dir, { recursive: true });
|
|
7
|
+
const mode = await findFileMode(filePath);
|
|
8
|
+
const tempPath = path.join(dir, `.${path.basename(filePath)}.${randomBytes(8).toString('hex')}.tmp`);
|
|
9
|
+
try {
|
|
10
|
+
await fs.writeFile(tempPath, content, { mode });
|
|
11
|
+
if (mode !== undefined)
|
|
12
|
+
await fs.chmod(tempPath, mode);
|
|
13
|
+
await fs.rename(tempPath, filePath);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
try {
|
|
17
|
+
await fs.rm(tempPath, { force: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function findFileMode(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const stats = await fs.stat(filePath);
|
|
27
|
+
return stats.mode & 0o777;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function createTempTree(entries: Record<string, string | Uint8Array>, options?: CreateTempTreeOptions): TempTree;
|
|
2
|
+
export interface CreateTempTreeOptions {
|
|
3
|
+
prefix?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TempTree extends Disposable {
|
|
6
|
+
readonly dir: string;
|
|
7
|
+
mkdir(entryPath: string): string;
|
|
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;
|
|
12
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
for (const [entry, contents] of Object.entries(entries)) {
|
|
10
|
+
if (entry.endsWith('/')) {
|
|
11
|
+
mkdir(entry);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
write(entry, contents);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
fs.rmSync(dir, { force: true, recursive: true });
|
|
20
|
+
throw error;
|
|
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
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
dir,
|
|
52
|
+
mkdir,
|
|
53
|
+
resolve,
|
|
54
|
+
symlink,
|
|
55
|
+
write,
|
|
56
|
+
writeJson,
|
|
57
|
+
[Symbol.dispose]() {
|
|
58
|
+
fs.rmSync(dir, { force: true, recursive: true });
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function assertNamesDirectChild(prefix) {
|
|
63
|
+
if (prefix.includes('/') || prefix.includes('\\')) {
|
|
64
|
+
throw new Error(`Temporary-directory prefix "${prefix}" contains a path separator`);
|
|
65
|
+
}
|
|
66
|
+
if (['', '.', '..'].includes(prefix)) {
|
|
67
|
+
throw new Error(`Temporary-directory prefix "${prefix}" names no new directory`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function chooseLinkType(targetPath) {
|
|
71
|
+
return fs.statSync(targetPath, { throwIfNoEntry: false })?.isDirectory() === true ? 'junction' : 'file';
|
|
72
|
+
}
|
|
73
|
+
function resolveWithinTree(dir, segments) {
|
|
74
|
+
const target = path.resolve(dir, ...segments);
|
|
75
|
+
if (target !== dir && !target.startsWith(dir + path.sep)) {
|
|
76
|
+
throw new Error(`Path "${target}" falls outside the temporary tree at "${dir}"`);
|
|
77
|
+
}
|
|
78
|
+
return target;
|
|
79
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { createTempTree, type CreateTempTreeOptions, type TempTree } from './createTempTree.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { createTempTree } from "./createTempTree.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/toolbelt.filesystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Filesystem utilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config-cascade",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"CHANGELOG.md"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@williamthorsen/toolbelt.errors": "0.
|
|
47
|
+
"@williamthorsen/toolbelt.errors": "0.5.0"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=24.0.0"
|
|
@@ -1,38 +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) {
|
|
5
|
-
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'toolbelt-')));
|
|
6
|
-
try {
|
|
7
|
-
for (const [entry, contents] of Object.entries(entries)) {
|
|
8
|
-
const entryPath = resolveWithinTree(dir, [entry]);
|
|
9
|
-
if (entry.endsWith('/')) {
|
|
10
|
-
fs.mkdirSync(entryPath, { recursive: true });
|
|
11
|
-
}
|
|
12
|
-
else {
|
|
13
|
-
fs.mkdirSync(path.dirname(entryPath), { recursive: true });
|
|
14
|
-
fs.writeFileSync(entryPath, contents);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
fs.rmSync(dir, { force: true, recursive: true });
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
dir,
|
|
24
|
-
resolve(...segments) {
|
|
25
|
-
return resolveWithinTree(dir, segments);
|
|
26
|
-
},
|
|
27
|
-
[Symbol.dispose]() {
|
|
28
|
-
fs.rmSync(dir, { force: true, recursive: true });
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function resolveWithinTree(dir, segments) {
|
|
33
|
-
const target = path.resolve(dir, ...segments);
|
|
34
|
-
if (target !== dir && !target.startsWith(dir + path.sep)) {
|
|
35
|
-
throw new Error(`Path "${target}" falls outside the temporary tree at "${dir}"`);
|
|
36
|
-
}
|
|
37
|
-
return target;
|
|
38
|
-
}
|