path-class 0.3.4 → 0.4.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.
@@ -25,6 +25,26 @@ declare function readDirType(options: {
25
25
  export declare class Path {
26
26
  #private;
27
27
  constructor(path: string | URL | Path);
28
+ /**
29
+ * Similar to `new URL(path, base)`, but accepting and returning `Path` objects.
30
+ * Note that `base` must be one of:
31
+ *
32
+ * - a `string` representing an absolute path,
33
+ * - a `URL` object, or
34
+ * - a `Path` representing an absolute path.
35
+ *
36
+ */
37
+ static resolve(path: string | URL | Path, base: string | URL | Path): Path;
38
+ isAbsolutePath(): boolean;
39
+ /**
40
+ * The `Path` can have a trailing `/`, indicating that it represents a
41
+ * directory. (If there is no trailing slash, it can represent either a file
42
+ * or a directory.)
43
+ *
44
+ * Some operations will refuse to treat a directory path as a file path. This
45
+ * function identifies such paths.
46
+ */
47
+ isUnambiguousDirPath(): boolean;
28
48
  /**
29
49
  * Same as `.toString()`, but more concise.
30
50
  */
@@ -51,7 +71,6 @@ export declare class Path {
51
71
  rename(destination: string | URL | Path): Promise<void>;
52
72
  /** Create a temporary dir inside the global temp dir for the current user. */
53
73
  static makeTempDir(prefix?: string): Promise<Path>;
54
- trash(): Promise<void>;
55
74
  rm(options?: Parameters<typeof rm>[1]): Promise<void>;
56
75
  /**
57
76
  * Equivalent to:
@@ -12,30 +12,66 @@ import {
12
12
  } from "node:fs/promises";
13
13
  import { homedir, tmpdir } from "node:os";
14
14
  import { basename, dirname, extname, join } from "node:path";
15
- import { fileURLToPath } from "node:url";
16
- import { default as trash } from "trash";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
16
  import { xdgCache, xdgConfig, xdgData, xdgState } from "xdg-basedir";
18
17
  var Path = class _Path {
19
18
  // @ts-expect-error ts(2564): False positive. https://github.com/microsoft/TypeScript/issues/32194
20
19
  #path;
21
20
  constructor(path) {
21
+ this.#setNormalizedPath(_Path.#pathlikeToString(path));
22
+ }
23
+ /**
24
+ * Similar to `new URL(path, base)`, but accepting and returning `Path` objects.
25
+ * Note that `base` must be one of:
26
+ *
27
+ * - a `string` representing an absolute path,
28
+ * - a `URL` object, or
29
+ * - a `Path` representing an absolute path.
30
+ *
31
+ */
32
+ static resolve(path, base) {
33
+ const baseURL = (() => {
34
+ if (!(base instanceof _Path)) {
35
+ return base;
36
+ }
37
+ if (!base.isAbsolutePath()) {
38
+ throw new Error(
39
+ "The `base` arg to `Path.resolve(\u2026)` must be an absolute path."
40
+ );
41
+ }
42
+ return pathToFileURL(base.#path);
43
+ })();
44
+ return new _Path(new URL(_Path.#pathlikeToString(path), baseURL));
45
+ }
46
+ static #pathlikeToString(path) {
22
47
  if (path instanceof _Path) {
23
- this.#setNormalizedPath(path.#path);
24
- return;
48
+ return path.#path;
25
49
  }
26
50
  if (path instanceof URL) {
27
- this.#setNormalizedPath(fileURLToPath(path));
28
- return;
51
+ return fileURLToPath(path);
29
52
  }
30
53
  if (typeof path === "string") {
31
- this.#setNormalizedPath(path);
32
- return;
54
+ return path;
33
55
  }
34
56
  throw new Error("Invalid path");
35
57
  }
36
58
  #setNormalizedPath(path) {
37
59
  this.#path = join(path);
38
60
  }
61
+ isAbsolutePath() {
62
+ return this.#path.startsWith("/");
63
+ }
64
+ /**
65
+ * The `Path` can have a trailing `/`, indicating that it represents a
66
+ * directory. (If there is no trailing slash, it can represent either a file
67
+ * or a directory.)
68
+ *
69
+ * Some operations will refuse to treat a directory path as a file path. This
70
+ * function identifies such paths.
71
+ */
72
+ isUnambiguousDirPath() {
73
+ return this.#path.endsWith("/");
74
+ }
39
75
  /**
40
76
  * Same as `.toString()`, but more concise.
41
77
  */
@@ -82,7 +118,7 @@ var Path = class _Path {
82
118
  return this.extension;
83
119
  }
84
120
  #mustNotHaveTrailingSlash() {
85
- if (this.#path.endsWith("/")) {
121
+ if (this.isUnambiguousDirPath()) {
86
122
  throw new Error(
87
123
  "Path ends with a slash, which cannot be treated as a file."
88
124
  );
@@ -156,9 +192,6 @@ var Path = class _Path {
156
192
  await mkdtemp(new _Path(tmpdir()).join(prefix ?? "js-temp-").toString())
157
193
  );
158
194
  }
159
- async trash() {
160
- await trash(this.#path, { glob: false });
161
- }
162
195
  async rm(options) {
163
196
  await rm(this.#path, options);
164
197
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/index.ts"],
4
- "sourcesContent": ["import type { Dirent, ObjectEncodingOptions } from \"node:fs\";\nimport {\n cp,\n mkdir,\n mkdtemp,\n readdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { basename, dirname, extname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { default as trash } from \"trash\";\nimport { xdgCache, xdgConfig, xdgData, xdgState } from \"xdg-basedir\";\n\n// Modifying the type of `readdir(\u2026)` from `node:fs/promises` to remove the\n// first parameter is difficult, if not impossible. So we give up and duplicate\n// the types manually. This ensures ergonomic types, such as an inferred return\n// type of `string[]` when `options` is not passed.\n\ndeclare function readDirType(\n options?:\n | (ObjectEncodingOptions & {\n withFileTypes?: false | undefined;\n recursive?: boolean | undefined;\n })\n | BufferEncoding\n | null,\n): Promise<string[]>;\n\ndeclare function readDirType(\n options:\n | {\n encoding: \"buffer\";\n withFileTypes?: false | undefined;\n recursive?: boolean | undefined;\n }\n | \"buffer\",\n): Promise<Buffer[]>;\n\ndeclare function readDirType(\n options?:\n | (ObjectEncodingOptions & {\n withFileTypes?: false | undefined;\n recursive?: boolean | undefined;\n })\n | BufferEncoding\n | null,\n): Promise<string[] | Buffer[]>;\n\ndeclare function readDirType(\n options: ObjectEncodingOptions & {\n withFileTypes: true;\n recursive?: boolean | undefined;\n },\n): Promise<Dirent[]>;\n\ndeclare function readDirType(options: {\n encoding: \"buffer\";\n withFileTypes: true;\n recursive?: boolean | undefined;\n}): Promise<Dirent<Buffer>[]>;\n\nexport class Path {\n // @ts-expect-error ts(2564): False positive. https://github.com/microsoft/TypeScript/issues/32194\n #path: string;\n constructor(path: string | URL | Path) {\n if (path instanceof Path) {\n this.#setNormalizedPath(path.#path);\n return;\n }\n if (path instanceof URL) {\n this.#setNormalizedPath(fileURLToPath(path));\n return;\n }\n if (typeof path === \"string\") {\n this.#setNormalizedPath(path);\n return;\n }\n throw new Error(\"Invalid path\");\n }\n\n #setNormalizedPath(path: string): void {\n this.#path = join(path);\n }\n\n /**\n * Same as `.toString()`, but more concise.\n */\n get path() {\n return this.#path;\n }\n\n toString(): string {\n return this.#path;\n }\n\n /// Constructs a new path by appending the given path segments.\n // TODO: accept `Path` inputs?\n join(...segments: string[]): Path {\n return new Path(join(this.#path, ...segments));\n }\n\n extendBasename(suffix: string): Path {\n const joinedSuffix = join(suffix);\n if (joinedSuffix !== basename(joinedSuffix)) {\n throw new Error(\"Invalid suffix to extend file name.\");\n }\n // TODO: join basename and dirname instead?\n return new Path(this.#path + joinedSuffix);\n }\n\n get parent(): Path {\n return new Path(dirname(this.#path));\n }\n\n // Normally I'd stick with `node`'s name, but I think `.dirname` is a\n // particularly poor name. So we support `.dirname` for discovery but mark it\n // as deprecated, even if it will never be removed.\n /** @deprecated Alias for `.parent`. */\n get dirname(): Path {\n return this.parent;\n }\n\n get basename(): Path {\n return new Path(basename(this.#path));\n }\n\n get extension(): string {\n this.#mustNotHaveTrailingSlash();\n return extname(this.#path);\n }\n\n // Normally I'd stick with `node`'s name, but I think `.extname` is a\n // particularly poor name. So we support `.extname` for discovery but mark it\n // as deprecated, even if it will never be removed.\n /** @deprecated Alias for `.extension`. */\n get extname(): string {\n return this.extension;\n }\n\n #mustNotHaveTrailingSlash(): void {\n if (this.#path.endsWith(\"/\")) {\n throw new Error(\n \"Path ends with a slash, which cannot be treated as a file.\",\n );\n }\n }\n\n async exists(constraints?: {\n mustBe: \"file\" | \"directory\";\n }): Promise<boolean> {\n let stats: Awaited<ReturnType<typeof stat>>;\n try {\n stats = await stat(this.#path);\n // biome-ignore lint/suspicious/noExplicitAny: TypeScript limitation\n } catch (e: any) {\n if (e.code === \"ENOENT\") {\n return false;\n }\n throw e;\n }\n if (!constraints?.mustBe) {\n return true;\n }\n switch (constraints?.mustBe) {\n case \"file\": {\n this.#mustNotHaveTrailingSlash();\n if (stats.isFile()) {\n return true;\n }\n throw new Error(`Path exists but is not a file: ${this.#path}`);\n }\n case \"directory\": {\n if (stats.isDirectory()) {\n return true;\n }\n throw new Error(`Path exists but is not a directory: ${this.#path}`);\n }\n default: {\n throw new Error(\"Invalid path type constraint\");\n }\n }\n }\n\n async existsAsFile(): Promise<boolean> {\n return this.exists({ mustBe: \"file\" });\n }\n\n async existsAsDir(): Promise<boolean> {\n return this.exists({ mustBe: \"directory\" });\n }\n\n // I don't think `mkdir` is a great name, but it does match the\n // well-established canonical commandline name. So in this case we keep the\n // awkward abbreviation.\n /** Defaults to `recursive: true`. */\n async mkdir(options?: Parameters<typeof mkdir>[1]): Promise<Path> {\n const optionsObject = (() => {\n if (typeof options === \"string\" || typeof options === \"number\") {\n return { mode: options };\n }\n return options ?? {};\n })();\n await mkdir(this.#path, { recursive: true, ...optionsObject });\n return this;\n }\n\n // TODO: check idempotency semantics when the destination exists and is a folder.\n /** Returns the destination path. */\n async cp(\n destination: string | URL | Path,\n options?: Parameters<typeof cp>[2],\n ): Promise<Path> {\n await cp(this.#path, new Path(destination).#path, options);\n return new Path(destination);\n }\n\n // TODO: check idempotency semantics when the destination exists and is a folder.\n async rename(destination: string | URL | Path): Promise<void> {\n await rename(this.#path, new Path(destination).#path);\n }\n\n /** Create a temporary dir inside the global temp dir for the current user. */\n static async makeTempDir(prefix?: string): Promise<Path> {\n return new Path(\n await mkdtemp(new Path(tmpdir()).join(prefix ?? \"js-temp-\").toString()),\n );\n }\n\n async trash(): Promise<void> {\n await trash(this.#path, { glob: false });\n }\n\n async rm(options?: Parameters<typeof rm>[1]): Promise<void> {\n await rm(this.#path, options);\n }\n\n /**\n * Equivalent to:\n *\n * .rm({ recursive: true, force: true, ...(options ?? {}) })\n *\n */\n async rm_rf(options?: Parameters<typeof rm>[1]): Promise<void> {\n await this.rm({ recursive: true, force: true, ...(options ?? {}) });\n }\n\n async fileText(): Promise<string> {\n return readFile(this.#path, \"utf-8\");\n }\n\n async fileJSON<T>(): Promise<T> {\n return JSON.parse(await this.fileText());\n }\n\n /** Creates intermediate directories if they do not exist.\n *\n * Returns the original `Path` (for chaining).\n */\n async write(\n data: Parameters<typeof writeFile>[1],\n options?: Parameters<typeof writeFile>[2],\n ): Promise<Path> {\n await this.parent.mkdir();\n await writeFile(this.#path, data, options);\n return this;\n }\n\n /**\n * If only `data` is provided, this is equivalent to:\n *\n * .write(JSON.stringify(data, null, \" \"));\n *\n * `replacer` and `space` can also be specified, making this equivalent to:\n *\n * .write(JSON.stringify(data, replacer, space));\n *\n * Returns the original `Path` (for chaining).\n */\n async writeJSON<T>(\n data: T,\n replacer: Parameters<typeof JSON.stringify>[1] = null,\n space: Parameters<typeof JSON.stringify>[2] = \" \",\n ): Promise<Path> {\n await this.write(JSON.stringify(data, replacer, space));\n return this;\n }\n\n // Normally we'd add a `@deprecated` alias named `.readdir`, but that would\n // differ only by capitalization of a single non-leading character. This can\n // be a bit confusing, especially when autocompleting. So for this function in\n // particular we don't include an alias.\n readDir: typeof readDirType = (options) =>\n // biome-ignore lint/suspicious/noExplicitAny: Needed to wrangle the types.\n readdir(this.#path, options as any) as any;\n\n static get homedir(): Path {\n return new Path(homedir());\n }\n\n static xdg = {\n cache: new Path(xdgCache ?? Path.homedir.join(\".cache\")),\n config: new Path(xdgConfig ?? Path.homedir.join(\".config\")),\n data: new Path(xdgData ?? Path.homedir.join(\".local/share\")),\n state: new Path(xdgState ?? Path.homedir.join(\".local/state\")),\n };\n\n /** Chainable function to print the path. Prints the same as:\n *\n * if (args.length > 0) {\n * console.log(...args);\n * }\n * console.log(this.path);\n *\n */\n // biome-ignore lint/suspicious/noExplicitAny: This is the correct type, based on `console.log(\u2026)`.\n debugPrint(...args: any[]): Path {\n if (args.length > 0) {\n console.log(...args);\n }\n console.log(this.#path);\n return this;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Dirent, ObjectEncodingOptions } from \"node:fs\";\nimport {\n cp,\n mkdir,\n mkdtemp,\n readdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { basename, dirname, extname, join } from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { xdgCache, xdgConfig, xdgData, xdgState } from \"xdg-basedir\";\n\n// Modifying the type of `readdir(\u2026)` from `node:fs/promises` to remove the\n// first parameter is difficult, if not impossible. So we give up and duplicate\n// the types manually. This ensures ergonomic types, such as an inferred return\n// type of `string[]` when `options` is not passed.\n\ndeclare function readDirType(\n options?:\n | (ObjectEncodingOptions & {\n withFileTypes?: false | undefined;\n recursive?: boolean | undefined;\n })\n | BufferEncoding\n | null,\n): Promise<string[]>;\n\ndeclare function readDirType(\n options:\n | {\n encoding: \"buffer\";\n withFileTypes?: false | undefined;\n recursive?: boolean | undefined;\n }\n | \"buffer\",\n): Promise<Buffer[]>;\n\ndeclare function readDirType(\n options?:\n | (ObjectEncodingOptions & {\n withFileTypes?: false | undefined;\n recursive?: boolean | undefined;\n })\n | BufferEncoding\n | null,\n): Promise<string[] | Buffer[]>;\n\ndeclare function readDirType(\n options: ObjectEncodingOptions & {\n withFileTypes: true;\n recursive?: boolean | undefined;\n },\n): Promise<Dirent[]>;\n\ndeclare function readDirType(options: {\n encoding: \"buffer\";\n withFileTypes: true;\n recursive?: boolean | undefined;\n}): Promise<Dirent<Buffer>[]>;\n\nexport class Path {\n // @ts-expect-error ts(2564): False positive. https://github.com/microsoft/TypeScript/issues/32194\n #path: string;\n constructor(path: string | URL | Path) {\n this.#setNormalizedPath(Path.#pathlikeToString(path));\n }\n\n /**\n * Similar to `new URL(path, base)`, but accepting and returning `Path` objects.\n * Note that `base` must be one of:\n *\n * - a `string` representing an absolute path,\n * - a `URL` object, or\n * - a `Path` representing an absolute path.\n *\n */\n static resolve(path: string | URL | Path, base: string | URL | Path): Path {\n const baseURL = (() => {\n if (!(base instanceof Path)) {\n return base;\n }\n if (!base.isAbsolutePath()) {\n throw new Error(\n \"The `base` arg to `Path.resolve(\u2026)` must be an absolute path.\",\n );\n }\n return pathToFileURL(base.#path);\n })();\n return new Path(new URL(Path.#pathlikeToString(path), baseURL));\n }\n\n static #pathlikeToString(path: string | URL | Path): string {\n if (path instanceof Path) {\n return path.#path;\n }\n if (path instanceof URL) {\n return fileURLToPath(path);\n }\n if (typeof path === \"string\") {\n return path;\n }\n throw new Error(\"Invalid path\");\n }\n\n #setNormalizedPath(path: string): void {\n this.#path = join(path);\n }\n\n isAbsolutePath(): boolean {\n return this.#path.startsWith(\"/\");\n }\n\n /**\n * The `Path` can have a trailing `/`, indicating that it represents a\n * directory. (If there is no trailing slash, it can represent either a file\n * or a directory.)\n *\n * Some operations will refuse to treat a directory path as a file path. This\n * function identifies such paths.\n */\n isUnambiguousDirPath(): boolean {\n return this.#path.endsWith(\"/\");\n }\n\n /**\n * Same as `.toString()`, but more concise.\n */\n get path() {\n return this.#path;\n }\n\n toString(): string {\n return this.#path;\n }\n\n /// Constructs a new path by appending the given path segments.\n // TODO: accept `Path` inputs?\n join(...segments: string[]): Path {\n return new Path(join(this.#path, ...segments));\n }\n\n extendBasename(suffix: string): Path {\n const joinedSuffix = join(suffix);\n if (joinedSuffix !== basename(joinedSuffix)) {\n throw new Error(\"Invalid suffix to extend file name.\");\n }\n // TODO: join basename and dirname instead?\n return new Path(this.#path + joinedSuffix);\n }\n\n get parent(): Path {\n return new Path(dirname(this.#path));\n }\n\n // Normally I'd stick with `node`'s name, but I think `.dirname` is a\n // particularly poor name. So we support `.dirname` for discovery but mark it\n // as deprecated, even if it will never be removed.\n /** @deprecated Alias for `.parent`. */\n get dirname(): Path {\n return this.parent;\n }\n\n get basename(): Path {\n return new Path(basename(this.#path));\n }\n\n get extension(): string {\n this.#mustNotHaveTrailingSlash();\n return extname(this.#path);\n }\n\n // Normally I'd stick with `node`'s name, but I think `.extname` is a\n // particularly poor name. So we support `.extname` for discovery but mark it\n // as deprecated, even if it will never be removed.\n /** @deprecated Alias for `.extension`. */\n get extname(): string {\n return this.extension;\n }\n\n #mustNotHaveTrailingSlash(): void {\n if (this.isUnambiguousDirPath()) {\n throw new Error(\n \"Path ends with a slash, which cannot be treated as a file.\",\n );\n }\n }\n\n async exists(constraints?: {\n mustBe: \"file\" | \"directory\";\n }): Promise<boolean> {\n let stats: Awaited<ReturnType<typeof stat>>;\n try {\n stats = await stat(this.#path);\n // biome-ignore lint/suspicious/noExplicitAny: TypeScript limitation\n } catch (e: any) {\n if (e.code === \"ENOENT\") {\n return false;\n }\n throw e;\n }\n if (!constraints?.mustBe) {\n return true;\n }\n switch (constraints?.mustBe) {\n case \"file\": {\n this.#mustNotHaveTrailingSlash();\n if (stats.isFile()) {\n return true;\n }\n throw new Error(`Path exists but is not a file: ${this.#path}`);\n }\n case \"directory\": {\n if (stats.isDirectory()) {\n return true;\n }\n throw new Error(`Path exists but is not a directory: ${this.#path}`);\n }\n default: {\n throw new Error(\"Invalid path type constraint\");\n }\n }\n }\n\n async existsAsFile(): Promise<boolean> {\n return this.exists({ mustBe: \"file\" });\n }\n\n async existsAsDir(): Promise<boolean> {\n return this.exists({ mustBe: \"directory\" });\n }\n\n // I don't think `mkdir` is a great name, but it does match the\n // well-established canonical commandline name. So in this case we keep the\n // awkward abbreviation.\n /** Defaults to `recursive: true`. */\n async mkdir(options?: Parameters<typeof mkdir>[1]): Promise<Path> {\n const optionsObject = (() => {\n if (typeof options === \"string\" || typeof options === \"number\") {\n return { mode: options };\n }\n return options ?? {};\n })();\n await mkdir(this.#path, { recursive: true, ...optionsObject });\n return this;\n }\n\n // TODO: check idempotency semantics when the destination exists and is a folder.\n /** Returns the destination path. */\n async cp(\n destination: string | URL | Path,\n options?: Parameters<typeof cp>[2],\n ): Promise<Path> {\n await cp(this.#path, new Path(destination).#path, options);\n return new Path(destination);\n }\n\n // TODO: check idempotency semantics when the destination exists and is a folder.\n async rename(destination: string | URL | Path): Promise<void> {\n await rename(this.#path, new Path(destination).#path);\n }\n\n /** Create a temporary dir inside the global temp dir for the current user. */\n static async makeTempDir(prefix?: string): Promise<Path> {\n return new Path(\n await mkdtemp(new Path(tmpdir()).join(prefix ?? \"js-temp-\").toString()),\n );\n }\n\n async rm(options?: Parameters<typeof rm>[1]): Promise<void> {\n await rm(this.#path, options);\n }\n\n /**\n * Equivalent to:\n *\n * .rm({ recursive: true, force: true, ...(options ?? {}) })\n *\n */\n async rm_rf(options?: Parameters<typeof rm>[1]): Promise<void> {\n await this.rm({ recursive: true, force: true, ...(options ?? {}) });\n }\n\n async fileText(): Promise<string> {\n return readFile(this.#path, \"utf-8\");\n }\n\n async fileJSON<T>(): Promise<T> {\n return JSON.parse(await this.fileText());\n }\n\n /** Creates intermediate directories if they do not exist.\n *\n * Returns the original `Path` (for chaining).\n */\n async write(\n data: Parameters<typeof writeFile>[1],\n options?: Parameters<typeof writeFile>[2],\n ): Promise<Path> {\n await this.parent.mkdir();\n await writeFile(this.#path, data, options);\n return this;\n }\n\n /**\n * If only `data` is provided, this is equivalent to:\n *\n * .write(JSON.stringify(data, null, \" \"));\n *\n * `replacer` and `space` can also be specified, making this equivalent to:\n *\n * .write(JSON.stringify(data, replacer, space));\n *\n * Returns the original `Path` (for chaining).\n */\n async writeJSON<T>(\n data: T,\n replacer: Parameters<typeof JSON.stringify>[1] = null,\n space: Parameters<typeof JSON.stringify>[2] = \" \",\n ): Promise<Path> {\n await this.write(JSON.stringify(data, replacer, space));\n return this;\n }\n\n // Normally we'd add a `@deprecated` alias named `.readdir`, but that would\n // differ only by capitalization of a single non-leading character. This can\n // be a bit confusing, especially when autocompleting. So for this function in\n // particular we don't include an alias.\n readDir: typeof readDirType = (options) =>\n // biome-ignore lint/suspicious/noExplicitAny: Needed to wrangle the types.\n readdir(this.#path, options as any) as any;\n\n static get homedir(): Path {\n return new Path(homedir());\n }\n\n static xdg = {\n cache: new Path(xdgCache ?? Path.homedir.join(\".cache\")),\n config: new Path(xdgConfig ?? Path.homedir.join(\".config\")),\n data: new Path(xdgData ?? Path.homedir.join(\".local/share\")),\n state: new Path(xdgState ?? Path.homedir.join(\".local/state\")),\n };\n\n /** Chainable function to print the path. Prints the same as:\n *\n * if (args.length > 0) {\n * console.log(...args);\n * }\n * console.log(this.path);\n *\n */\n // biome-ignore lint/suspicious/noExplicitAny: This is the correct type, based on `console.log(\u2026)`.\n debugPrint(...args: any[]): Path {\n if (args.length > 0) {\n console.log(...args);\n }\n console.log(this.#path);\n return this;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "path-class",
3
- "version": "0.3.4",
3
+ "version": "0.4.1",
4
4
  "author": "Lucas Garron <code@garron.net>",
5
5
  "type": "module",
6
6
  "main": "./dist/lib/path-class/index.js",
@@ -15,12 +15,11 @@
15
15
  "@biomejs/biome": "^2.2.5",
16
16
  "@cubing/dev-config": "^0.3.6",
17
17
  "@types/bun": "^1.3.0",
18
- "esbuild": "^0.25.10"
18
+ "esbuild": "^0.25.10",
19
+ "typescript": "^5.9.3"
19
20
  },
20
21
  "dependencies": {
21
22
  "@types/node": "^24.7.1",
22
- "trash": "^10.0.0",
23
- "typescript": "^5.9.3",
24
23
  "xdg-basedir": "^5.1.0"
25
24
  },
26
25
  "files": [
package/src/index.test.ts CHANGED
@@ -12,6 +12,10 @@ test("constructor", async () => {
12
12
  expect(new Path("trailing/slash/").path).toEqual("trailing/slash/");
13
13
  });
14
14
 
15
+ test("Path.resolve(…)", async () => {
16
+ expect(Path.resolve("foo", new Path("/bar/baz")).path).toEqual("/bar/foo");
17
+ });
18
+
15
19
  test("normalize", async () => {
16
20
  expect(new Path("foo//bar").path).toEqual("foo/bar");
17
21
  expect(new Path("foo////bar").path).toEqual("foo/bar");
@@ -106,7 +110,7 @@ test(".existsAsDir()", async () => {
106
110
  );
107
111
  expect(await filePath.exists({ mustBe: "directory" })).toBe(true);
108
112
  expect(await filePath.existsAsDir()).toBe(true);
109
- await filePath.trash();
113
+ await filePath.rm_rf();
110
114
  expect(await filePath.exists()).toBe(false);
111
115
  expect(await filePath.exists({ mustBe: "file" })).toBe(false);
112
116
  expect(await filePath.exists({ mustBe: "directory" })).toBe(false);
@@ -153,13 +157,6 @@ test(".makeTempDir(…)", async () => {
153
157
  expect(tempDir2.basename.path).toStartWith("foo");
154
158
  });
155
159
 
156
- test("trash", async () => {
157
- const tempDir = await Path.makeTempDir();
158
- expect(await tempDir.exists()).toBe(true);
159
- await tempDir.trash();
160
- expect(await tempDir.exists()).toBe(false);
161
- });
162
-
163
160
  test("rm (file)", async () => {
164
161
  const file = (await Path.makeTempDir()).join("file.txt");
165
162
  await file.write("");
package/src/index.ts CHANGED
@@ -12,8 +12,7 @@ import {
12
12
  } from "node:fs/promises";
13
13
  import { homedir, tmpdir } from "node:os";
14
14
  import { basename, dirname, extname, join } from "node:path";
15
- import { fileURLToPath } from "node:url";
16
- import { default as trash } from "trash";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
16
  import { xdgCache, xdgConfig, xdgData, xdgState } from "xdg-basedir";
18
17
 
19
18
  // Modifying the type of `readdir(…)` from `node:fs/promises` to remove the
@@ -68,17 +67,42 @@ export class Path {
68
67
  // @ts-expect-error ts(2564): False positive. https://github.com/microsoft/TypeScript/issues/32194
69
68
  #path: string;
70
69
  constructor(path: string | URL | Path) {
70
+ this.#setNormalizedPath(Path.#pathlikeToString(path));
71
+ }
72
+
73
+ /**
74
+ * Similar to `new URL(path, base)`, but accepting and returning `Path` objects.
75
+ * Note that `base` must be one of:
76
+ *
77
+ * - a `string` representing an absolute path,
78
+ * - a `URL` object, or
79
+ * - a `Path` representing an absolute path.
80
+ *
81
+ */
82
+ static resolve(path: string | URL | Path, base: string | URL | Path): Path {
83
+ const baseURL = (() => {
84
+ if (!(base instanceof Path)) {
85
+ return base;
86
+ }
87
+ if (!base.isAbsolutePath()) {
88
+ throw new Error(
89
+ "The `base` arg to `Path.resolve(…)` must be an absolute path.",
90
+ );
91
+ }
92
+ return pathToFileURL(base.#path);
93
+ })();
94
+ return new Path(new URL(Path.#pathlikeToString(path), baseURL));
95
+ }
96
+
97
+ static #pathlikeToString(path: string | URL | Path): string {
71
98
  if (path instanceof Path) {
72
- this.#setNormalizedPath(path.#path);
73
- return;
99
+ return path.#path;
74
100
  }
75
101
  if (path instanceof URL) {
76
- this.#setNormalizedPath(fileURLToPath(path));
77
- return;
102
+ return fileURLToPath(path);
78
103
  }
79
104
  if (typeof path === "string") {
80
- this.#setNormalizedPath(path);
81
- return;
105
+ return path;
82
106
  }
83
107
  throw new Error("Invalid path");
84
108
  }
@@ -87,6 +111,22 @@ export class Path {
87
111
  this.#path = join(path);
88
112
  }
89
113
 
114
+ isAbsolutePath(): boolean {
115
+ return this.#path.startsWith("/");
116
+ }
117
+
118
+ /**
119
+ * The `Path` can have a trailing `/`, indicating that it represents a
120
+ * directory. (If there is no trailing slash, it can represent either a file
121
+ * or a directory.)
122
+ *
123
+ * Some operations will refuse to treat a directory path as a file path. This
124
+ * function identifies such paths.
125
+ */
126
+ isUnambiguousDirPath(): boolean {
127
+ return this.#path.endsWith("/");
128
+ }
129
+
90
130
  /**
91
131
  * Same as `.toString()`, but more concise.
92
132
  */
@@ -143,7 +183,7 @@ export class Path {
143
183
  }
144
184
 
145
185
  #mustNotHaveTrailingSlash(): void {
146
- if (this.#path.endsWith("/")) {
186
+ if (this.isUnambiguousDirPath()) {
147
187
  throw new Error(
148
188
  "Path ends with a slash, which cannot be treated as a file.",
149
189
  );
@@ -231,10 +271,6 @@ export class Path {
231
271
  );
232
272
  }
233
273
 
234
- async trash(): Promise<void> {
235
- await trash(this.#path, { glob: false });
236
- }
237
-
238
274
  async rm(options?: Parameters<typeof rm>[1]): Promise<void> {
239
275
  await rm(this.#path, options);
240
276
  }