path-class 0.6.1 → 0.7.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.
@@ -0,0 +1,215 @@
1
+ import {
2
+ cpSync,
3
+ lstatSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ statSync,
10
+ symlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { mustNotHaveTrailingSlash, Path } from "../Path";
14
+ import "./static";
15
+ import type { readDirSyncType, readFileSyncType } from "./modifiedNodeTypes";
16
+
17
+ // Note that (non-static) functions in this file are defined using `function(…)
18
+ // { … }` rather than arrow functions, specifically because we want `this` to
19
+ // operate on the `Path` instance.
20
+
21
+ declare module "../Path" {
22
+ interface Path {
23
+ existsSync(constraints?: { mustBe: "file" | "directory" }): boolean;
24
+ existsAsFileSync(): boolean;
25
+ existsAsDirSync(): boolean;
26
+
27
+ mkdirSync(options?: Parameters<typeof mkdirSync>[1]): Path;
28
+ cpSync(
29
+ destination: string | URL | Path,
30
+ options?: Parameters<typeof cpSync>[2],
31
+ ): Path;
32
+ renameSync(destination: string | URL | Path): void;
33
+
34
+ rmSync(options?: Parameters<typeof rmSync>[1]): void;
35
+ rm_rfSync(options?: Parameters<typeof rmSync>[1]): void;
36
+
37
+ readSync: typeof readFileSyncType;
38
+ readTextSync(): string;
39
+ readJSONSync<T>(): T;
40
+
41
+ writeSync(
42
+ data: Parameters<typeof writeFileSync>[1],
43
+ options?: Parameters<typeof writeFileSync>[2] | undefined,
44
+ ): Path;
45
+ writeJSONSync<T>(
46
+ data: T,
47
+ replacer?: Parameters<typeof JSON.stringify>[1],
48
+ space?: Parameters<typeof JSON.stringify>[2],
49
+ ): Path;
50
+
51
+ readDirSync: typeof readDirSyncType;
52
+
53
+ /** Returns the destination path. */
54
+ symlinkSync(
55
+ target: string | URL | Path,
56
+ type?: Parameters<typeof symlinkSync>[2],
57
+ ): Path;
58
+
59
+ statSync(
60
+ options?: Parameters<typeof statSync>[1],
61
+ ): ReturnType<typeof statSync>;
62
+
63
+ // I don't think `lstat` is a great name, but it does match the
64
+ // well-established canonical system call. So in this case we keep the
65
+ // awkward abbreviation.
66
+ lstatSync(
67
+ options?: Parameters<typeof lstatSync>[1],
68
+ ): ReturnType<typeof lstatSync>;
69
+ }
70
+ }
71
+
72
+ // TODO: find a neat way to dedup with the async version?
73
+ Path.prototype.existsSync = function (constraints?: {
74
+ mustBe: "file" | "directory";
75
+ }): boolean {
76
+ let stats: ReturnType<typeof statSync>;
77
+ try {
78
+ stats = statSync(this.path);
79
+ // biome-ignore lint/suspicious/noExplicitAny: TypeScript limitation
80
+ } catch (e: any) {
81
+ if (e.code === "ENOENT") {
82
+ return false;
83
+ }
84
+ throw e;
85
+ }
86
+ if (!constraints?.mustBe) {
87
+ return true;
88
+ }
89
+ switch (constraints?.mustBe) {
90
+ case "file": {
91
+ mustNotHaveTrailingSlash(this);
92
+ if (stats.isFile()) {
93
+ return true;
94
+ }
95
+ throw new Error(`Path exists but is not a file: ${this.path}`);
96
+ }
97
+ case "directory": {
98
+ if (stats.isDirectory()) {
99
+ return true;
100
+ }
101
+ throw new Error(`Path exists but is not a directory: ${this.path}`);
102
+ }
103
+ default: {
104
+ throw new Error("Invalid path type constraint");
105
+ }
106
+ }
107
+ };
108
+
109
+ Path.prototype.existsAsFileSync = function (): boolean {
110
+ return this.existsSync({ mustBe: "file" });
111
+ };
112
+
113
+ Path.prototype.existsAsDirSync = function (): boolean {
114
+ return this.existsSync({ mustBe: "directory" });
115
+ };
116
+
117
+ Path.prototype.mkdirSync = function (
118
+ options?: Parameters<typeof mkdirSync>[1],
119
+ ): Path {
120
+ const optionsObject = (() => {
121
+ if (typeof options === "string" || typeof options === "number") {
122
+ return { mode: options };
123
+ }
124
+ return options ?? {};
125
+ })();
126
+ mkdirSync(this.path, { recursive: true, ...optionsObject });
127
+ return this;
128
+ };
129
+
130
+ Path.prototype.cpSync = function (
131
+ destination: string | URL | Path,
132
+ options?: Parameters<typeof cpSync>[2],
133
+ ): Path {
134
+ cpSync(this.path, new Path(destination).path, options);
135
+ return new Path(destination);
136
+ };
137
+
138
+ Path.prototype.renameSync = function (destination: string | URL | Path): void {
139
+ renameSync(this.path, new Path(destination).path);
140
+ };
141
+
142
+ Path.prototype.rmSync = function (
143
+ options?: Parameters<typeof rmSync>[1],
144
+ ): void {
145
+ rmSync(this.path, options);
146
+ };
147
+
148
+ Path.prototype.rm_rfSync = function (
149
+ options?: Parameters<typeof rmSync>[1],
150
+ ): void {
151
+ this.rmSync({ recursive: true, force: true, ...(options ?? {}) });
152
+ };
153
+
154
+ Path.prototype.readSync = function () {
155
+ /** @ts-expect-error ts(2683) */
156
+ return readFileSync(this.path);
157
+ } as typeof readFileSyncType;
158
+
159
+ Path.prototype.readTextSync = function (): string {
160
+ return readFileSync(this.path, "utf-8");
161
+ };
162
+
163
+ Path.prototype.readJSONSync = function <T>(): T {
164
+ return JSON.parse(this.readTextSync());
165
+ };
166
+
167
+ Path.prototype.writeSync = function (
168
+ data: Parameters<typeof writeFileSync>[1],
169
+ options?: Parameters<typeof writeFileSync>[2],
170
+ ): Path {
171
+ this.parent.mkdirSync();
172
+ writeFileSync(this.path, data, options);
173
+ return this;
174
+ };
175
+
176
+ Path.prototype.writeJSONSync = function <T>(
177
+ data: T,
178
+ replacer: Parameters<typeof JSON.stringify>[1] = null,
179
+ space: Parameters<typeof JSON.stringify>[2] = " ",
180
+ ): Path {
181
+ this.parent.mkdirSync();
182
+ this.writeSync(JSON.stringify(data, replacer, space));
183
+ return this;
184
+ };
185
+
186
+ /** @ts-expect-error ts(2322): Wrangle types */
187
+ Path.prototype.readDirSync = function (options) {
188
+ // biome-ignore lint/suspicious/noExplicitAny: Needed to wrangle the types.
189
+ return readdirSync(this.path, options as any);
190
+ };
191
+
192
+ Path.prototype.symlinkSync = function symlink(
193
+ target: string | URL | Path,
194
+ type?: Parameters<typeof symlinkSync>[2],
195
+ ): Path {
196
+ const targetPath = new Path(target);
197
+ symlinkSync(
198
+ this.path,
199
+ targetPath.path,
200
+ type as Exclude<Parameters<typeof symlinkSync>[2], undefined>, // 🤷
201
+ );
202
+ return targetPath;
203
+ };
204
+
205
+ Path.prototype.statSync = function (
206
+ options?: Parameters<typeof statSync>[1],
207
+ ): ReturnType<typeof statSync> {
208
+ return statSync(this.path, options);
209
+ };
210
+
211
+ Path.prototype.lstatSync = function (
212
+ options?: Parameters<typeof lstatSync>[1],
213
+ ): ReturnType<typeof lstatSync> {
214
+ return lstatSync(this.path, options);
215
+ };
@@ -0,0 +1,66 @@
1
+ // Note: this file is `.ts` rather than `.d.ts` to ensure it ends up in the `tsc` output.
2
+
3
+ import type { Dirent, ObjectEncodingOptions } from "node:fs";
4
+
5
+ export declare function readFileSyncType(
6
+ options?: {
7
+ encoding?: null | undefined;
8
+ flag?: string | undefined;
9
+ } | null,
10
+ ): NonSharedBuffer;
11
+ export declare function readFileSyncType(
12
+ options:
13
+ | {
14
+ encoding: BufferEncoding;
15
+ flag?: string | undefined;
16
+ }
17
+ | BufferEncoding,
18
+ ): string;
19
+ export declare function readFileSyncType(
20
+ options?:
21
+ | (ObjectEncodingOptions & {
22
+ flag?: string | undefined;
23
+ })
24
+ | BufferEncoding
25
+ | null,
26
+ ): string | NonSharedBuffer;
27
+
28
+ export declare function readDirSyncType(
29
+ options?:
30
+ | {
31
+ encoding: BufferEncoding | null;
32
+ withFileTypes?: false | undefined;
33
+ recursive?: boolean | undefined;
34
+ }
35
+ | BufferEncoding
36
+ | null,
37
+ ): string[];
38
+ export declare function readDirSyncType(
39
+ options:
40
+ | {
41
+ encoding: "buffer";
42
+ withFileTypes?: false | undefined;
43
+ recursive?: boolean | undefined;
44
+ }
45
+ | "buffer",
46
+ ): Buffer[];
47
+ export declare function readDirSyncType(
48
+ options?:
49
+ | (ObjectEncodingOptions & {
50
+ withFileTypes?: false | undefined;
51
+ recursive?: boolean | undefined;
52
+ })
53
+ | BufferEncoding
54
+ | null,
55
+ ): string[] | Buffer[];
56
+ export declare function readDirSyncType(
57
+ options: ObjectEncodingOptions & {
58
+ withFileTypes: true;
59
+ recursive?: boolean | undefined;
60
+ },
61
+ ): Dirent[];
62
+ export declare function readDirSyncType(options: {
63
+ encoding: "buffer";
64
+ withFileTypes: true;
65
+ recursive?: boolean | undefined;
66
+ }): Dirent<Buffer>[];
@@ -0,0 +1,14 @@
1
+ import { mkdtempSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { Path } from "../Path";
4
+
5
+ declare module "../Path" {
6
+ namespace Path {
7
+ export function makeTempDirSync(prefix?: string): Path;
8
+ }
9
+ }
10
+
11
+ Path.makeTempDirSync = (prefix?: string): Path =>
12
+ new Path(
13
+ mkdtempSync(new Path(tmpdir()).join(prefix ?? "js-temp-").toString()),
14
+ );
@@ -0,0 +1,224 @@
1
+ import { expect, test } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { Path } from "../Path";
5
+ import "./index";
6
+
7
+ test(".existsAsFileSync()", () => {
8
+ const filePath = Path.makeTempDirSync().join("file.txt");
9
+ expect(filePath.existsSync()).toBe(false);
10
+ expect(filePath.existsSync({ mustBe: "file" })).toBe(false);
11
+ expect(filePath.existsSync({ mustBe: "directory" })).toBe(false);
12
+ expect(filePath.existsAsFileSync()).toBe(false);
13
+ filePath.writeSync("test");
14
+ expect(filePath.existsSync()).toBe(true);
15
+ expect(filePath.existsSync({ mustBe: "file" })).toBe(true);
16
+ expect(() => filePath.existsSync({ mustBe: "directory" })).toThrow(
17
+ /Path exists but is not a directory/,
18
+ );
19
+ expect(filePath.existsAsFileSync()).toBe(true);
20
+ });
21
+
22
+ test(".existsAsDir()", () => {
23
+ const filePath = Path.makeTempDirSync();
24
+ expect(filePath.existsSync()).toBe(true);
25
+ expect(() => filePath.existsSync({ mustBe: "file" })).toThrow(
26
+ /Path exists but is not a file/,
27
+ );
28
+ expect(filePath.existsSync({ mustBe: "directory" })).toBe(true);
29
+ expect(filePath.existsAsDirSync()).toBe(true);
30
+ filePath.rm_rfSync();
31
+ expect(filePath.existsSync()).toBe(false);
32
+ expect(filePath.existsSync({ mustBe: "file" })).toBe(false);
33
+ expect(filePath.existsSync({ mustBe: "directory" })).toBe(false);
34
+ expect(filePath.existsAsDirSync()).toBe(false);
35
+ });
36
+
37
+ test(".mkdirSync(…) (un-nested)", () => {
38
+ const dir = Path.makeTempDirSync().join("mkdir-test");
39
+ expect(dir.existsSync()).toBe(false);
40
+ dir.mkdirSync();
41
+ expect(dir.existsSync()).toBe(true);
42
+ });
43
+
44
+ test(".mkdirSync(…) (nested)", () => {
45
+ const dir = Path.makeTempDirSync().join("mkdir-test/nested");
46
+ expect(dir.existsSync()).toBe(false);
47
+ expect(() => dir.mkdirSync({ recursive: false })).toThrow("no such file");
48
+ dir.mkdirSync();
49
+ expect(dir.existsSync()).toBe(true);
50
+ });
51
+
52
+ test(".cpSync(…)", () => {
53
+ const parentDir = Path.makeTempDirSync();
54
+ const file1 = parentDir.join("file1.txt");
55
+ const file2 = parentDir.join("file2.txt");
56
+
57
+ file1.writeSync("hello world");
58
+ expect(file1.existsSync()).toBe(true);
59
+ expect(file2.existsSync()).toBe(false);
60
+
61
+ file1.cpSync(file2);
62
+ expect(file1.existsSync()).toBe(true);
63
+ expect(file2.existsSync()).toBe(true);
64
+ });
65
+
66
+ test(".renameSync(…)", () => {
67
+ const parentDir = Path.makeTempDirSync();
68
+ const file1 = parentDir.join("file1.txt");
69
+ const file2 = parentDir.join("file2.txt");
70
+
71
+ file1.writeSync("hello world");
72
+ expect(file1.existsSync()).toBe(true);
73
+ expect(file2.existsSync()).toBe(false);
74
+
75
+ file1.renameSync(file2);
76
+ expect(file1.existsSync()).toBe(false);
77
+ expect(file2.existsSync()).toBe(true);
78
+ });
79
+
80
+ test(".makeTempDirSync(…)", () => {
81
+ const tempDir = Path.makeTempDirSync();
82
+ expect(tempDir.path).toContain("/js-temp-");
83
+ expect(tempDir.basename.path).toStartWith("js-temp-");
84
+ expect(tempDir.existsAsDirSync()).toBe(true);
85
+
86
+ const tempDir2 = Path.makeTempDirSync("foo");
87
+ expect(tempDir2.path).not.toContain("/js-temp-");
88
+ expect(tempDir2.basename.path).toStartWith("foo");
89
+ });
90
+
91
+ test(".rmSync(…) (file)", () => {
92
+ const file = Path.makeTempDirSync().join("file.txt");
93
+ file.writeSync("");
94
+ expect(file.existsAsFileSync()).toBe(true);
95
+ file.rmSync();
96
+ expect(file.existsAsFileSync()).toBe(false);
97
+ expect(file.parent.existsAsDirSync()).toBe(true);
98
+ expect(() => file.rmSync()).toThrowError(/ENOENT/);
99
+ });
100
+
101
+ test(".rmSync(…) (folder)", () => {
102
+ const tempDir = Path.makeTempDirSync();
103
+ const file = tempDir.join("file.txt");
104
+ file.writeSync("");
105
+ expect(tempDir.existsAsDirSync()).toBe(true);
106
+ expect(() => tempDir.rmSync()).toThrowError(/EACCES|EFAULT/);
107
+ file.rmSync();
108
+ tempDir.rmSync({ recursive: true });
109
+ expect(tempDir.existsAsDirSync()).toBe(false);
110
+ expect(() => tempDir.rmSync()).toThrowError(/ENOENT/);
111
+ });
112
+
113
+ test(".rm_rfSync(…) (file)", () => {
114
+ const file = Path.makeTempDirSync().join("file.txt");
115
+ file.writeSync("");
116
+ expect(file.existsAsFileSync()).toBe(true);
117
+ file.rm_rfSync();
118
+ expect(file.existsAsFileSync()).toBe(false);
119
+ expect(file.parent.existsAsDirSync()).toBe(true);
120
+ file.rm_rfSync();
121
+ expect(file.existsAsFileSync()).toBe(false);
122
+ });
123
+
124
+ test(".rm_rfSync(…) (folder)", () => {
125
+ const tempDir = Path.makeTempDirSync();
126
+ tempDir.join("file.txt").writeSync("");
127
+ expect(tempDir.path).toContain("/js-temp-");
128
+ expect(tempDir.existsSync()).toBe(true);
129
+ tempDir.rm_rfSync();
130
+ expect(tempDir.existsSync()).toBe(false);
131
+ tempDir.rm_rfSync();
132
+ expect(tempDir.existsSync()).toBe(false);
133
+ });
134
+
135
+ test(".readTextSync()", () => {
136
+ const file = Path.makeTempDirSync().join("file.txt");
137
+ file.writeSync("hi");
138
+ file.writeSync("bye");
139
+
140
+ expect(file.readTextSync()).toBe("bye");
141
+ expect(readFileSync(file.path, "utf-8")).toBe("bye");
142
+ });
143
+
144
+ test(".readJSONWync()", () => {
145
+ const file = Path.makeTempDirSync().join("file.json");
146
+ file.writeSync(JSON.stringify({ foo: "bar" }));
147
+
148
+ expect(file.readJSONSync()).toEqual<Record<string, string>>({ foo: "bar" });
149
+ expect(file.readJSONSync<Record<string, string>>()).toEqual({ foo: "bar" });
150
+ expect(JSON.parse(readFileSync(file.path, "utf-8"))).toEqual<
151
+ Record<string, string>
152
+ >({ foo: "bar" });
153
+ });
154
+
155
+ test(".writeSync(…)", () => {
156
+ const tempDir = Path.makeTempDirSync();
157
+ const file = tempDir.join("file.json");
158
+ expect(file.writeSync("foo")).toBe(file);
159
+
160
+ expect(readFileSync(join(tempDir.path, "./file.json"), "utf-8")).toEqual(
161
+ "foo",
162
+ );
163
+
164
+ const file2 = tempDir.join("nested/file2.json");
165
+ expect(file2.writeSync("bar")).toBe(file2);
166
+ expect(
167
+ readFileSync(join(tempDir.path, "./nested/file2.json"), "utf-8"),
168
+ ).toEqual("bar");
169
+ });
170
+
171
+ test(".writeJSONSync(…)", () => {
172
+ const file = Path.makeTempDirSync().join("file.json");
173
+ expect(file.writeJSONSync({ foo: "bar" })).toBe(file);
174
+
175
+ expect(file.readJSONSync()).toEqual<Record<string, string>>({ foo: "bar" });
176
+ });
177
+
178
+ test(".readDirSync(…)", () => {
179
+ const dir = Path.makeTempDirSync();
180
+ dir.join("file.txt").writeSync("hello");
181
+ dir.join("dir/file.json").writeSync("hello");
182
+
183
+ const contentsAsStrings = dir.readDirSync();
184
+ expect(new Set(contentsAsStrings)).toEqual(new Set(["file.txt", "dir"]));
185
+
186
+ const contentsAsEntries = dir.readDirSync({ withFileTypes: true });
187
+ expect(new Set(contentsAsEntries.map((entry) => entry.name))).toEqual(
188
+ new Set(["file.txt", "dir"]),
189
+ );
190
+ });
191
+
192
+ test(".symlinkSync(…)", () => {
193
+ const tempDir = Path.makeTempDirSync();
194
+ const source = tempDir.join("foo.txt");
195
+ const target = tempDir.join("bar.txt");
196
+ source.symlinkSync(target);
197
+ expect(target.existsAsFileSync()).toBe(false);
198
+ expect(() => target.readText()).toThrow(/ENOENT/);
199
+ source.writeSync("hello");
200
+ expect(target.existsAsFileSync()).toBe(true);
201
+ expect(target.readTextSync()).toEqual("hello");
202
+ });
203
+
204
+ test(".statSync(…)", () => {
205
+ const file = Path.makeTempDirSync().join("foo.txt");
206
+ file.writeSync("hello");
207
+
208
+ expect(file.statSync()?.size).toEqual(5);
209
+ expect(file.statSync()?.size).toBeTypeOf("number");
210
+ expect(file.statSync({ bigint: true })?.size).toBeTypeOf("bigint");
211
+ });
212
+
213
+ test(".lstatSync(…)", () => {
214
+ const tempDir = Path.makeTempDirSync();
215
+ const source = tempDir.join("foo.txt");
216
+ const target = tempDir.join("bar.txt");
217
+ source.symlinkSync(target);
218
+ source.writeSync("hello");
219
+
220
+ expect(source.lstatSync()?.isSymbolicLink()).toBe(false);
221
+ expect(target.lstatSync()?.isSymbolicLink()).toBe(true);
222
+
223
+ expect(target.readTextSync()).toEqual("hello");
224
+ });