@fgv/ts-json-base 5.1.0-0 → 5.1.0-2

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.
@@ -20,7 +20,7 @@
20
20
  * SOFTWARE.
21
21
  */
22
22
  import { captureResult, fail, succeed } from '@fgv/ts-utils';
23
- import { isMutableAccessors } from './fileTreeAccessors';
23
+ import { isMutableAccessors, isMutableFileItem } from './fileTreeAccessors';
24
24
  /**
25
25
  * Class representing a directory in a file tree.
26
26
  * @public
@@ -65,34 +65,98 @@ export class DirectoryItem {
65
65
  return this._hal.getChildren(this.absolutePath);
66
66
  }
67
67
  /**
68
- * {@inheritDoc FileTree.IFileTreeDirectoryItem.createChildFile}
68
+ * {@inheritDoc FileTree.IMutableFileTreeDirectoryItem.createChildFile}
69
69
  */
70
70
  createChildFile(name, contents) {
71
- if (!isMutableAccessors(this._hal)) {
71
+ const hal = this._hal;
72
+ if (!isMutableAccessors(hal)) {
73
+ /* c8 ignore next 2 - defensive: all current accessor implementations support mutation interface */
72
74
  return fail(`${this.absolutePath}: mutation not supported`);
73
75
  }
74
- const filePath = this._hal.joinPaths(this.absolutePath, name);
75
- return this._hal.saveFileContents(filePath, contents).onSuccess(() => this._hal.getItem(filePath).onSuccess((item) => {
76
+ const filePath = hal.joinPaths(this.absolutePath, name);
77
+ return hal.saveFileContents(filePath, contents).onSuccess(() => hal.getItem(filePath).onSuccess((item) => {
76
78
  /* c8 ignore next 3 - defensive: verifies accessor returned correct item type after save */
77
- if (item.type !== 'file') {
78
- return fail(`${filePath}: expected file but got ${item.type}`);
79
+ if (!isMutableFileItem(item)) {
80
+ return fail(`${filePath}: expected mutable file but got ${item.type}`);
79
81
  }
80
82
  return succeed(item);
81
83
  }));
82
84
  }
83
85
  /**
84
- * {@inheritDoc FileTree.IFileTreeDirectoryItem.createChildDirectory}
86
+ * {@inheritDoc FileTree.IMutableFileTreeDirectoryItem.createChildDirectory}
85
87
  */
86
88
  createChildDirectory(name) {
87
- if (!isMutableAccessors(this._hal)) {
89
+ const hal = this._hal;
90
+ if (!isMutableAccessors(hal)) {
91
+ /* c8 ignore next 2 - defensive: all current accessor implementations support mutation interface */
88
92
  return fail(`${this.absolutePath}: mutation not supported`);
89
93
  }
90
- /* c8 ignore next 3 - defensive: createDirectory should always exist if isMutableAccessors is true */
91
- if (this._hal.createDirectory === undefined) {
92
- return fail(`${this.absolutePath}: directory creation not supported`);
94
+ const dirPath = hal.joinPaths(this.absolutePath, name);
95
+ return hal.createDirectory(dirPath).onSuccess(() => DirectoryItem.create(dirPath, hal));
96
+ }
97
+ /**
98
+ * {@inheritDoc FileTree.IMutableFileTreeDirectoryItem.deleteChild}
99
+ */
100
+ deleteChild(name, options) {
101
+ const hal = this._hal;
102
+ if (!isMutableAccessors(hal)) {
103
+ /* c8 ignore next 2 - defensive: all current accessor implementations support mutation interface */
104
+ return fail(`${this.absolutePath}: mutation not supported`);
105
+ }
106
+ const childPath = hal.joinPaths(this.absolutePath, name);
107
+ return hal.getItem(childPath).onSuccess((item) => {
108
+ if (item.type === 'file') {
109
+ return hal.deleteFile(childPath);
110
+ }
111
+ // Directory child
112
+ if (options === null || options === void 0 ? void 0 : options.recursive) {
113
+ return this._deleteRecursive(childPath);
114
+ }
115
+ return hal.deleteDirectory(childPath);
116
+ });
117
+ }
118
+ /**
119
+ * {@inheritDoc FileTree.IMutableFileTreeDirectoryItem.delete}
120
+ */
121
+ delete() {
122
+ const hal = this._hal;
123
+ if (!isMutableAccessors(hal)) {
124
+ /* c8 ignore next 2 - defensive: all current accessor implementations support mutation interface */
125
+ return fail(`${this.absolutePath}: mutation not supported`);
126
+ }
127
+ return hal.deleteDirectory(this.absolutePath);
128
+ }
129
+ /**
130
+ * Recursively deletes all children of a directory and then the directory itself.
131
+ * @param dirPath - The absolute path of the directory to delete.
132
+ * @returns `Success` with `true` if the directory was deleted, or `Failure` with an error message.
133
+ * @internal
134
+ */
135
+ _deleteRecursive(dirPath) {
136
+ const hal = this._hal;
137
+ /* c8 ignore next 3 - defensive: caller already verified mutable */
138
+ if (!isMutableAccessors(hal)) {
139
+ return fail(`${dirPath}: mutation not supported`);
93
140
  }
94
- const dirPath = this._hal.joinPaths(this.absolutePath, name);
95
- return this._hal.createDirectory(dirPath).onSuccess(() => DirectoryItem.create(dirPath, this._hal));
141
+ return hal.getChildren(dirPath).onSuccess((children) => {
142
+ for (const child of children) {
143
+ if (child.type === 'file') {
144
+ const result = hal.deleteFile(child.absolutePath);
145
+ /* c8 ignore next 3 - defensive: error propagation during recursive delete */
146
+ if (result.isFailure()) {
147
+ return result;
148
+ }
149
+ }
150
+ else {
151
+ const result = this._deleteRecursive(child.absolutePath);
152
+ /* c8 ignore next 3 - defensive: error propagation during recursive delete */
153
+ if (result.isFailure()) {
154
+ return result;
155
+ }
156
+ }
157
+ }
158
+ return hal.deleteDirectory(dirPath);
159
+ });
96
160
  }
97
161
  }
98
162
  //# sourceMappingURL=directoryItem.js.map
@@ -126,6 +126,16 @@ export class FileItem {
126
126
  /* c8 ignore next 2 - defensive: all current accessor implementations support mutation interface */
127
127
  return fail(`${this.absolutePath}: mutation not supported`);
128
128
  }
129
+ /**
130
+ * {@inheritDoc FileTree.IFileTreeFileItem.delete}
131
+ */
132
+ delete() {
133
+ if (!isMutableAccessors(this._hal)) {
134
+ /* c8 ignore next 2 - defensive: all current accessor implementations support mutation interface */
135
+ return fail(`${this.absolutePath}: mutation not supported`);
136
+ }
137
+ return this._hal.deleteFile(this.absolutePath);
138
+ }
129
139
  /**
130
140
  * Default function to infer the content type of a file.
131
141
  * @param filePath - The path of the file.
@@ -19,6 +19,9 @@
19
19
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
20
  * SOFTWARE.
21
21
  */
22
+ // ============================================================================
23
+ // Type Guards
24
+ // ============================================================================
22
25
  /**
23
26
  * Type guard to check if accessors support mutation.
24
27
  * @param accessors - The accessors to check.
@@ -27,7 +30,11 @@
27
30
  */
28
31
  export function isMutableAccessors(accessors) {
29
32
  const mutable = accessors;
30
- return typeof mutable.fileIsMutable === 'function' && typeof mutable.saveFileContents === 'function';
33
+ return (typeof mutable.fileIsMutable === 'function' &&
34
+ typeof mutable.saveFileContents === 'function' &&
35
+ typeof mutable.deleteFile === 'function' &&
36
+ typeof mutable.createDirectory === 'function' &&
37
+ typeof mutable.deleteDirectory === 'function');
31
38
  }
32
39
  /**
33
40
  * Type guard to check if accessors support persistence.
@@ -43,4 +50,32 @@ export function isPersistentAccessors(accessors) {
43
50
  typeof persistent.isDirty === 'function' &&
44
51
  typeof persistent.getDirtyPaths === 'function');
45
52
  }
53
+ /**
54
+ * Type guard to check if a file item supports mutation.
55
+ * @param item - The file item to check.
56
+ * @returns `true` if the item implements {@link FileTree.IMutableFileTreeFileItem}.
57
+ * @public
58
+ */
59
+ export function isMutableFileItem(item) {
60
+ const mutable = item;
61
+ return (mutable.type === 'file' &&
62
+ typeof mutable.getIsMutable === 'function' &&
63
+ typeof mutable.setContents === 'function' &&
64
+ typeof mutable.setRawContents === 'function' &&
65
+ typeof mutable.delete === 'function');
66
+ }
67
+ /**
68
+ * Type guard to check if a directory item supports mutation.
69
+ * @param item - The directory item to check.
70
+ * @returns `true` if the item implements {@link FileTree.IMutableFileTreeDirectoryItem}.
71
+ * @public
72
+ */
73
+ export function isMutableDirectoryItem(item) {
74
+ const mutable = item;
75
+ return (mutable.type === 'directory' &&
76
+ typeof mutable.createChildFile === 'function' &&
77
+ typeof mutable.createChildDirectory === 'function' &&
78
+ typeof mutable.deleteChild === 'function' &&
79
+ typeof mutable.delete === 'function');
80
+ }
46
81
  //# sourceMappingURL=fileTreeAccessors.js.map
@@ -148,6 +148,7 @@ export class FsFileTreeAccessors {
148
148
  }
149
149
  }
150
150
  return succeedWithDetail(true, 'persistent');
151
+ /* c8 ignore next 3 - unreachable when running as root (CI), tested in mutableFsTree.test.ts */
151
152
  }
152
153
  catch (_a) {
153
154
  return failWithDetail(`${absolutePath}: permission denied`, 'permission-denied');
@@ -165,6 +166,22 @@ export class FsFileTreeAccessors {
165
166
  });
166
167
  });
167
168
  }
169
+ /**
170
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.deleteFile}
171
+ */
172
+ deleteFile(path) {
173
+ return this.fileIsMutable(path).asResult.onSuccess(() => {
174
+ const absolutePath = this.resolveAbsolutePath(path);
175
+ return captureResult(() => {
176
+ const stat = fs.statSync(absolutePath);
177
+ if (!stat.isFile()) {
178
+ throw new Error(`${absolutePath}: not a file`);
179
+ }
180
+ fs.unlinkSync(absolutePath);
181
+ return true;
182
+ });
183
+ });
184
+ }
168
185
  /**
169
186
  * {@inheritDoc FileTree.IMutableFileTreeAccessors.createDirectory}
170
187
  */
@@ -179,5 +196,22 @@ export class FsFileTreeAccessors {
179
196
  return absolutePath;
180
197
  });
181
198
  }
199
+ /**
200
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.deleteDirectory}
201
+ */
202
+ deleteDirectory(dirPath) {
203
+ return this.fileIsMutable(dirPath).asResult.onSuccess(() => {
204
+ const absolutePath = this.resolveAbsolutePath(dirPath);
205
+ return captureResult(() => {
206
+ const stat = fs.statSync(absolutePath);
207
+ if (!stat.isDirectory()) {
208
+ throw new Error(`${absolutePath}: not a directory`);
209
+ }
210
+ // fs.rmdirSync fails if directory is non-empty (desired behavior)
211
+ fs.rmdirSync(absolutePath);
212
+ return true;
213
+ });
214
+ });
215
+ }
182
216
  }
183
217
  //# sourceMappingURL=fsTree.js.map
@@ -92,6 +92,9 @@ class MutableInMemoryDirectory {
92
92
  }
93
93
  return this.addFile(name, contents, contentType);
94
94
  }
95
+ removeChild(name) {
96
+ return this._children.delete(name);
97
+ }
95
98
  }
96
99
  /**
97
100
  * Implementation of {@link FileTree.IMutableFileTreeAccessors} that uses an in-memory
@@ -320,6 +323,78 @@ export class InMemoryTreeAccessors {
320
323
  }
321
324
  return succeedWithDetail(true, 'transient');
322
325
  }
326
+ /**
327
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.deleteFile}
328
+ */
329
+ deleteFile(path) {
330
+ const absolutePath = this.resolveAbsolutePath(path);
331
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
332
+ if (parts.length === 0) {
333
+ return fail(`${absolutePath}: invalid file path`);
334
+ }
335
+ const fileName = parts.pop();
336
+ // Navigate to parent directory
337
+ let dir = this._mutableRoot;
338
+ for (const part of parts) {
339
+ const child = dir.children.get(part);
340
+ if (!child || !(child instanceof MutableInMemoryDirectory)) {
341
+ return fail(`${absolutePath}: parent directory not found`);
342
+ }
343
+ dir = child;
344
+ }
345
+ if (!dir.removeChild(fileName)) {
346
+ return fail(`${absolutePath}: file not found`);
347
+ }
348
+ // Also remove from the read layer's directory children and path index
349
+ const parentPath = parts.length === 0 ? '/' : '/' + parts.join('/');
350
+ const readParent = this._tree.byAbsolutePath.get(parentPath);
351
+ if (readParent instanceof InMemoryDirectory) {
352
+ readParent.removeChild(fileName);
353
+ }
354
+ this._tree.byAbsolutePath.delete(absolutePath);
355
+ this._mutableByPath.delete(absolutePath);
356
+ return succeed(true);
357
+ }
358
+ /**
359
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.deleteDirectory}
360
+ */
361
+ deleteDirectory(path) {
362
+ const absolutePath = this.resolveAbsolutePath(path);
363
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
364
+ if (parts.length === 0) {
365
+ return fail(`${absolutePath}: invalid directory path`);
366
+ }
367
+ const dirName = parts.pop();
368
+ // Navigate to parent directory
369
+ let parentDir = this._mutableRoot;
370
+ for (const part of parts) {
371
+ const child = parentDir.children.get(part);
372
+ if (!child || !(child instanceof MutableInMemoryDirectory)) {
373
+ return fail(`${absolutePath}: parent directory not found`);
374
+ }
375
+ parentDir = child;
376
+ }
377
+ // Verify target is a directory
378
+ const target = parentDir.children.get(dirName);
379
+ if (!target || !(target instanceof MutableInMemoryDirectory)) {
380
+ return fail(`${absolutePath}: not a directory`);
381
+ }
382
+ // Check non-empty
383
+ if (target.children.size > 0) {
384
+ return fail(`${absolutePath}: directory is not empty`);
385
+ }
386
+ parentDir.removeChild(dirName);
387
+ // Also remove from the read layer
388
+ /* c8 ignore next 1 - defensive: branch for top-level directory deletion */
389
+ const readParentPath = parts.length === 0 ? '/' : '/' + parts.join('/');
390
+ const readParent = this._tree.byAbsolutePath.get(readParentPath);
391
+ if (readParent instanceof InMemoryDirectory) {
392
+ readParent.removeChild(dirName);
393
+ }
394
+ this._tree.byAbsolutePath.delete(absolutePath);
395
+ this._mutableByPath.delete(absolutePath);
396
+ return succeed(true);
397
+ }
323
398
  /**
324
399
  * {@inheritDoc FileTree.IMutableFileTreeAccessors.saveFileContents}
325
400
  */
@@ -90,6 +90,14 @@ export class InMemoryDirectory {
90
90
  this._children.set(name, child);
91
91
  return succeed(child);
92
92
  }
93
+ /**
94
+ * Removes a child from the directory.
95
+ * @param name - The name of the child to remove.
96
+ * @returns `true` if the child was found and removed, `false` otherwise.
97
+ */
98
+ removeChild(name) {
99
+ return this._children.delete(name);
100
+ }
93
101
  /**
94
102
  * Gets the absolute path for a child of this directory with the supplied
95
103
  * name.