@williamthorsen/toolbelt.filesystem 0.8.2 → 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 +97 -13
- package/README.md +100 -151
- package/dist/esm/1-proposed/index.d.ts +0 -1
- package/dist/esm/1-proposed/index.js +0 -1
- package/dist/esm/1-proposed/replaceFileExtension.d.ts +20 -0
- package/dist/esm/3-candidate/index.d.ts +1 -1
- package/dist/esm/3-candidate/index.js +1 -1
- package/dist/esm/3-candidate/writeAtomic.d.ts +24 -0
- package/dist/esm/4-release/directory-chain-matches.d.ts +39 -0
- package/dist/esm/4-release/listDirectoryChain.d.ts +15 -0
- package/dist/esm/4-release/loadConfigCascade.d.ts +36 -0
- package/dist/esm/4-release/reconcileFile.d.ts +22 -0
- package/dist/esm/4-release/reconcileFileFromFile.d.ts +21 -0
- package/package.json +10 -2
- package/dist/esm/1-proposed/writeAtomic.d.ts +0 -1
- package/dist/esm/3-candidate/createTempTree.d.ts +0 -19
- package/dist/esm/3-candidate/createTempTree.js +0 -148
- /package/dist/esm/{1-proposed → 3-candidate}/writeAtomic.js +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes `content` to `filePath` through a sibling temp file and a rename, so a concurrent reader sees either the
|
|
3
|
+
* previous file or the complete new one, never a partial write. Siting the temp file beside the target
|
|
4
|
+
* keeps the rename within one filesystem, where it is atomic.
|
|
5
|
+
*
|
|
6
|
+
* Missing parent directories are created. An existing target's permission bits are copied onto the replacement,
|
|
7
|
+
* which a bare `writeFile` would preserve by truncating in place and a rename would otherwise reset to the
|
|
8
|
+
* platform default.
|
|
9
|
+
*
|
|
10
|
+
* Two guarantees that it does not make. Nothing is fsynced, so a power loss can lose a write from which this
|
|
11
|
+
* function has already returned; "atomic" here means no torn reads. And the rename replaces the target's directory
|
|
12
|
+
* entry, so a symlink at `filePath` becomes a regular file rather than being written through.
|
|
13
|
+
*
|
|
14
|
+
* A failure removes the temp file best-effort and rethrows the error that caused it. Where the cleanup itself
|
|
15
|
+
* fails, the temp file survives beside the target under a dot-prefixed name ending in `.tmp`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* await writeAtomic('.agents/manifest.json', `${JSON.stringify(manifest, null, 2)}\n`);
|
|
19
|
+
*
|
|
20
|
+
* @category Filesystem
|
|
21
|
+
* @experimental
|
|
22
|
+
* @stage candidate
|
|
23
|
+
*/
|
|
24
|
+
export declare function writeAtomic(filePath: string, content: string | Uint8Array): Promise<void>;
|
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
import { type ListDirectoryChainOptions } from './listDirectoryChain.js';
|
|
2
2
|
export interface DirectoryChainMatch {
|
|
3
|
+
/** The chain level at which the match was found, which differs from the entry's own directory for a nested name. */
|
|
3
4
|
dir: string;
|
|
4
5
|
entryName: string;
|
|
5
6
|
entryPath: string;
|
|
6
7
|
}
|
|
8
|
+
/** Forwarded to `listDirectoryChain`. */
|
|
7
9
|
export type DirectoryChainMatchOptions = ListDirectoryChainOptions;
|
|
10
|
+
/**
|
|
11
|
+
* Returns the nearest directory at or above `startDir` holding one of `names`, or `undefined` when none does.
|
|
12
|
+
*
|
|
13
|
+
* Probing stops at the first level that matches, so no level beyond it is touched. Where every level's match
|
|
14
|
+
* matters rather than the nearest, `listDirectoryChainMatches` collects them all.
|
|
15
|
+
*
|
|
16
|
+
* Each name is a path relative to the level against which it is probed, so a nested location such as
|
|
17
|
+
* `.config/stack.config.mjs` works. A name that would leave its level is rejected before any level is probed.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* findDirectoryChainMatch('/home/dev/app/src', ['.git']);
|
|
21
|
+
* // { dir: '/home/dev/app', entryName: '.git', entryPath: '/home/dev/app/.git' }
|
|
22
|
+
*
|
|
23
|
+
* @category Filesystem
|
|
24
|
+
* @stage release
|
|
25
|
+
* @throws If a name is absolute or escapes its level, or if `stopAtDir` is not on the chain.
|
|
26
|
+
*/
|
|
8
27
|
export declare function findDirectoryChainMatch(startDir: string, names: ReadonlyArray<string>, options?: DirectoryChainMatchOptions): DirectoryChainMatch | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Returns, for each directory in the chain at or above `startDir`, the first of `names` that exists there.
|
|
30
|
+
*
|
|
31
|
+
* Ordering is nearest first, and a level yields at most one match: the earliest of `names` found there. A level
|
|
32
|
+
* holding none contributes nothing, so an empty result is an ordinary outcome rather than an error. A name matches
|
|
33
|
+
* a directory as readily as a file.
|
|
34
|
+
*
|
|
35
|
+
* Every level is probed. Where only the nearest match matters, `findDirectoryChainMatch` stops at the first.
|
|
36
|
+
*
|
|
37
|
+
* Each name is a path relative to the level against which it is probed, so a nested location such as
|
|
38
|
+
* `.config/stack.config.mjs` works. A name that would leave its level is rejected before any level is probed.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* listDirectoryChainMatches('/home/dev/app/src', ['.git'], { stopAtDir: '/home/dev' });
|
|
42
|
+
* // [{ dir: '/home/dev/app', entryName: '.git', entryPath: '/home/dev/app/.git' }]
|
|
43
|
+
*
|
|
44
|
+
* @category Filesystem
|
|
45
|
+
* @stage release
|
|
46
|
+
* @throws If a name is absolute or escapes its level, or if `stopAtDir` is not on the chain.
|
|
47
|
+
*/
|
|
9
48
|
export declare function listDirectoryChainMatches(startDir: string, names: ReadonlyArray<string>, options?: DirectoryChainMatchOptions): DirectoryChainMatch[];
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns `startDir` resolved to an absolute path, followed by each of its ancestors, nearest first.
|
|
3
|
+
*
|
|
4
|
+
* The chain runs to the filesystem root unless `stopAtDir` bounds it, and always holds at least the start
|
|
5
|
+
* directory, which the return type records. Paths are manipulated as strings; nothing is read from disk.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* listDirectoryChain('/home/dev/app/src', { stopAtDir: '/home/dev' });
|
|
9
|
+
* // ['/home/dev/app/src', '/home/dev/app', '/home/dev']
|
|
10
|
+
*
|
|
11
|
+
* @category Filesystem
|
|
12
|
+
* @stage release
|
|
13
|
+
* @throws If `stopAtDir` is neither the start directory nor one of its ancestors.
|
|
14
|
+
*/
|
|
1
15
|
export declare function listDirectoryChain(startDir: string, options?: ListDirectoryChainOptions): [string, ...string[]];
|
|
2
16
|
export interface ListDirectoryChainOptions {
|
|
17
|
+
/** Bounds the ascent, inclusive. Must be the start directory or one of its ancestors. */
|
|
3
18
|
stopAtDir?: string | undefined;
|
|
4
19
|
}
|
|
@@ -1,17 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads every config file between a starting directory and `stopAtDir`, nearest first.
|
|
3
|
+
*
|
|
4
|
+
* At each level from `startDir` up to and including `stopAtDir`, the first of `fileNames` that exists
|
|
5
|
+
* is taken as that level's config; levels holding none contribute nothing. The matched files are then
|
|
6
|
+
* imported one at a time, nearest first, and `shouldStopAscent` is consulted after each: Once it returns
|
|
7
|
+
* true, the ascent halts and no farther file is imported. Nothing above `stopAtDir` is ever read:
|
|
8
|
+
* Each name must stay within the level against which it is probed, so one that escapes is rejected up front.
|
|
9
|
+
*
|
|
10
|
+
* The boundary is the caller's to choose, which keeps this function free of any notion of what
|
|
11
|
+
* marks a project. `findProjectRoot` from `@williamthorsen/toolbelt.packaging` resolves one from markers.
|
|
12
|
+
*
|
|
13
|
+
* Each config is the module's default export; validating its contents is the caller's job.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const { entries } = await loadConfigCascade({
|
|
17
|
+
* fileNames: ['stack.config.mjs'],
|
|
18
|
+
* shouldStopAscent: (config) => config.shouldStopAscent === true,
|
|
19
|
+
* startDir: process.cwd(),
|
|
20
|
+
* stopAtDir: findProjectRoot(process.cwd()).rootDir,
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* @category Filesystem
|
|
24
|
+
* @stage release
|
|
25
|
+
* @throws If a file name is absolute or escapes its level, if `stopAtDir` is off the chain, or if a matched
|
|
26
|
+
* file has no default export.
|
|
27
|
+
*/
|
|
1
28
|
export declare function loadConfigCascade<TConfig = unknown>(options: LoadConfigCascadeOptions<TConfig>): Promise<ConfigCascade<TConfig>>;
|
|
2
29
|
export type CascadeStopReason = 'predicate' | 'stop-dir';
|
|
3
30
|
export interface ConfigCascade<TConfig> {
|
|
31
|
+
/** Ordered nearest first, starting at `startDir`. */
|
|
4
32
|
entries: ConfigEntry<TConfig>[];
|
|
5
33
|
stopReason: CascadeStopReason;
|
|
6
34
|
}
|
|
7
35
|
export interface ConfigEntry<TConfig> {
|
|
8
36
|
config: TConfig;
|
|
37
|
+
/** The cascade level at which the file was found, which differs from the file's own directory when `fileNames` holds a nested path. */
|
|
9
38
|
dir: string;
|
|
10
39
|
filePath: string;
|
|
11
40
|
}
|
|
12
41
|
export interface LoadConfigCascadeOptions<TConfig> {
|
|
42
|
+
/**
|
|
43
|
+
* Paths relative to each level, in precedence order; the first that exists at a level is that level's config.
|
|
44
|
+
* A name that leaves its level (an absolute path, or one whose `..` segments escape it) is rejected, since
|
|
45
|
+
* it would read outside the bounds that the cascade exists to enforce.
|
|
46
|
+
*/
|
|
13
47
|
fileNames: ReadonlyArray<string>;
|
|
48
|
+
/** Called after each config is loaded; returning true halts the ascent before the next import. */
|
|
14
49
|
shouldStopAscent?: ((config: TConfig) => boolean) | undefined;
|
|
15
50
|
startDir: string;
|
|
51
|
+
/** Bounds the ascent, inclusive. Must be the start directory or one of its ancestors. */
|
|
16
52
|
stopAtDir: string;
|
|
17
53
|
}
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes `content` to `filePath`, reporting what the write took rather than throwing.
|
|
3
|
+
*
|
|
4
|
+
* Missing parent directories are created. An existing file is compared against `content` first, and what counts
|
|
5
|
+
* as a difference follows `conflictPolicy`: `'replace'` promises the file holds exactly `content` afterwards, so
|
|
6
|
+
* only byte-identical content reports `up-to-date`; `'skip'` modifies nothing either way, so its comparison
|
|
7
|
+
* ignores trailing whitespace per line and at end of file, which keeps formatter churn from reading as a
|
|
8
|
+
* conflict. `up-to-date` therefore means the same thing under both policies: This one has no work to do.
|
|
9
|
+
*
|
|
10
|
+
* An I/O error is reported as `failed` rather than thrown, which lets a caller writing several files
|
|
11
|
+
* collect a result for each instead of losing the rest to the first failure. A dry run writes nothing and
|
|
12
|
+
* creates no directory, returning the outcome that the real call would have produced, short of a write failure,
|
|
13
|
+
* which nothing detects without attempting the write.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* reconcileFile('.config/tool.config.ts', template, { conflictPolicy: 'replace' });
|
|
17
|
+
* // { filePath: '.config/tool.config.ts', outcome: 'overwritten' }
|
|
18
|
+
*
|
|
19
|
+
* @category Filesystem
|
|
20
|
+
* @stage release
|
|
21
|
+
*/
|
|
1
22
|
export declare function reconcileFile(filePath: string, content: string, options?: ReconcileFileOptions): FileReconciliation;
|
|
2
23
|
export type FileReconciliation = {
|
|
3
24
|
filePath: string;
|
|
@@ -12,6 +33,7 @@ export type FileReconciliation = {
|
|
|
12
33
|
error: string;
|
|
13
34
|
};
|
|
14
35
|
export interface ReconcileFileOptions {
|
|
36
|
+
/** What to do with an existing file whose content differs. Defaults to `'skip'`, which never destroys one. */
|
|
15
37
|
conflictPolicy?: 'replace' | 'skip' | undefined;
|
|
16
38
|
isDryRun?: boolean | undefined;
|
|
17
39
|
}
|
|
@@ -1,2 +1,23 @@
|
|
|
1
1
|
import { type FileReconciliation, type ReconcileFileOptions } from './reconcileFile.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reconciles `filePath` against the utf8 text of `sourcePath`, reporting what the write took rather than throwing.
|
|
4
|
+
*
|
|
5
|
+
* Everything past the read is `reconcileFile`: The comparison, the conflict policy, the created parent directories,
|
|
6
|
+
* and the outcome vocabulary are its. A source that cannot be read reports `failed`, and a missing source is not
|
|
7
|
+
* distinguished from an unreadable one. The reason names `sourcePath` and the cause; the path is interpolated
|
|
8
|
+
* because a read-stage failure has none of its own (`EISDIR: illegal operation on a directory, read`) and the
|
|
9
|
+
* result's `filePath` is the destination.
|
|
10
|
+
*
|
|
11
|
+
* The source is read even under `isDryRun`, because the outcome depends on comparing its content, so a dry run can
|
|
12
|
+
* report `failed` where `reconcileFile` cannot. It still writes nothing.
|
|
13
|
+
*
|
|
14
|
+
* The read is utf8 text, so a binary source is not supported.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* reconcileFileFromFile('.config/git-cliff.toml', bundledTemplatePath);
|
|
18
|
+
* // { filePath: '.config/git-cliff.toml', outcome: 'created' }
|
|
19
|
+
*
|
|
20
|
+
* @category Filesystem
|
|
21
|
+
* @stage release
|
|
22
|
+
*/
|
|
2
23
|
export declare function reconcileFileFromFile(filePath: string, sourcePath: string, options?: ReconcileFileOptions): FileReconciliation;
|
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 +0,0 @@
|
|
|
1
|
-
export declare function writeAtomic(filePath: string, content: string | Uint8Array): Promise<void>;
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
exists(entryPath: string): boolean;
|
|
8
|
-
list(entryPath?: string): string[];
|
|
9
|
-
listFiles(entryPath?: string): string[];
|
|
10
|
-
mkdir(entryPath: string): string;
|
|
11
|
-
read(entryPath: string): string;
|
|
12
|
-
readJson(entryPath: string): unknown;
|
|
13
|
-
resolve(...segments: string[]): string;
|
|
14
|
-
rm(entryPath: string): void;
|
|
15
|
-
symlink(linkPath: string, targetPath: string): string;
|
|
16
|
-
write(entryPath: string, contents: string | Uint8Array): string;
|
|
17
|
-
writeAll(entries: Record<string, string | Uint8Array>): void;
|
|
18
|
-
writeJson(entryPath: string, value: unknown): string;
|
|
19
|
-
}
|
|
@@ -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
|