pwd-fs 3.3.5 → 3.4.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.
@@ -1,8 +1,10 @@
1
- import type { PoweredFileSystem } from '../powered-file-system';
1
+ import type { CopyFilter, PoweredFileSystem } from '../powered-file-system';
2
2
  /**
3
3
  * Resolves source and destination paths before delegating recursive copy work.
4
4
  */
5
5
  export declare function copy<T extends boolean = false>(this: PoweredFileSystem, src: string, dest: string, options?: {
6
6
  sync?: T;
7
7
  umask?: number;
8
+ overwrite?: boolean;
9
+ filter?: CopyFilter;
8
10
  }): T extends true ? void : Promise<void>;
@@ -13,13 +13,14 @@ const recurse_io_sync_1 = require("../recurse-io-sync");
13
13
  function copy(src, dest, options) {
14
14
  src = node_path_1.default.resolve(this.pwd, src);
15
15
  dest = node_path_1.default.resolve(this.pwd, dest);
16
- const { sync = false, umask = 0o000 } = options ?? {};
16
+ const { sync = false, umask = 0o000, overwrite = false, filter } = options ?? {};
17
+ const copyOptions = { umask, overwrite, filter };
17
18
  if (sync) {
18
- (0, recurse_io_sync_1.copySync)(src, dest, umask);
19
+ (0, recurse_io_sync_1.copySync)(src, dest, copyOptions);
19
20
  return undefined;
20
21
  }
21
22
  return new Promise((resolve, reject) => {
22
- (0, recurse_io_1.copy)(src, dest, umask, (err) => {
23
+ (0, recurse_io_1.copy)(src, dest, copyOptions, (err) => {
23
24
  if (err) {
24
25
  return reject(err);
25
26
  }
@@ -50,6 +50,28 @@ const test_utils_1 = require("../test-utils");
50
50
  await index_1.pfs.copy(tmpDir, node_path_1.default.dirname(tmpDir));
51
51
  });
52
52
  });
53
+ (0, node_test_1.it)('Positive: Overwrite should replace an existing target file', async () => {
54
+ node_fs_1.default.writeFileSync(node_path_1.default.join(tmpDir, 'digest', 'tings.txt'), 'old');
55
+ await index_1.pfs.copy(node_path_1.default.join(tmpDir, 'tings.txt'), node_path_1.default.join(tmpDir, 'digest'), {
56
+ overwrite: true
57
+ });
58
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(tmpDir, 'digest', 'tings.txt'), 'utf8');
59
+ (0, node_assert_1.default)(content !== 'old');
60
+ });
61
+ (0, node_test_1.it)('Positive: Filter should skip matching entries', async () => {
62
+ const destRoot = (0, test_utils_1.createTmpDir)();
63
+ try {
64
+ await index_1.pfs.copy(tmpDir, destRoot, {
65
+ overwrite: true,
66
+ filter: (src) => node_path_1.default.basename(src) !== 'tings.txt'
67
+ });
68
+ (0, node_assert_1.default)(node_fs_1.default.existsSync(node_path_1.default.join(destRoot, node_path_1.default.basename(tmpDir), 'digest')));
69
+ (0, node_assert_1.default)(node_fs_1.default.existsSync(node_path_1.default.join(destRoot, node_path_1.default.basename(tmpDir), 'tings.txt')) === false);
70
+ }
71
+ finally {
72
+ (0, test_utils_1.restore)(destRoot);
73
+ }
74
+ });
53
75
  (0, node_test_1.it)('[sync] Positive: Copying a file', () => {
54
76
  index_1.pfs.copy(node_path_1.default.join(tmpDir, 'tings.txt'), node_path_1.default.join(tmpDir, 'digest'), {
55
77
  sync: true
@@ -79,4 +101,28 @@ const test_utils_1 = require("../test-utils");
79
101
  });
80
102
  });
81
103
  });
104
+ (0, node_test_1.it)('[sync] Positive: Overwrite should replace an existing target file', () => {
105
+ node_fs_1.default.writeFileSync(node_path_1.default.join(tmpDir, 'digest', 'tings.txt'), 'old');
106
+ index_1.pfs.copy(node_path_1.default.join(tmpDir, 'tings.txt'), node_path_1.default.join(tmpDir, 'digest'), {
107
+ sync: true,
108
+ overwrite: true
109
+ });
110
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(tmpDir, 'digest', 'tings.txt'), 'utf8');
111
+ (0, node_assert_1.default)(content !== 'old');
112
+ });
113
+ (0, node_test_1.it)('[sync] Positive: Filter should skip matching entries', () => {
114
+ const destRoot = (0, test_utils_1.createTmpDir)();
115
+ try {
116
+ index_1.pfs.copy(tmpDir, destRoot, {
117
+ sync: true,
118
+ overwrite: true,
119
+ filter: (src) => node_path_1.default.basename(src) !== 'tings.txt'
120
+ });
121
+ (0, node_assert_1.default)(node_fs_1.default.existsSync(node_path_1.default.join(destRoot, node_path_1.default.basename(tmpDir), 'digest')));
122
+ (0, node_assert_1.default)(node_fs_1.default.existsSync(node_path_1.default.join(destRoot, node_path_1.default.basename(tmpDir), 'tings.txt')) === false);
123
+ }
124
+ finally {
125
+ (0, test_utils_1.restore)(destRoot);
126
+ }
127
+ });
82
128
  });
@@ -0,0 +1,7 @@
1
+ import type { PoweredFileSystem } from '../powered-file-system';
2
+ /**
3
+ * Removes directory contents while preserving the directory itself.
4
+ */
5
+ export declare function emptyDir<T extends boolean = false>(this: PoweredFileSystem, src: string, options?: {
6
+ sync?: T;
7
+ }): T extends true ? void : Promise<void>;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.emptyDir = emptyDir;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const recurse_io_1 = require("../recurse-io");
9
+ const recurse_io_sync_1 = require("../recurse-io-sync");
10
+ /**
11
+ * Removes directory contents while preserving the directory itself.
12
+ */
13
+ function emptyDir(src, options) {
14
+ src = node_path_1.default.resolve(this.pwd, src);
15
+ const { sync = false } = options ?? {};
16
+ if (sync) {
17
+ (0, recurse_io_sync_1.emptyDirSync)(src);
18
+ return undefined;
19
+ }
20
+ return new Promise((resolve, reject) => {
21
+ (0, recurse_io_1.emptyDir)(src, (err) => {
22
+ if (err) {
23
+ return reject(err);
24
+ }
25
+ resolve();
26
+ });
27
+ });
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_assert_1 = __importDefault(require("node:assert"));
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const chance_1 = __importDefault(require("chance"));
10
+ const node_test_1 = require("node:test");
11
+ const index_1 = require("../index");
12
+ const test_utils_1 = require("../test-utils");
13
+ /**
14
+ * Verifies directory cleanup while preserving the directory itself.
15
+ */
16
+ (0, node_test_1.describe)('emptyDir(src [, options])', () => {
17
+ const chance = new chance_1.default();
18
+ let tmpDir = '';
19
+ (0, node_test_1.beforeEach)(() => {
20
+ tmpDir = (0, test_utils_1.createTmpDir)();
21
+ const frame = {
22
+ [node_path_1.default.join(tmpDir, 'tings.txt')]: {
23
+ type: 'file',
24
+ data: chance.string()
25
+ },
26
+ [node_path_1.default.join(tmpDir, 'digest')]: { type: 'directory' },
27
+ [node_path_1.default.join(tmpDir, 'digest', 'nested.txt')]: {
28
+ type: 'file',
29
+ data: chance.string()
30
+ }
31
+ };
32
+ (0, test_utils_1.fmock)(frame);
33
+ });
34
+ (0, node_test_1.afterEach)(() => {
35
+ (0, test_utils_1.restore)(tmpDir);
36
+ });
37
+ (0, node_test_1.it)('Positive: Removes all directory contents but preserves the directory', async () => {
38
+ await index_1.pfs.emptyDir(tmpDir);
39
+ (0, node_assert_1.default)(node_fs_1.default.existsSync(tmpDir));
40
+ node_assert_1.default.deepStrictEqual(node_fs_1.default.readdirSync(tmpDir), []);
41
+ });
42
+ (0, node_test_1.it)('Negative: Throw if resource is not directory', async () => {
43
+ await node_assert_1.default.rejects(async () => {
44
+ await index_1.pfs.emptyDir(node_path_1.default.join(tmpDir, 'tings.txt'));
45
+ });
46
+ });
47
+ (0, node_test_1.it)('[sync] Positive: Removes all directory contents but preserves the directory', () => {
48
+ index_1.pfs.emptyDir(tmpDir, {
49
+ sync: true
50
+ });
51
+ (0, node_assert_1.default)(node_fs_1.default.existsSync(tmpDir));
52
+ node_assert_1.default.deepStrictEqual(node_fs_1.default.readdirSync(tmpDir), []);
53
+ });
54
+ (0, node_test_1.it)('[sync] Negative: Throw if resource is not directory', () => {
55
+ node_assert_1.default.throws(() => {
56
+ index_1.pfs.emptyDir(node_path_1.default.join(tmpDir, 'tings.txt'), {
57
+ sync: true
58
+ });
59
+ });
60
+ });
61
+ });
@@ -0,0 +1,8 @@
1
+ import type { PoweredFileSystem } from '../powered-file-system';
2
+ /**
3
+ * Reads the target path stored in a symbolic link.
4
+ */
5
+ export declare function readlink<T extends boolean = false>(this: PoweredFileSystem, src: string, options?: {
6
+ sync?: T;
7
+ encoding?: BufferEncoding;
8
+ }): T extends true ? string : Promise<string>;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readlink = readlink;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ /**
10
+ * Reads the target path stored in a symbolic link.
11
+ */
12
+ function readlink(src, options) {
13
+ src = node_path_1.default.resolve(this.pwd, src);
14
+ const { sync = false, encoding = 'utf8' } = options ?? {};
15
+ if (sync) {
16
+ return node_fs_1.default.readlinkSync(src, { encoding });
17
+ }
18
+ return new Promise((resolve, reject) => {
19
+ node_fs_1.default.readlink(src, { encoding }, (err, resolved) => {
20
+ if (err) {
21
+ return reject(err);
22
+ }
23
+ resolve(resolved);
24
+ });
25
+ });
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_assert_1 = __importDefault(require("node:assert"));
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const chance_1 = __importDefault(require("chance"));
9
+ const node_test_1 = require("node:test");
10
+ const index_1 = require("../index");
11
+ const test_utils_1 = require("../test-utils");
12
+ /**
13
+ * Verifies symbolic link target resolution without dereferencing it.
14
+ */
15
+ (0, node_test_1.describe)('readlink(src [, options])', () => {
16
+ const chance = new chance_1.default();
17
+ let tmpDir = '';
18
+ (0, node_test_1.beforeEach)(() => {
19
+ tmpDir = (0, test_utils_1.createTmpDir)();
20
+ (0, test_utils_1.fmock)({
21
+ [node_path_1.default.join(tmpDir, 'tings.txt')]: {
22
+ type: 'file',
23
+ data: chance.string()
24
+ },
25
+ [node_path_1.default.join(tmpDir, 'flexapp')]: {
26
+ type: 'symlink',
27
+ target: node_path_1.default.join(tmpDir, 'tings.txt')
28
+ }
29
+ });
30
+ });
31
+ (0, node_test_1.afterEach)(() => {
32
+ (0, test_utils_1.restore)(tmpDir);
33
+ });
34
+ (0, node_test_1.it)('Positive: Reads the stored symlink target', async () => {
35
+ const target = await index_1.pfs.readlink(node_path_1.default.join(tmpDir, 'flexapp'));
36
+ (0, node_assert_1.default)(target === node_path_1.default.join(tmpDir, 'tings.txt'));
37
+ });
38
+ (0, node_test_1.it)('[sync] Positive: Reads the stored symlink target', () => {
39
+ const target = index_1.pfs.readlink(node_path_1.default.join(tmpDir, 'flexapp'), {
40
+ sync: true
41
+ });
42
+ (0, node_assert_1.default)(target === node_path_1.default.join(tmpDir, 'tings.txt'));
43
+ });
44
+ });
@@ -0,0 +1,8 @@
1
+ import type { PoweredFileSystem } from '../powered-file-system';
2
+ /**
3
+ * Resolves a path to its canonical absolute location.
4
+ */
5
+ export declare function realpath<T extends boolean = false>(this: PoweredFileSystem, src: string, options?: {
6
+ sync?: T;
7
+ encoding?: BufferEncoding;
8
+ }): T extends true ? string : Promise<string>;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.realpath = realpath;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ /**
10
+ * Resolves a path to its canonical absolute location.
11
+ */
12
+ function realpath(src, options) {
13
+ src = node_path_1.default.resolve(this.pwd, src);
14
+ const { sync = false, encoding = 'utf8' } = options ?? {};
15
+ if (sync) {
16
+ return node_fs_1.default.realpathSync(src, { encoding });
17
+ }
18
+ return new Promise((resolve, reject) => {
19
+ node_fs_1.default.realpath(src, { encoding }, (err, resolved) => {
20
+ if (err) {
21
+ return reject(err);
22
+ }
23
+ resolve(resolved);
24
+ });
25
+ });
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_assert_1 = __importDefault(require("node:assert"));
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const chance_1 = __importDefault(require("chance"));
9
+ const node_test_1 = require("node:test");
10
+ const index_1 = require("../index");
11
+ const test_utils_1 = require("../test-utils");
12
+ /**
13
+ * Verifies canonical path resolution through symbolic links.
14
+ */
15
+ (0, node_test_1.describe)('realpath(src [, options])', () => {
16
+ const chance = new chance_1.default();
17
+ let tmpDir = '';
18
+ (0, node_test_1.beforeEach)(() => {
19
+ tmpDir = (0, test_utils_1.createTmpDir)();
20
+ (0, test_utils_1.fmock)({
21
+ [node_path_1.default.join(tmpDir, 'tings.txt')]: {
22
+ type: 'file',
23
+ data: chance.string()
24
+ },
25
+ [node_path_1.default.join(tmpDir, 'flexapp')]: {
26
+ type: 'symlink',
27
+ target: node_path_1.default.join(tmpDir, 'tings.txt')
28
+ }
29
+ });
30
+ });
31
+ (0, node_test_1.afterEach)(() => {
32
+ (0, test_utils_1.restore)(tmpDir);
33
+ });
34
+ (0, node_test_1.it)('Positive: Resolves the canonical target path', async () => {
35
+ const target = await index_1.pfs.realpath(node_path_1.default.join(tmpDir, 'flexapp'));
36
+ (0, node_assert_1.default)(target === node_path_1.default.join(tmpDir, 'tings.txt'));
37
+ });
38
+ (0, node_test_1.it)('[sync] Positive: Resolves the canonical target path', () => {
39
+ const target = index_1.pfs.realpath(node_path_1.default.join(tmpDir, 'flexapp'), {
40
+ sync: true
41
+ });
42
+ (0, node_assert_1.default)(target === node_path_1.default.join(tmpDir, 'tings.txt'));
43
+ });
44
+ });
@@ -6,6 +6,7 @@ import { bitmask } from './bitmask';
6
6
  export type Mode = keyof IConstants;
7
7
  export type Flag = Extract<fs.OpenMode, string>;
8
8
  export type Stats = fs.Stats;
9
+ export type CopyFilter = (src: string, dest: string) => boolean;
9
10
  export * from './bitmask';
10
11
  export interface IConstants {
11
12
  e: number;
@@ -72,6 +73,8 @@ export declare class PoweredFileSystem {
72
73
  copy<T extends boolean = false>(src: string, dest: string, options?: {
73
74
  sync?: T;
74
75
  umask?: number;
76
+ overwrite?: boolean;
77
+ filter?: CopyFilter;
75
78
  }): T extends true ? void : Promise<void>;
76
79
  /**
77
80
  * Renames or moves a file system node.
@@ -85,6 +88,12 @@ export declare class PoweredFileSystem {
85
88
  remove<T extends boolean = false>(src: string, options?: {
86
89
  sync?: T;
87
90
  }): T extends true ? void : Promise<void>;
91
+ /**
92
+ * Removes all directory entries while preserving the directory itself.
93
+ */
94
+ emptyDir<T extends boolean = false>(src: string, options?: {
95
+ sync?: T;
96
+ }): T extends true ? void : Promise<void>;
88
97
  /**
89
98
  * Reads a file relative to the current instance root.
90
99
  */
@@ -117,6 +126,20 @@ export declare class PoweredFileSystem {
117
126
  sync?: T;
118
127
  encoding?: BufferEncoding | null;
119
128
  }): T extends true ? string[] : Promise<string[]>;
129
+ /**
130
+ * Resolves the target of a symbolic link.
131
+ */
132
+ readlink<T extends boolean = false>(src: string, options?: {
133
+ sync?: T;
134
+ encoding?: BufferEncoding;
135
+ }): T extends true ? string : Promise<string>;
136
+ /**
137
+ * Resolves a path to its canonical absolute location.
138
+ */
139
+ realpath<T extends boolean = false>(src: string, options?: {
140
+ sync?: T;
141
+ encoding?: BufferEncoding;
142
+ }): T extends true ? string : Promise<string>;
120
143
  /**
121
144
  * Creates a directory tree relative to the current instance root.
122
145
  */
@@ -25,9 +25,12 @@ const append_1 = require("./powered-file-system/append");
25
25
  const chmod_1 = require("./powered-file-system/chmod");
26
26
  const chown_1 = require("./powered-file-system/chown");
27
27
  const copy_1 = require("./powered-file-system/copy");
28
+ const empty_dir_1 = require("./powered-file-system/empty-dir");
28
29
  const mkdir_1 = require("./powered-file-system/mkdir");
29
30
  const read_1 = require("./powered-file-system/read");
31
+ const readlink_1 = require("./powered-file-system/readlink");
30
32
  const readdir_1 = require("./powered-file-system/readdir");
33
+ const realpath_1 = require("./powered-file-system/realpath");
31
34
  const remove_1 = require("./powered-file-system/remove");
32
35
  const rename_1 = require("./powered-file-system/rename");
33
36
  const stat_1 = require("./powered-file-system/stat");
@@ -110,6 +113,12 @@ class PoweredFileSystem {
110
113
  remove(src, options) {
111
114
  return remove_1.remove.call(this, src, options);
112
115
  }
116
+ /**
117
+ * Removes all directory entries while preserving the directory itself.
118
+ */
119
+ emptyDir(src, options) {
120
+ return empty_dir_1.emptyDir.call(this, src, options);
121
+ }
113
122
  /**
114
123
  * Reads a file relative to the current instance root.
115
124
  */
@@ -134,6 +143,18 @@ class PoweredFileSystem {
134
143
  readdir(dir, options) {
135
144
  return readdir_1.readdir.call(this, dir, options);
136
145
  }
146
+ /**
147
+ * Resolves the target of a symbolic link.
148
+ */
149
+ readlink(src, options) {
150
+ return readlink_1.readlink.call(this, src, options);
151
+ }
152
+ /**
153
+ * Resolves a path to its canonical absolute location.
154
+ */
155
+ realpath(src, options) {
156
+ return realpath_1.realpath.call(this, src, options);
157
+ }
137
158
  /**
138
159
  * Creates a directory tree relative to the current instance root.
139
160
  */
@@ -1,3 +1,4 @@
1
+ import type { ICopyOptions } from './recurse-io';
1
2
  /**
2
3
  * Synchronous counterpart of the recursive chmod implementation.
3
4
  */
@@ -9,11 +10,15 @@ export declare function chownSync(src: string, uid: number, gid: number): void;
9
10
  /**
10
11
  * Synchronously copies a file system node into the target directory.
11
12
  */
12
- export declare function copySync(src: string, dir: string, umask: number): void;
13
+ export declare function copySync(src: string, dir: string, options: ICopyOptions): void;
13
14
  /**
14
15
  * Synchronously removes files, directories, and symlinks without following links.
15
16
  */
16
17
  export declare function removeSync(src: string): void;
18
+ /**
19
+ * Synchronously removes all entries inside a directory while preserving it.
20
+ */
21
+ export declare function emptyDirSync(src: string): void;
17
22
  /**
18
23
  * Synchronously creates a directory tree using permissions derived from umask.
19
24
  */
@@ -7,6 +7,7 @@ exports.chmodSync = chmodSync;
7
7
  exports.chownSync = chownSync;
8
8
  exports.copySync = copySync;
9
9
  exports.removeSync = removeSync;
10
+ exports.emptyDirSync = emptyDirSync;
10
11
  exports.mkdirSync = mkdirSync;
11
12
  const node_fs_1 = __importDefault(require("node:fs"));
12
13
  const node_path_1 = __importDefault(require("node:path"));
@@ -45,23 +46,33 @@ function chownSync(src, uid, gid) {
45
46
  /**
46
47
  * Synchronously copies a file system node into the target directory.
47
48
  */
48
- function copySync(src, dir, umask) {
49
+ function copySync(src, dir, options) {
49
50
  const stat = node_fs_1.default.statSync(src);
51
+ const loc = node_path_1.default.basename(src);
52
+ const dest = node_path_1.default.join(dir, loc);
53
+ if (dest === src) {
54
+ throw new Error(`Source and destination are identical: ${src}`);
55
+ }
56
+ if (options.filter && options.filter(src, dest) === false) {
57
+ return;
58
+ }
50
59
  if (stat.isDirectory()) {
51
60
  const list = node_fs_1.default.readdirSync(src);
52
- const loc = node_path_1.default.basename(src);
53
- const mode = 0o777 & ~umask;
54
- dir = node_path_1.default.join(dir, loc);
55
- node_fs_1.default.mkdirSync(dir, { mode });
61
+ const mode = 0o777 & ~options.umask;
62
+ if (options.overwrite && node_fs_1.default.existsSync(dest)) {
63
+ removeSync(dest);
64
+ }
65
+ node_fs_1.default.mkdirSync(dest, { mode });
56
66
  for (const loc of list) {
57
- copySync(node_path_1.default.join(src, loc), dir, umask);
67
+ copySync(node_path_1.default.join(src, loc), dest, options);
58
68
  }
59
69
  }
60
70
  else {
61
- const loc = node_path_1.default.basename(src);
62
- const use = node_path_1.default.join(dir, loc);
63
- node_fs_1.default.copyFileSync(src, use);
64
- node_fs_1.default.chmodSync(use, 0o666 & ~umask);
71
+ if (options.overwrite && node_fs_1.default.existsSync(dest)) {
72
+ removeSync(dest);
73
+ }
74
+ node_fs_1.default.copyFileSync(src, dest);
75
+ node_fs_1.default.chmodSync(dest, 0o666 & ~options.umask);
65
76
  }
66
77
  }
67
78
  /**
@@ -84,6 +95,15 @@ function removeSync(src) {
84
95
  node_fs_1.default.unlinkSync(src);
85
96
  }
86
97
  }
98
+ /**
99
+ * Synchronously removes all entries inside a directory while preserving it.
100
+ */
101
+ function emptyDirSync(src) {
102
+ const list = node_fs_1.default.readdirSync(src);
103
+ for (const loc of list) {
104
+ removeSync(node_path_1.default.join(src, loc));
105
+ }
106
+ }
87
107
  /**
88
108
  * Synchronously creates a directory tree using permissions derived from umask.
89
109
  */
@@ -1,4 +1,9 @@
1
1
  import { NoParamCallback } from 'node:fs';
2
+ export interface ICopyOptions {
3
+ umask: number;
4
+ overwrite: boolean;
5
+ filter?: (src: string, dest: string) => boolean;
6
+ }
2
7
  /**
3
8
  * Applies chmod depth-first so directories are updated after their contents.
4
9
  */
@@ -10,11 +15,15 @@ export declare function chown(src: string, uid: number, gid: number, callback: N
10
15
  /**
11
16
  * Copies a file system node into the target directory, creating directories as needed.
12
17
  */
13
- export declare function copy(src: string, dir: string, umask: number, callback: NoParamCallback): void;
18
+ export declare function copy(src: string, dir: string, options: ICopyOptions, callback: NoParamCallback): void;
14
19
  /**
15
20
  * Removes files, directories, and symlinks without following symbolic links.
16
21
  */
17
22
  export declare function remove(src: string, callback: NoParamCallback): void;
23
+ /**
24
+ * Removes all entries inside a directory while preserving the directory itself.
25
+ */
26
+ export declare function emptyDir(src: string, callback: NoParamCallback): void;
18
27
  /**
19
28
  * Creates a directory tree with the permissions derived from the provided umask.
20
29
  */
@@ -7,6 +7,7 @@ exports.chmod = chmod;
7
7
  exports.chown = chown;
8
8
  exports.copy = copy;
9
9
  exports.remove = remove;
10
+ exports.emptyDir = emptyDir;
10
11
  exports.mkdir = mkdir;
11
12
  const node_fs_1 = __importDefault(require("node:fs"));
12
13
  const node_path_1 = __importDefault(require("node:path"));
@@ -88,55 +89,113 @@ function chown(src, uid, gid, callback) {
88
89
  /**
89
90
  * Copies a file system node into the target directory, creating directories as needed.
90
91
  */
91
- function copy(src, dir, umask, callback) {
92
+ function copy(src, dir, options, callback) {
92
93
  node_fs_1.default.stat(src, (err, stat) => {
93
94
  if (err) {
94
95
  return callback(err);
95
96
  }
97
+ const loc = node_path_1.default.basename(src);
98
+ const dest = node_path_1.default.join(dir, loc);
99
+ if (dest === src) {
100
+ return callback(new Error(`Source and destination are identical: ${src}`));
101
+ }
102
+ if (options.filter && options.filter(src, dest) === false) {
103
+ return callback(null);
104
+ }
96
105
  if (stat.isDirectory()) {
97
106
  node_fs_1.default.readdir(src, (err, list) => {
98
107
  if (err) {
99
108
  return callback(err);
100
109
  }
101
- const loc = node_path_1.default.basename(src);
102
- const destDir = node_path_1.default.join(dir, loc);
103
- const mode = 0o777 & ~umask;
104
- node_fs_1.default.mkdir(destDir, { mode }, (err) => {
110
+ const mode = 0o777 & ~options.umask;
111
+ const create = () => {
112
+ node_fs_1.default.mkdir(dest, { mode }, (err) => {
113
+ if (err) {
114
+ if (err.code === 'EEXIST') {
115
+ err = new Error(`Target already exists: ${dest}`);
116
+ }
117
+ return callback(err);
118
+ }
119
+ if (list.length === 0) {
120
+ return callback(null);
121
+ }
122
+ let reduce = list.length;
123
+ for (const item of list) {
124
+ copy(node_path_1.default.join(src, item), dest, options, (err) => {
125
+ if (err) {
126
+ return callback(err);
127
+ }
128
+ if (--reduce === 0) {
129
+ callback(null);
130
+ }
131
+ });
132
+ }
133
+ });
134
+ };
135
+ if (!options.overwrite) {
136
+ return create();
137
+ }
138
+ node_fs_1.default.lstat(dest, (err, destStat) => {
105
139
  if (err) {
106
- if (err.code === 'EEXIST') {
107
- err = new Error(`Target already exists: ${destDir}`);
140
+ if (err.code === 'ENOENT') {
141
+ return create();
108
142
  }
109
143
  return callback(err);
110
144
  }
111
- if (list.length === 0) {
112
- return callback(null);
113
- }
114
- let reduce = list.length;
115
- for (const item of list) {
116
- copy(node_path_1.default.join(src, item), destDir, umask, (err) => {
145
+ if (destStat.isDirectory()) {
146
+ return remove(dest, (err) => {
117
147
  if (err) {
118
148
  return callback(err);
119
149
  }
120
- if (--reduce === 0) {
121
- callback(null);
122
- }
150
+ create();
123
151
  });
124
152
  }
153
+ node_fs_1.default.unlink(dest, (err) => {
154
+ if (err) {
155
+ return callback(err);
156
+ }
157
+ create();
158
+ });
125
159
  });
126
160
  });
127
161
  }
128
162
  else {
129
- const loc = node_path_1.default.basename(src);
130
- const dest = node_path_1.default.join(dir, loc);
131
- const mode = 0o666 & ~umask;
132
- const readStream = node_fs_1.default.createReadStream(src);
133
- const writeStream = node_fs_1.default.createWriteStream(dest, { mode });
134
- readStream.on('error', callback);
135
- writeStream.on('error', callback);
136
- writeStream.on('close', () => {
137
- node_fs_1.default.chmod(dest, mode, callback);
163
+ const mode = 0o666 & ~options.umask;
164
+ const write = () => {
165
+ const readStream = node_fs_1.default.createReadStream(src);
166
+ const writeStream = node_fs_1.default.createWriteStream(dest, { mode });
167
+ readStream.on('error', callback);
168
+ writeStream.on('error', callback);
169
+ writeStream.on('close', () => {
170
+ node_fs_1.default.chmod(dest, mode, callback);
171
+ });
172
+ readStream.pipe(writeStream);
173
+ };
174
+ if (!options.overwrite) {
175
+ return write();
176
+ }
177
+ node_fs_1.default.lstat(dest, (err, destStat) => {
178
+ if (err) {
179
+ if (err.code === 'ENOENT') {
180
+ return write();
181
+ }
182
+ return callback(err);
183
+ }
184
+ if (destStat.isDirectory()) {
185
+ return remove(dest, (err) => {
186
+ if (err) {
187
+ return callback(err);
188
+ }
189
+ write();
190
+ });
191
+ }
192
+ node_fs_1.default.unlink(dest, (err) => {
193
+ if (err) {
194
+ return callback(err);
195
+ }
196
+ write();
197
+ });
138
198
  });
139
- readStream.pipe(writeStream);
140
199
  }
141
200
  });
142
201
  }
@@ -177,6 +236,30 @@ function remove(src, callback) {
177
236
  }
178
237
  });
179
238
  }
239
+ /**
240
+ * Removes all entries inside a directory while preserving the directory itself.
241
+ */
242
+ function emptyDir(src, callback) {
243
+ node_fs_1.default.readdir(src, (err, list) => {
244
+ if (err) {
245
+ return callback(err);
246
+ }
247
+ if (list.length === 0) {
248
+ return callback(null);
249
+ }
250
+ let reduce = list.length;
251
+ for (const loc of list) {
252
+ remove(node_path_1.default.join(src, loc), (err) => {
253
+ if (err) {
254
+ return callback(err);
255
+ }
256
+ if (--reduce === 0) {
257
+ callback(null);
258
+ }
259
+ });
260
+ }
261
+ });
262
+ }
180
263
  /**
181
264
  * Creates a directory tree with the permissions derived from the provided umask.
182
265
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pwd-fs",
3
- "version": "3.3.5",
3
+ "version": "3.4.0",
4
4
  "description": "Path-aware file system utilities with scoped working directories and recursive operations",
5
5
  "keywords": [
6
6
  "umask",
package/readme.md CHANGED
@@ -31,6 +31,42 @@ It is especially useful for:
31
31
  npm install pwd-fs
32
32
  ```
33
33
 
34
+ ## Table of Contents
35
+
36
+ - [Quick Start](#quick-start)
37
+ - [Common Recipes](#common-recipes)
38
+ - [Compatibility](#compatibility)
39
+ - [Exports](#exports)
40
+ - [API](#api)
41
+ - [`new PoweredFileSystem(pwd?)`](#new-poweredfilesystempwd)
42
+ - [`pfs.pwd`](#pfspwd)
43
+ - [`pfs.constants`](#pfsconstants)
44
+ - [`PoweredFileSystem.bitmask(mode)`](#poweredfilesystembitmaskmode)
45
+ - [`pfs.test(src, options?)`](#pfstestsrc-options)
46
+ - [`pfs.stat(src, options?)`](#pfsstatsrc-options)
47
+ - [`pfs.chmod(src, mode, options?)`](#pfschmodsrc-mode-options)
48
+ - [`pfs.chown(src, options?)`](#pfschownsrc-options)
49
+ - [`pfs.symlink(src, dest, options?)`](#pfssymlinksrc-dest-options)
50
+ - [`pfs.copy(src, dest, options?)`](#pfscopysrc-dest-options)
51
+ - [`pfs.rename(src, dest, options?)`](#pfsrenamesrc-dest-options)
52
+ - [`pfs.remove(src, options?)`](#pfsremovesrc-options)
53
+ - [`pfs.emptyDir(src, options?)`](#pfsemptydirsrc-options)
54
+ - [`pfs.read(src, options?)`](#pfsreadsrc-options)
55
+ - [`pfs.write(src, data, options?)`](#pfswritesrc-data-options)
56
+ - [`pfs.append(src, data, options?)`](#pfsappendsrc-data-options)
57
+ - [`pfs.readdir(dir, options?)`](#pfsreaddirdir-options)
58
+ - [`pfs.readlink(src, options?)`](#pfsreadlinksrc-options)
59
+ - [`pfs.realpath(src, options?)`](#pfsrealpathsrc-options)
60
+ - [`pfs.mkdir(dir, options?)`](#pfsmkdirdir-options)
61
+ - [Sync Mode](#sync-mode)
62
+ - [Error Behavior](#error-behavior)
63
+ - [Umask Behavior](#umask-behavior)
64
+ - [Notes](#notes)
65
+ - [Platform Caveats](#platform-caveats)
66
+ - [When To Use Native `fs`](#when-to-use-native-fs)
67
+ - [Development](#development)
68
+ - [License](#license)
69
+
34
70
  ## Quick Start
35
71
 
36
72
  ```ts
@@ -65,6 +101,19 @@ Result:
65
101
  - destination directory: `./dist`
66
102
  - created output: `./dist/assets`
67
103
 
104
+ ### Empty a directory but keep it
105
+
106
+ ```ts
107
+ await pfs.emptyDir('./cache');
108
+ ```
109
+
110
+ ### Resolve a symlink target
111
+
112
+ ```ts
113
+ const target = await pfs.readlink('./current');
114
+ const resolved = await pfs.realpath('./current');
115
+ ```
116
+
68
117
  ### Append to a file
69
118
 
70
119
  ```ts
@@ -232,7 +281,12 @@ Copies `src` into the destination directory.
232
281
  copy<T extends boolean = false>(
233
282
  src: string,
234
283
  dest: string,
235
- options?: { sync?: T; umask?: number }
284
+ options?: {
285
+ sync?: T;
286
+ umask?: number;
287
+ overwrite?: boolean;
288
+ filter?: (src: string, dest: string) => boolean;
289
+ }
236
290
  ): T extends true ? void : Promise<void>
237
291
  ```
238
292
 
@@ -241,6 +295,8 @@ Behavior:
241
295
  - copying a file creates `dest/<basename(src)>`
242
296
  - copying a directory creates `dest/<basename(src)>` recursively
243
297
  - the target must not already exist
298
+ - `overwrite: true` replaces an existing target entry with the same basename
299
+ - `filter()` can skip specific source entries during the copy
244
300
 
245
301
  ```ts
246
302
  await pfs.copy('./assets', './dist');
@@ -248,6 +304,13 @@ await pfs.copy('./assets', './dist');
248
304
 
249
305
  This creates `./dist/assets`, not a direct rename to `./dist`.
250
306
 
307
+ ```ts
308
+ await pfs.copy('./assets', './dist', {
309
+ overwrite: true,
310
+ filter: (src) => !src.endsWith('.map')
311
+ });
312
+ ```
313
+
251
314
  ### `pfs.rename(src, dest, options?)`
252
315
 
253
316
  Renames or moves a file system entry.
@@ -276,6 +339,21 @@ Behavior:
276
339
  - directories are removed recursively
277
340
  - symbolic links are unlinked without deleting the target
278
341
 
342
+ ### `pfs.emptyDir(src, options?)`
343
+
344
+ Removes all entries inside a directory while preserving the directory itself.
345
+
346
+ ```ts
347
+ emptyDir<T extends boolean = false>(
348
+ src: string,
349
+ options?: { sync?: T }
350
+ ): T extends true ? void : Promise<void>
351
+ ```
352
+
353
+ ```ts
354
+ await pfs.emptyDir('./tmp');
355
+ ```
356
+
279
357
  ### `pfs.read(src, options?)`
280
358
 
281
359
  Reads a file.
@@ -363,6 +441,28 @@ readdir<T extends boolean = false>(
363
441
 
364
442
  - default `encoding`: `'utf8'`
365
443
 
444
+ ### `pfs.readlink(src, options?)`
445
+
446
+ Reads the stored target path from a symbolic link.
447
+
448
+ ```ts
449
+ readlink<T extends boolean = false>(
450
+ src: string,
451
+ options?: { sync?: T; encoding?: BufferEncoding }
452
+ ): T extends true ? string : Promise<string>
453
+ ```
454
+
455
+ ### `pfs.realpath(src, options?)`
456
+
457
+ Resolves a path to its canonical absolute location.
458
+
459
+ ```ts
460
+ realpath<T extends boolean = false>(
461
+ src: string,
462
+ options?: { sync?: T; encoding?: BufferEncoding }
463
+ ): T extends true ? string : Promise<string>
464
+ ```
465
+
366
466
  ### `pfs.mkdir(dir, options?)`
367
467
 
368
468
  Creates a directory tree recursively.
@@ -400,11 +500,13 @@ Typical cases:
400
500
  - `test()` is the exception:
401
501
  it returns `false` for inaccessible or missing paths instead of rejecting or throwing
402
502
  - `read()`, `stat()`, `readdir()`, `chmod()`, `chown()`, `rename()`, and `remove()` fail for missing paths
503
+ - `readlink()` and `realpath()` fail for missing paths
403
504
  - `read()` fails when the target is a directory
404
505
  - `readdir()` fails when the target is not a directory
506
+ - `emptyDir()` fails when the target is not a directory
405
507
  - `write()` fails when the target path points to a directory
406
508
  - `copy()` fails when the source does not exist
407
- - `copy()` also fails when the destination already contains an entry with the same basename as the source
509
+ - `copy()` also fails when the destination already contains an entry with the same basename as the source, unless `overwrite: true` is used
408
510
  - `symlink()` fails when the destination already exists
409
511
  - `mkdir()` accepts an existing directory, but fails when a path segment is a file
410
512