path-class 0.12.2 → 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/src/Path.ts CHANGED
@@ -36,6 +36,7 @@ import type {
36
36
  readFileType,
37
37
  statType,
38
38
  } from "./modifiedNodeTypes";
39
+ import { stringifyIfPath } from "./stringifyfIfPath";
39
40
 
40
41
  // Note that (non-static) functions in this file are defined using `function(…)
41
42
  // { … }` rather than arrow functions, specifically because we want `this` to
@@ -76,6 +77,30 @@ export function resolutionPrefix(pathString: string): ResolutionPrefix {
76
77
  return ResolutionPrefix.Bare;
77
78
  }
78
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
+
79
104
  export class Path {
80
105
  // @ts-expect-error ts(2564): False positive. https://github.com/microsoft/TypeScript/issues/32194
81
106
  #path: string;
@@ -87,6 +112,27 @@ export class Path {
87
112
  this.#setNormalizedPath(s);
88
113
  }
89
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
+ }
90
136
  static fromString(s: string): Path {
91
137
  if (typeof s !== "string") {
92
138
  throw new Error(
@@ -126,31 +172,80 @@ export class Path {
126
172
  return new Path(new URL(Path.#pathlikeToString(path), baseURL));
127
173
  }
128
174
 
129
- static #pathlikeToString(path: string | URL | Path): string {
130
- if (path instanceof Path) {
131
- return path.#path;
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
+ );
132
221
  }
133
- if (path instanceof URL) {
134
- return fileURLToPath(path);
222
+
223
+ const other = new Path(potentialDescendant);
224
+ if (this.isAbsolutePath() !== other.isAbsolutePath()) {
225
+ return null;
135
226
  }
136
- if (typeof path === "string") {
137
- // TODO: allow turning off this heuristic?
138
- if (path.startsWith("file:///")) {
139
- return fileURLToPath(path);
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;
140
240
  }
141
- return path;
142
241
  }
143
- throw new Error("Invalid path");
242
+ return new Path("./")
243
+ .join(...otherParts.slice(thisParts.length))
244
+ .toggleTrailingSlash(other.hasTrailingSlash());
144
245
  }
145
246
 
146
- // Preserves the `ResolutionPrefix` status when possible.
147
- #setNormalizedPath(path: string): void {
148
- const prefix = resolutionPrefix(path);
149
- this.#path = join(path);
150
- if (prefix === ResolutionPrefix.Relative && !this.#path.startsWith(".")) {
151
- // We don't have to handle the case of `"."`, as it already starts with `"."`
152
- this.#path = `./${this.#path}`;
153
- }
247
+ unresolve(path: string | URL | Path): Path {
248
+ return Path.resolve(path, this);
154
249
  }
155
250
 
156
251
  isAbsolutePath(): boolean {
@@ -237,7 +332,9 @@ export class Path {
237
332
  }
238
333
  return s;
239
334
  });
240
- return new Path(join(this.#path, ...segmentStrings));
335
+ return new Path(
336
+ joinPreservingRelativeResolutionPrefix(this.#path, segmentStrings),
337
+ );
241
338
  }
242
339
 
243
340
  /**
@@ -496,7 +593,47 @@ export class Path {
496
593
  * */
497
594
  static async makeTempDir(prefix?: string): Promise<AsyncDisposablePath> {
498
595
  return new AsyncDisposablePath(
499
- await mkdtemp(new Path(tmpdir()).join(prefix ?? "js-temp-").toString()),
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 },
500
637
  );
501
638
  }
502
639
 
@@ -711,25 +848,22 @@ export class Path {
711
848
  }
712
849
 
713
850
  export class AsyncDisposablePath extends Path {
714
- async [Symbol.asyncDispose]() {
715
- await this.rm_rf();
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
+ }
716
862
  }
717
- }
718
863
 
719
- /**
720
- * This function is useful to serialize any `Path`s in a structure to pass on to
721
- * functions that do not know about the `Path` class, e.g.
722
- *
723
- * function process(args: (string | Path)[]) {
724
- * const argsAsStrings = args.map(stringifyIfPath);
725
- * }
726
- *
727
- */
728
- export function stringifyIfPath<T>(value: T | Path): T | string {
729
- if (value instanceof Path) {
730
- return value.toString();
731
- }
732
- return value;
864
+ async [Symbol.asyncDispose]() {
865
+ await (this.#options?.disposePathInstead ?? this).rm_rf();
866
+ }
733
867
  }
734
868
 
735
869
  export function mustNotHaveTrailingSlash(path: Path): void {
@@ -739,8 +873,3 @@ export function mustNotHaveTrailingSlash(path: Path): void {
739
873
  );
740
874
  }
741
875
  }
742
-
743
- const tmp = await Path.makeTempDir();
744
- (await tmp.join("foo.json").write("foo")).rename(
745
- tmp.join("sdfsD", "sdfsdfsdf", "sdfsdf.json"),
746
- );
package/src/index.ts CHANGED
@@ -2,5 +2,6 @@ export {
2
2
  AsyncDisposablePath,
3
3
  Path,
4
4
  ResolutionPrefix,
5
- stringifyIfPath,
6
5
  } from "./Path";
6
+
7
+ export { stringifyIfPath } from "./stringifyfIfPath";
@@ -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
+ }
@@ -2,12 +2,46 @@ import { expect, test } from "bun:test";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import "./PathSync";
5
+ import { execSync } from "node:child_process";
5
6
  import { constants } from "node:fs/promises";
6
- import { PrintableShellCommand } from "printable-shell-command";
7
7
  import { PathSync } from "./PathSync";
8
8
 
9
+ test.concurrent("PathSync.resolve(…)", () => {
10
+ expect(PathSync.resolve("foo/lish", new PathSync("/bar/baz")).path).toEqual(
11
+ "/bar/foo/lish",
12
+ );
13
+ expect(PathSync.resolve("foo/lish", new PathSync("/bar/baz/")).path).toEqual(
14
+ "/bar/baz/foo/lish",
15
+ );
16
+ expect(
17
+ () => PathSync.resolve("foo/lish", new PathSync("bar/baz")).path,
18
+ ).toThrow(/must be an absolute path/);
19
+ expect(PathSync.resolve("foo/lish", import.meta.url).path).toEqual(
20
+ new PathSync(import.meta.url).parent.join("foo/lish").path,
21
+ );
22
+ expect(PathSync.resolve("foo", "file:///hello/world").path).toEqual(
23
+ "/hello/foo",
24
+ );
25
+ expect(PathSync.resolve("foo", "file:///hello/world/").path).toEqual(
26
+ "/hello/world/foo",
27
+ );
28
+ });
29
+
30
+ test.concurrent(".resolve(…)", () => {
31
+ expect(new PathSync("/bar/baz").resolve("foo/lish").path).toEqual(
32
+ "/bar/foo/lish",
33
+ );
34
+ expect(new PathSync("/bar/baz/").resolve("foo/lish").path).toEqual(
35
+ "/bar/baz/foo/lish",
36
+ );
37
+ expect(() => new PathSync("bar/baz").resolve("foo/lish").path).toThrow(
38
+ /must be an absolute path/,
39
+ );
40
+ expect(new PathSync("/bar/baz").resolve("foo/lish")).toBeInstanceOf(PathSync);
41
+ });
42
+
9
43
  test.concurrent(".existsAsFileSync()", () => {
10
- const filePath = PathSync.makeTempDirSync().join("file.txt");
44
+ using filePath = PathSync.tempFilePathSync({ basename: "file.txt" });
11
45
  expect(filePath.existsSync()).toBe(false);
12
46
  expect(filePath.existsSync({ mustBe: "file" })).toBe(false);
13
47
  expect(filePath.existsSync({ mustBe: "directory" })).toBe(false);
@@ -25,29 +59,31 @@ test.concurrent(".existsAsFileSync()", () => {
25
59
  });
26
60
 
27
61
  test.concurrent(".existsAsDir()", () => {
28
- const filePath = PathSync.makeTempDirSync();
29
- expect(filePath.existsSync()).toBe(true);
30
- expect(() => filePath.existsSync({ mustBe: "file" })).toThrow(
62
+ using tempDir = PathSync.makeTempDirSync();
63
+ expect(tempDir.existsSync()).toBe(true);
64
+ expect(() => tempDir.existsSync({ mustBe: "file" })).toThrow(
31
65
  /PathSync exists but is not a file/,
32
66
  );
33
- expect(filePath.existsSync({ mustBe: "directory" })).toBe(true);
34
- expect(filePath.existsAsDirSync()).toBe(true);
35
- filePath.rm_rfSync();
36
- expect(filePath.existsSync()).toBe(false);
37
- expect(filePath.existsSync({ mustBe: "file" })).toBe(false);
38
- expect(filePath.existsSync({ mustBe: "directory" })).toBe(false);
39
- expect(filePath.existsAsDirSync()).toBe(false);
67
+ expect(tempDir.existsSync({ mustBe: "directory" })).toBe(true);
68
+ expect(tempDir.existsAsDirSync()).toBe(true);
69
+ tempDir.rm_rfSync();
70
+ expect(tempDir.existsSync()).toBe(false);
71
+ expect(tempDir.existsSync({ mustBe: "file" })).toBe(false);
72
+ expect(tempDir.existsSync({ mustBe: "directory" })).toBe(false);
73
+ expect(tempDir.existsAsDirSync()).toBe(false);
40
74
  });
41
75
 
42
76
  test.concurrent(".mkdirSync(…) (un-nested)", () => {
43
- const dir = PathSync.makeTempDirSync().join("mkdir-test");
77
+ using tempDir = PathSync.makeTempDirSync();
78
+ const dir = tempDir.join("mkdir-test");
44
79
  expect(dir.existsSync()).toBe(false);
45
80
  dir.mkdirSync();
46
81
  expect(dir.existsSync()).toBe(true);
47
82
  });
48
83
 
49
84
  test.concurrent(".mkdirSync(…) (nested)", () => {
50
- const dir = PathSync.makeTempDirSync().join("mkdir-test/nested");
85
+ using tempDir = PathSync.makeTempDirSync();
86
+ const dir = tempDir.join("mkdir-test/nested");
51
87
  expect(dir.existsSync()).toBe(false);
52
88
  expect(() => dir.mkdirSync({ recursive: false })).toThrow("no such file");
53
89
  dir.mkdirSync();
@@ -55,7 +91,7 @@ test.concurrent(".mkdirSync(…) (nested)", () => {
55
91
  });
56
92
 
57
93
  test.concurrent(".cpSync(…)", () => {
58
- const parentDir = PathSync.makeTempDirSync();
94
+ using parentDir = PathSync.makeTempDirSync();
59
95
  const file1 = parentDir.join("file1.txt");
60
96
  const file2 = parentDir.join("file2.txt");
61
97
  const file3 = parentDir.join("nonexistent/dirs/file3.txt");
@@ -80,7 +116,7 @@ test.concurrent(".cpSync(…)", () => {
80
116
  });
81
117
 
82
118
  test.concurrent(".renameSync(…)", () => {
83
- const parentDir = PathSync.makeTempDirSync();
119
+ using parentDir = PathSync.makeTempDirSync();
84
120
  const file1 = parentDir.join("file1.txt");
85
121
  const file2 = parentDir.join("file2.txt");
86
122
  const file3 = parentDir.join("nonexistent/dirs/file3.txt");
@@ -105,18 +141,28 @@ test.concurrent(".renameSync(…)", () => {
105
141
  });
106
142
 
107
143
  test.concurrent(".makeTempDirSync(…)", () => {
108
- const tempDir = PathSync.makeTempDirSync();
109
- expect(tempDir.path).toContain("/js-temp-");
110
- expect(tempDir.basename.path).toStartWith("js-temp-");
111
- expect(tempDir.existsAsDirSync()).toBe(true);
112
-
113
- const tempDir2 = PathSync.makeTempDirSync("foo");
114
- expect(tempDir2.path).not.toContain("/js-temp-");
115
- expect(tempDir2.basename.path).toStartWith("foo");
144
+ let disposablePathSyncString: string;
145
+ {
146
+ using tempDir = PathSync.makeTempDirSync();
147
+ disposablePathSyncString = tempDir.path;
148
+ expect(tempDir.path).toContain("/js-temp-");
149
+ expect(tempDir.basename.path).toStartWith("js-temp-");
150
+ expect(tempDir.existsAsDirSync()).toBe(true);
151
+ }
152
+ expect(new PathSync(disposablePathSyncString).existsAsDirSync()).toBe(false);
153
+
154
+ let disposablePathSyncString2: string;
155
+ {
156
+ using tempDir2 = PathSync.makeTempDirSync("foo");
157
+ disposablePathSyncString2 = tempDir2.path;
158
+ expect(tempDir2.path).not.toContain("/js-temp-");
159
+ expect(tempDir2.basename.path).toStartWith("foo");
160
+ }
161
+ expect(new PathSync(disposablePathSyncString2).existsAsDirSync()).toBe(false);
116
162
  });
117
163
 
118
164
  test.concurrent(".rmSync(…) (file)", () => {
119
- const file = PathSync.makeTempDirSync().join("file.txt");
165
+ using file = PathSync.tempFilePathSync({ basename: "file.txt" });
120
166
  file.writeSync("");
121
167
  expect(file.existsAsFileSync()).toBe(true);
122
168
  file.rmSync();
@@ -126,7 +172,7 @@ test.concurrent(".rmSync(…) (file)", () => {
126
172
  });
127
173
 
128
174
  test.concurrent(".rmSync(…) (folder)", () => {
129
- const tempDir = PathSync.makeTempDirSync();
175
+ using tempDir = PathSync.makeTempDirSync();
130
176
  const file = tempDir.join("file.txt");
131
177
  file.writeSync("");
132
178
  expect(tempDir.existsAsDirSync()).toBe(true);
@@ -138,7 +184,7 @@ test.concurrent(".rmSync(…) (folder)", () => {
138
184
  });
139
185
 
140
186
  test.concurrent(".rmDirSync(…) (folder)", () => {
141
- const tempDir = PathSync.makeTempDirSync();
187
+ using tempDir = PathSync.makeTempDirSync();
142
188
  const file = tempDir.join("file.txt");
143
189
  file.writeSync("");
144
190
  expect(tempDir.existsAsDirSync()).toBe(true);
@@ -150,7 +196,7 @@ test.concurrent(".rmDirSync(…) (folder)", () => {
150
196
  });
151
197
 
152
198
  test.concurrent(".rm_rfSync(…) (file)", () => {
153
- const file = PathSync.makeTempDirSync().join("file.txt");
199
+ using file = PathSync.tempFilePathSync({ basename: "file.txt" });
154
200
  file.writeSync("");
155
201
  expect(file.existsAsFileSync()).toBe(true);
156
202
  file.rm_rfSync();
@@ -161,7 +207,7 @@ test.concurrent(".rm_rfSync(…) (file)", () => {
161
207
  });
162
208
 
163
209
  test.concurrent(".rm_rfSync(…) (folder)", () => {
164
- const tempDir = PathSync.makeTempDirSync();
210
+ using tempDir = PathSync.makeTempDirSync();
165
211
  tempDir.join("file.txt").writeSync("");
166
212
  expect(tempDir.path).toContain("/js-temp-");
167
213
  expect(tempDir.existsSync()).toBe(true);
@@ -172,7 +218,7 @@ test.concurrent(".rm_rfSync(…) (folder)", () => {
172
218
  });
173
219
 
174
220
  test.concurrent(".readTextSync()", () => {
175
- const file = PathSync.makeTempDirSync().join("file.txt");
221
+ using file = PathSync.tempFilePathSync({ basename: "file.txt" });
176
222
  file.writeSync("hi");
177
223
  file.writeSync("bye");
178
224
 
@@ -181,7 +227,7 @@ test.concurrent(".readTextSync()", () => {
181
227
  });
182
228
 
183
229
  test.concurrent(".readJSONSync()", () => {
184
- const file = PathSync.makeTempDirSync().join("file.json");
230
+ using file = PathSync.tempFilePathSync({ basename: "file.json" });
185
231
  file.writeSync(JSON.stringify({ foo: "bar" }));
186
232
 
187
233
  expect(file.readJSONSync()).toEqual<Record<string, string>>({ foo: "bar" });
@@ -192,7 +238,7 @@ test.concurrent(".readJSONSync()", () => {
192
238
  });
193
239
 
194
240
  test.concurrent(".readJSONSync(…) with fallback", () => {
195
- const tempDir = PathSync.makeTempDirSync();
241
+ using tempDir = PathSync.makeTempDirSync();
196
242
  const file = tempDir.join("file.json");
197
243
  const json: { foo?: number } = file.readJSONSync({ fallback: { foo: 4 } });
198
244
  expect(json).toEqual({ foo: 4 });
@@ -210,7 +256,7 @@ test.concurrent(".readJSONSync(…) with fallback", () => {
210
256
  });
211
257
 
212
258
  test.concurrent(".writeSync(…)", () => {
213
- const tempDir = PathSync.makeTempDirSync();
259
+ using tempDir = PathSync.makeTempDirSync();
214
260
  const file = tempDir.join("file.json");
215
261
  expect(file.writeSync("foo")).toBe(file);
216
262
 
@@ -226,14 +272,14 @@ test.concurrent(".writeSync(…)", () => {
226
272
  });
227
273
 
228
274
  test.concurrent(".writeJSONSync(…)", () => {
229
- const file = PathSync.makeTempDirSync().join("file.json");
275
+ using file = PathSync.tempFilePathSync({ basename: "file.json" });
230
276
  expect(file.writeJSONSync({ foo: "bar" })).toBe(file);
231
277
 
232
278
  expect(file.readJSONSync()).toEqual<Record<string, string>>({ foo: "bar" });
233
279
  });
234
280
 
235
281
  test.concurrent(".appendFileSync(…)", () => {
236
- const file = PathSync.makeTempDirSync().join("file.txt");
282
+ using file = PathSync.tempFilePathSync({ basename: "file.txt" });
237
283
  file.appendFileSync("test\n");
238
284
  expect(file.readTextSync()).toEqual("test\n");
239
285
  file.appendFileSync("more\n");
@@ -241,7 +287,7 @@ test.concurrent(".appendFileSync(…)", () => {
241
287
  });
242
288
 
243
289
  test.concurrent(".readDirSync(…)", () => {
244
- const dir = PathSync.makeTempDirSync();
290
+ using dir = PathSync.makeTempDirSync();
245
291
  dir.join("file.txt").writeSync("hello");
246
292
  dir.join("dir/file.json").writeSync("hello");
247
293
 
@@ -255,7 +301,7 @@ test.concurrent(".readDirSync(…)", () => {
255
301
  });
256
302
 
257
303
  test.concurrent(".symlinkSync(…)", () => {
258
- const tempDir = PathSync.makeTempDirSync();
304
+ using tempDir = PathSync.makeTempDirSync();
259
305
  const source = tempDir.join("foo.txt");
260
306
  const target = tempDir.join("bar.txt");
261
307
  source.symlinkSync(target);
@@ -267,7 +313,7 @@ test.concurrent(".symlinkSync(…)", () => {
267
313
  });
268
314
 
269
315
  test.concurrent(".realpathSync(…)", () => {
270
- const tempDir = PathSync.makeTempDirSync();
316
+ using tempDir = PathSync.makeTempDirSync();
271
317
  const source = tempDir.join("foo.txt");
272
318
  source.writeSync("hello world!");
273
319
  const target = tempDir.join("bar.txt");
@@ -276,7 +322,7 @@ test.concurrent(".realpathSync(…)", () => {
276
322
  });
277
323
 
278
324
  test.concurrent(".statSync(…)", () => {
279
- const file = PathSync.makeTempDirSync().join("foo.txt");
325
+ using file = PathSync.tempFilePathSync({ basename: "foo.txt" });
280
326
  file.writeSync("hello");
281
327
 
282
328
  expect(file.statSync()?.size).toEqual(5);
@@ -285,7 +331,7 @@ test.concurrent(".statSync(…)", () => {
285
331
  });
286
332
 
287
333
  test.concurrent(".lstatSync(…)", () => {
288
- const tempDir = PathSync.makeTempDirSync();
334
+ using tempDir = PathSync.makeTempDirSync();
289
335
  const source = tempDir.join("foo.txt");
290
336
  const target = tempDir.join("bar.txt");
291
337
  source.symlinkSync(target);
@@ -297,46 +343,47 @@ test.concurrent(".lstatSync(…)", () => {
297
343
  expect(target.readTextSync()).toEqual("hello");
298
344
  });
299
345
 
346
+ // Note: this test uses `execSync(…)` because it runs the binary and returns
347
+ // expected error messages correctly. Further, it helps keep this entire test
348
+ // file sync (which we have some basic checks for in `lint-sync-code.ts` that
349
+ // don't seem like a great idea to work around).
300
350
  test.concurrent(".chmodSync(…)", () => {
301
- const binPath = PathSync.makeTempDirSync().join("nonexistent.bin");
302
- expect(() => new PrintableShellCommand(binPath, []).text()).toThrow(
303
- /ENOENT|Premature close/,
351
+ using binPath = PathSync.tempFilePathSync({ basename: "bin.bash" });
352
+ expect(() => execSync(binPath.path, { stdio: ["ignore"] })).toThrow(
353
+ /No such file or directory|not found/,
304
354
  );
305
- binPath.writeSync(`#!/usr/bin/env bash
306
-
307
- echo hi`);
308
- // TODO: why doesn't this work here instead (but works in `printable-shell-comand`)?
309
- // binPath.writeSync(`#!/usr/bin/env -S bun run --
355
+ binPath.writeSync(`#!/usr/bin/env -S bun run --
310
356
 
311
- // console.log("hi");`);
312
- expect(() => new PrintableShellCommand(binPath, []).text()).toThrow(
313
- /EACCES|Premature close/,
357
+ console.log("hi");`);
358
+ expect(() => execSync(binPath.path, { stdio: ["ignore"] })).toThrow(
359
+ /Permission denied/,
314
360
  );
315
361
  binPath.chmodSync(0o755);
316
- expect(() => new PrintableShellCommand(binPath, []).text()).not.toThrow();
362
+ expect(execSync(binPath.path, { encoding: "utf-8" })).toEqual("hi\n");
317
363
  });
318
364
 
365
+ // Note: this test uses `execSync(…)` because it runs the binary and returns
366
+ // expected error messages correctly. Further, it helps keep this entire test
367
+ // file sync (which we have some basic checks for in `lint-sync-code.ts` that
368
+ // don't seem like a great idea to work around).
319
369
  test.concurrent(".chmodXSync(…)", () => {
320
- const binPath = PathSync.makeTempDirSync().join("nonexistent.bin");
321
- expect(() => new PrintableShellCommand(binPath, []).text()).toThrow(
322
- /ENOENT|Premature close/,
370
+ using binPath = PathSync.tempFilePathSync({ basename: "bin.bash" });
371
+ expect(() => execSync(binPath.path, { stdio: ["ignore"] })).toThrow(
372
+ /No such file or directory|not found/,
323
373
  );
324
- binPath.writeSync(`#!/usr/bin/env bash
325
-
326
- echo hi`);
327
- // TODO: why doesn't this work here instead (but works in `printable-shell-comand`)?
328
- // binPath.writeSync(`#!/usr/bin/env -S bun run --
374
+ binPath.writeSync(`#!/usr/bin/env -S bun run --
329
375
 
330
- // console.log("hi");`);
331
- expect(() => new PrintableShellCommand(binPath, []).text()).toThrow(
332
- /EACCES|Premature close/,
376
+ console.log("hi");`);
377
+ // TODO: Should not be `ENOENT`? Probably `EACCES`.
378
+ expect(() => execSync(binPath.path, { stdio: ["ignore"] })).toThrow(
379
+ /Permission denied/,
333
380
  );
334
381
  expect(binPath.statSync().mode & constants.S_IWUSR).toBeTruthy();
335
382
  binPath.chmodSync(0o444);
336
383
  expect(binPath.statSync().mode & constants.S_IWUSR).toBeFalsy();
337
384
  expect(binPath.statSync().mode & constants.S_IXUSR).toBeFalsy();
338
385
  binPath.chmodXSync();
339
- expect(() => new PrintableShellCommand(binPath, []).text()).not.toThrow();
386
+ expect(execSync(binPath.path, { encoding: "utf-8" })).toEqual("hi\n");
340
387
  expect(binPath.statSync().mode & constants.S_IWUSR).toBeFalsy();
341
388
  expect(binPath.statSync().mode & constants.S_IXUSR).toBeTruthy();
342
389
  });