@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.
- package/dist/packlets/file-tree/directoryItem.js +78 -14
- package/dist/packlets/file-tree/fileItem.js +10 -0
- package/dist/packlets/file-tree/fileTreeAccessors.js +36 -1
- package/dist/packlets/file-tree/fsTree.js +34 -0
- package/dist/packlets/file-tree/in-memory/inMemoryTree.js +75 -0
- package/dist/packlets/file-tree/in-memory/treeBuilder.js +8 -0
- package/dist/ts-json-base.d.ts +190 -45
- package/lib/packlets/file-tree/directoryItem.d.ts +21 -6
- package/lib/packlets/file-tree/directoryItem.js +77 -13
- package/lib/packlets/file-tree/fileItem.d.ts +6 -2
- package/lib/packlets/file-tree/fileItem.js +10 -0
- package/lib/packlets/file-tree/fileTreeAccessors.d.ts +104 -10
- package/lib/packlets/file-tree/fileTreeAccessors.js +38 -1
- package/lib/packlets/file-tree/fsTree.d.ts +8 -0
- package/lib/packlets/file-tree/fsTree.js +34 -0
- package/lib/packlets/file-tree/in-memory/inMemoryTree.d.ts +8 -0
- package/lib/packlets/file-tree/in-memory/inMemoryTree.js +75 -0
- package/lib/packlets/file-tree/in-memory/treeBuilder.d.ts +6 -0
- package/lib/packlets/file-tree/in-memory/treeBuilder.js +8 -0
- package/package.json +18 -19
- package/dist/test/fixtures/file-tree/docs/api/reference.json +0 -1
|
@@ -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.
|
|
68
|
+
* {@inheritDoc FileTree.IMutableFileTreeDirectoryItem.createChildFile}
|
|
69
69
|
*/
|
|
70
70
|
createChildFile(name, contents) {
|
|
71
|
-
|
|
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 =
|
|
75
|
-
return
|
|
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
|
|
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.
|
|
86
|
+
* {@inheritDoc FileTree.IMutableFileTreeDirectoryItem.createChildDirectory}
|
|
85
87
|
*/
|
|
86
88
|
createChildDirectory(name) {
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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' &&
|
|
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.
|