path-class 0.12.1 → 0.13.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/dist/lib/path-class/Path.d.ts +74 -10
- package/dist/lib/path-class/chunks/{chunk-K2SCBFED.js → chunk-IZ3HOMWW.js} +157 -32
- package/dist/lib/path-class/chunks/chunk-IZ3HOMWW.js.map +7 -0
- package/dist/lib/path-class/index.d.ts +2 -1
- package/dist/lib/path-class/index.js +1 -1
- package/dist/lib/path-class/stringifyfIfPath.d.ts +11 -0
- package/dist/lib/path-class/sync/PathSync.d.ts +35 -1
- package/dist/lib/path-class/sync/index.js +59 -3
- package/dist/lib/path-class/sync/index.js.map +2 -2
- package/package.json +2 -1
- package/src/Path.test.ts +171 -97
- package/src/Path.ts +186 -43
- package/src/index.ts +2 -1
- package/src/stringifyIfPath.test.ts +9 -0
- package/src/stringifyfIfPath.ts +17 -0
- package/src/sync/PathSync.test.ts +111 -64
- package/src/sync/PathSync.ts +73 -3
- package/src/test.preload.ts +41 -2
- package/dist/lib/path-class/chunks/chunk-K2SCBFED.js.map +0 -7
package/src/Path.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
1
2
|
import {
|
|
2
3
|
appendFile,
|
|
3
4
|
chmod,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
import { homedir, tmpdir } from "node:os";
|
|
20
21
|
import { basename, dirname, extname, join } from "node:path";
|
|
21
22
|
import { cwd } from "node:process";
|
|
23
|
+
import { createInterface } from "node:readline/promises";
|
|
22
24
|
import { Readable } from "node:stream";
|
|
23
25
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
26
|
import {
|
|
@@ -34,6 +36,7 @@ import type {
|
|
|
34
36
|
readFileType,
|
|
35
37
|
statType,
|
|
36
38
|
} from "./modifiedNodeTypes";
|
|
39
|
+
import { stringifyIfPath } from "./stringifyfIfPath";
|
|
37
40
|
|
|
38
41
|
// Note that (non-static) functions in this file are defined using `function(…)
|
|
39
42
|
// { … }` rather than arrow functions, specifically because we want `this` to
|
|
@@ -74,6 +77,30 @@ export function resolutionPrefix(pathString: string): ResolutionPrefix {
|
|
|
74
77
|
return ResolutionPrefix.Bare;
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
const DEFAULT_TEMP_PREFIX = "js-temp-";
|
|
81
|
+
const DEFAULT_TEMP_FILE_NAME = "file";
|
|
82
|
+
|
|
83
|
+
function preserveRelativeResolutionPrefix(
|
|
84
|
+
pathString: string,
|
|
85
|
+
from: string,
|
|
86
|
+
): string {
|
|
87
|
+
if (
|
|
88
|
+
resolutionPrefix(from) === ResolutionPrefix.Relative &&
|
|
89
|
+
resolutionPrefix(pathString) !== ResolutionPrefix.Relative
|
|
90
|
+
) {
|
|
91
|
+
// We don't have to handle the case of `"."`, as it already starts with `"."`
|
|
92
|
+
return `./${pathString}`;
|
|
93
|
+
}
|
|
94
|
+
return pathString;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function joinPreservingRelativeResolutionPrefix(
|
|
98
|
+
base: string,
|
|
99
|
+
relative: string[],
|
|
100
|
+
): string {
|
|
101
|
+
return preserveRelativeResolutionPrefix(join(base, ...relative), base);
|
|
102
|
+
}
|
|
103
|
+
|
|
77
104
|
export class Path {
|
|
78
105
|
// @ts-expect-error ts(2564): False positive. https://github.com/microsoft/TypeScript/issues/32194
|
|
79
106
|
#path: string;
|
|
@@ -85,6 +112,27 @@ export class Path {
|
|
|
85
112
|
this.#setNormalizedPath(s);
|
|
86
113
|
}
|
|
87
114
|
|
|
115
|
+
static #pathlikeToString(path: string | URL | Path): string {
|
|
116
|
+
if (path instanceof Path) {
|
|
117
|
+
return path.#path;
|
|
118
|
+
}
|
|
119
|
+
if (path instanceof URL) {
|
|
120
|
+
return fileURLToPath(path);
|
|
121
|
+
}
|
|
122
|
+
if (typeof path === "string") {
|
|
123
|
+
// TODO: allow turning off this heuristic?
|
|
124
|
+
if (path.startsWith("file:///")) {
|
|
125
|
+
return fileURLToPath(path);
|
|
126
|
+
}
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
throw new Error("Invalid path");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Preserves the `ResolutionPrefix` status when possible.
|
|
133
|
+
#setNormalizedPath(path: string): void {
|
|
134
|
+
this.#path = joinPreservingRelativeResolutionPrefix(path, []);
|
|
135
|
+
}
|
|
88
136
|
static fromString(s: string): Path {
|
|
89
137
|
if (typeof s !== "string") {
|
|
90
138
|
throw new Error(
|
|
@@ -124,31 +172,80 @@ export class Path {
|
|
|
124
172
|
return new Path(new URL(Path.#pathlikeToString(path), baseURL));
|
|
125
173
|
}
|
|
126
174
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Convenience function. The following are equivalent:
|
|
177
|
+
*
|
|
178
|
+
* B.resolve(A);
|
|
179
|
+
* Path.resolve(A, B);
|
|
180
|
+
*
|
|
181
|
+
*/
|
|
182
|
+
resolve(path: string | URL | Path): Path {
|
|
183
|
+
return Path.resolve(path, this);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Computes the relative path from an ancestor (this) to another path.
|
|
188
|
+
*
|
|
189
|
+
* - If the path is a descendant *or is the same path*, this returns a
|
|
190
|
+
* relative path. Call {@link Path.asBare `.asBare()`} on the output if
|
|
191
|
+
* needed.
|
|
192
|
+
* - The output is `null` unless the input paths are:
|
|
193
|
+
* - Both absolute paths.
|
|
194
|
+
* - Both relative paths. Resolve the paths before passing to this function
|
|
195
|
+
* if needed.
|
|
196
|
+
* - Trailing slashes:
|
|
197
|
+
* - By default, the ancestor path must have a trailing slash for the
|
|
198
|
+
* function to be called. Pass `{ requireTrailingSlashForAncestor: false
|
|
199
|
+
* }` if needed. Note that recombining the ancestor path with the output
|
|
200
|
+
* using {@link Path.prototype.resolve `.resolve(…)` } does not result in
|
|
201
|
+
* the original input path if the ancestor path did not have a trailing
|
|
202
|
+
* slash.
|
|
203
|
+
* - For the descendant/same path check, trailing slashes are ignored. In
|
|
204
|
+
* particular, if the ancestor path has a trailing slash and the
|
|
205
|
+
* descendant path is the same path without a trailing slash, this is
|
|
206
|
+
* still considered to be the same path.
|
|
207
|
+
* - The output has trailing slash if and only if:
|
|
208
|
+
* - the input descendant does, or
|
|
209
|
+
* - the output is the absolute path `/`.
|
|
210
|
+
*/
|
|
211
|
+
descendantRelativePath(
|
|
212
|
+
potentialDescendant: string | URL | Path,
|
|
213
|
+
options?: { requireTrailingSlashForAncestor: boolean },
|
|
214
|
+
): Path | null {
|
|
215
|
+
const requireTrailingSlashForAncestor =
|
|
216
|
+
options?.requireTrailingSlashForAncestor ?? true;
|
|
217
|
+
if (requireTrailingSlashForAncestor && !this.hasTrailingSlash()) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
"Ancestor must have a trailing slash. Pass `{ requireTrailingSlashForAncestor: false }` if needed.",
|
|
220
|
+
);
|
|
130
221
|
}
|
|
131
|
-
|
|
132
|
-
|
|
222
|
+
|
|
223
|
+
const other = new Path(potentialDescendant);
|
|
224
|
+
if (this.isAbsolutePath() !== other.isAbsolutePath()) {
|
|
225
|
+
return null;
|
|
133
226
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
227
|
+
|
|
228
|
+
// Leading slashes are okay, as they will result in a `""` component for
|
|
229
|
+
// absolute paths (and we don't compare absolute paths to relative paths.)
|
|
230
|
+
const thisParts = this.toggleTrailingSlash(false).#path.split("/");
|
|
231
|
+
const otherParts = other.toggleTrailingSlash(false).#path.split("/");
|
|
232
|
+
|
|
233
|
+
if (otherParts.length < thisParts.length) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
for (let i = 0; i < thisParts.length; i++) {
|
|
237
|
+
console.log(i, thisParts[i], otherParts[i]);
|
|
238
|
+
if (thisParts[i] !== otherParts[i]) {
|
|
239
|
+
return null;
|
|
138
240
|
}
|
|
139
|
-
return path;
|
|
140
241
|
}
|
|
141
|
-
|
|
242
|
+
return new Path("./")
|
|
243
|
+
.join(...otherParts.slice(thisParts.length))
|
|
244
|
+
.toggleTrailingSlash(other.hasTrailingSlash());
|
|
142
245
|
}
|
|
143
246
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const prefix = resolutionPrefix(path);
|
|
147
|
-
this.#path = join(path);
|
|
148
|
-
if (prefix === ResolutionPrefix.Relative && !this.#path.startsWith(".")) {
|
|
149
|
-
// We don't have to handle the case of `"."`, as it already starts with `"."`
|
|
150
|
-
this.#path = `./${this.#path}`;
|
|
151
|
-
}
|
|
247
|
+
unresolve(path: string | URL | Path): Path {
|
|
248
|
+
return Path.resolve(path, this);
|
|
152
249
|
}
|
|
153
250
|
|
|
154
251
|
isAbsolutePath(): boolean {
|
|
@@ -235,7 +332,9 @@ export class Path {
|
|
|
235
332
|
}
|
|
236
333
|
return s;
|
|
237
334
|
});
|
|
238
|
-
return new Path(
|
|
335
|
+
return new Path(
|
|
336
|
+
joinPreservingRelativeResolutionPrefix(this.#path, segmentStrings),
|
|
337
|
+
);
|
|
239
338
|
}
|
|
240
339
|
|
|
241
340
|
/**
|
|
@@ -494,7 +593,47 @@ export class Path {
|
|
|
494
593
|
* */
|
|
495
594
|
static async makeTempDir(prefix?: string): Promise<AsyncDisposablePath> {
|
|
496
595
|
return new AsyncDisposablePath(
|
|
497
|
-
await mkdtemp(
|
|
596
|
+
await mkdtemp(
|
|
597
|
+
new Path(tmpdir()).join(prefix ?? DEFAULT_TEMP_PREFIX).toString(),
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Return a path:
|
|
604
|
+
*
|
|
605
|
+
* - whose parent dir is a temp dir that *has* been created, but
|
|
606
|
+
* - which has itself not yet been created.
|
|
607
|
+
*
|
|
608
|
+
* Note that this path can actually also be used to create dir, but it is most
|
|
609
|
+
* convenient to get a path for a temporary file that can be written to, while
|
|
610
|
+
* having a disposal implementation that cleans everything up:
|
|
611
|
+
*
|
|
612
|
+
* await using tempFile = await Path.tempFilePath({ basename: "foo.txt" });
|
|
613
|
+
* await tempFile.write("hello world!");
|
|
614
|
+
* // …
|
|
615
|
+
*
|
|
616
|
+
* Note that that the following are equivalent when *not* using `await using`:
|
|
617
|
+
*
|
|
618
|
+
* await Path.tempFilePath({ basename: "foo.txt" });
|
|
619
|
+
* (await Path.makeTempDir()).join("file.txt");
|
|
620
|
+
*
|
|
621
|
+
* However, it is recommended to use `await using` to ensure cleanup.
|
|
622
|
+
*/
|
|
623
|
+
static async tempFilePath(options: {
|
|
624
|
+
tempDirPrefix?: string;
|
|
625
|
+
basename?: string | Path;
|
|
626
|
+
}): Promise<AsyncDisposablePath> {
|
|
627
|
+
const tempDir = new Path(
|
|
628
|
+
await mkdtemp(
|
|
629
|
+
new Path(tmpdir())
|
|
630
|
+
.join(options?.tempDirPrefix ?? DEFAULT_TEMP_PREFIX)
|
|
631
|
+
.toString(),
|
|
632
|
+
),
|
|
633
|
+
);
|
|
634
|
+
return new AsyncDisposablePath(
|
|
635
|
+
tempDir.join(options?.basename ?? DEFAULT_TEMP_FILE_NAME),
|
|
636
|
+
{ disposePathInstead: tempDir },
|
|
498
637
|
);
|
|
499
638
|
}
|
|
500
639
|
|
|
@@ -524,6 +663,18 @@ export class Path {
|
|
|
524
663
|
return readFile(this.#path, "utf-8");
|
|
525
664
|
}
|
|
526
665
|
|
|
666
|
+
/**
|
|
667
|
+
* Yields one line from the text of the line at a time.
|
|
668
|
+
*
|
|
669
|
+
* This uses streams, so it can be considerably more efficient than calling e.g. `.split("\n")` on the output of {@link readText `.readText()`}.
|
|
670
|
+
*
|
|
671
|
+
* Note that this function does not have a `.readLinesSync()` counterpart.
|
|
672
|
+
*/
|
|
673
|
+
async *readLines(): AsyncIterable<string> {
|
|
674
|
+
const stream = createReadStream(this.#path, "utf-8");
|
|
675
|
+
yield* createInterface({ input: stream, terminal: false });
|
|
676
|
+
}
|
|
677
|
+
|
|
527
678
|
/**
|
|
528
679
|
* Reads JSON from the given file and parses it. No validation is performed
|
|
529
680
|
* (beyond JSON parsing).
|
|
@@ -697,25 +848,22 @@ export class Path {
|
|
|
697
848
|
}
|
|
698
849
|
|
|
699
850
|
export class AsyncDisposablePath extends Path {
|
|
700
|
-
|
|
701
|
-
|
|
851
|
+
#options?: { disposePathInstead: Path };
|
|
852
|
+
constructor(
|
|
853
|
+
path: ConstructorParameters<typeof Path>[0],
|
|
854
|
+
options?: { disposePathInstead: Path | string },
|
|
855
|
+
) {
|
|
856
|
+
super(path);
|
|
857
|
+
if (options) {
|
|
858
|
+
this.#options = {
|
|
859
|
+
disposePathInstead: new Path(options.disposePathInstead),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
702
862
|
}
|
|
703
|
-
}
|
|
704
863
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
*
|
|
709
|
-
* function process(args: (string | Path)[]) {
|
|
710
|
-
* const argsAsStrings = args.map(stringifyIfPath);
|
|
711
|
-
* }
|
|
712
|
-
*
|
|
713
|
-
*/
|
|
714
|
-
export function stringifyIfPath<T>(value: T | Path): T | string {
|
|
715
|
-
if (value instanceof Path) {
|
|
716
|
-
return value.toString();
|
|
717
|
-
}
|
|
718
|
-
return value;
|
|
864
|
+
async [Symbol.asyncDispose]() {
|
|
865
|
+
await (this.#options?.disposePathInstead ?? this).rm_rf();
|
|
866
|
+
}
|
|
719
867
|
}
|
|
720
868
|
|
|
721
869
|
export function mustNotHaveTrailingSlash(path: Path): void {
|
|
@@ -725,8 +873,3 @@ export function mustNotHaveTrailingSlash(path: Path): void {
|
|
|
725
873
|
);
|
|
726
874
|
}
|
|
727
875
|
}
|
|
728
|
-
|
|
729
|
-
const tmp = await Path.makeTempDir();
|
|
730
|
-
(await tmp.join("foo.json").write("foo")).rename(
|
|
731
|
-
tmp.join("sdfsD", "sdfsdfsdf", "sdfsdf.json"),
|
|
732
|
-
);
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { Path } from "./Path";
|
|
3
|
+
import { stringifyIfPath } from "./stringifyfIfPath";
|
|
4
|
+
|
|
5
|
+
test.concurrent(".stringifyIfPath(…)", async () => {
|
|
6
|
+
expect(stringifyIfPath(Path.homedir)).toBe("/mock/home/dir");
|
|
7
|
+
expect(stringifyIfPath("/mock/home/dir")).toBe("/mock/home/dir");
|
|
8
|
+
expect(stringifyIfPath(4)).toBe(4);
|
|
9
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Path } from "./Path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This function is useful to serialize any `Path`s in a structure to pass on to
|
|
5
|
+
* functions that do not know about the `Path` class, e.g.
|
|
6
|
+
*
|
|
7
|
+
* function process(args: (string | Path)[]) {
|
|
8
|
+
* const argsAsStrings = args.map(stringifyIfPath);
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
export function stringifyIfPath<T>(value: Exclude<T, Path> | Path): T | string {
|
|
13
|
+
if (value instanceof Path) {
|
|
14
|
+
return value.toString();
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|