@fgv/ts-json-base 5.0.2 → 5.1.0-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.
Files changed (43) hide show
  1. package/dist/packlets/converters/converters.js +36 -14
  2. package/dist/packlets/file-tree/directoryItem.js +99 -4
  3. package/dist/packlets/file-tree/fileItem.js +47 -9
  4. package/dist/packlets/file-tree/fileTreeAccessors.js +59 -1
  5. package/dist/packlets/file-tree/filterSpec.js +74 -0
  6. package/dist/packlets/file-tree/fsTree.js +107 -12
  7. package/dist/packlets/file-tree/in-memory/inMemoryTree.js +279 -21
  8. package/dist/packlets/file-tree/in-memory/treeBuilder.js +31 -0
  9. package/dist/packlets/file-tree/index.browser.js +1 -0
  10. package/dist/packlets/file-tree/index.js +1 -0
  11. package/dist/packlets/json-file/file.js +1 -1
  12. package/dist/packlets/json-file/jsonFsHelper.js +1 -1
  13. package/dist/packlets/validators/validators.js +8 -8
  14. package/dist/ts-json-base.d.ts +439 -65
  15. package/dist/tsdoc-metadata.json +1 -1
  16. package/lib/packlets/converters/converters.d.ts +20 -13
  17. package/lib/packlets/converters/converters.js +36 -13
  18. package/lib/packlets/file-tree/directoryItem.d.ts +29 -6
  19. package/lib/packlets/file-tree/directoryItem.js +98 -3
  20. package/lib/packlets/file-tree/fileItem.d.ts +31 -14
  21. package/lib/packlets/file-tree/fileItem.js +46 -8
  22. package/lib/packlets/file-tree/fileTreeAccessors.d.ts +237 -3
  23. package/lib/packlets/file-tree/fileTreeAccessors.js +63 -0
  24. package/lib/packlets/file-tree/filterSpec.d.ts +10 -0
  25. package/lib/packlets/file-tree/filterSpec.js +77 -0
  26. package/lib/packlets/file-tree/fsTree.d.ts +37 -13
  27. package/lib/packlets/file-tree/fsTree.js +106 -11
  28. package/lib/packlets/file-tree/in-memory/inMemoryTree.d.ts +37 -13
  29. package/lib/packlets/file-tree/in-memory/inMemoryTree.js +278 -20
  30. package/lib/packlets/file-tree/in-memory/treeBuilder.d.ts +15 -0
  31. package/lib/packlets/file-tree/in-memory/treeBuilder.js +31 -0
  32. package/lib/packlets/file-tree/index.browser.d.ts +1 -0
  33. package/lib/packlets/file-tree/index.browser.js +1 -0
  34. package/lib/packlets/file-tree/index.d.ts +1 -0
  35. package/lib/packlets/file-tree/index.js +1 -0
  36. package/lib/packlets/json-file/file.d.ts +1 -1
  37. package/lib/packlets/json-file/file.js +1 -1
  38. package/lib/packlets/json-file/jsonFsHelper.d.ts +1 -1
  39. package/lib/packlets/json-file/jsonFsHelper.js +1 -1
  40. package/lib/packlets/validators/validators.d.ts +9 -9
  41. package/lib/packlets/validators/validators.js +8 -8
  42. package/package.json +29 -30
  43. package/dist/test/fixtures/file-tree/docs/api/reference.json +0 -1
@@ -21,12 +21,13 @@
21
21
  */
22
22
  import path from 'path';
23
23
  import fs from 'fs';
24
- import { captureResult, succeed } from '@fgv/ts-utils';
24
+ import { captureResult, fail, failWithDetail, succeed, succeedWithDetail } from '@fgv/ts-utils';
25
25
  import { DirectoryItem } from './directoryItem';
26
26
  import { FileItem } from './fileItem';
27
+ import { isPathMutable } from './filterSpec';
27
28
  /**
28
- * Implementation of {@link FileTree.IFileTreeAccessors} that uses the
29
- * file system to access files and directories.
29
+ * Implementation of {@link FileTree.IMutableFileTreeAccessors} that uses the
30
+ * file system to access and modify files and directories.
30
31
  * @public
31
32
  */
32
33
  export class FsFileTreeAccessors {
@@ -36,12 +37,14 @@ export class FsFileTreeAccessors {
36
37
  * @public
37
38
  */
38
39
  constructor(params) {
39
- var _a;
40
+ var _a, _b;
40
41
  this.prefix = params === null || params === void 0 ? void 0 : params.prefix;
41
42
  this._inferContentType = (_a = params === null || params === void 0 ? void 0 : params.inferContentType) !== null && _a !== void 0 ? _a : FileItem.defaultInferContentType;
43
+ /* c8 ignore next 1 - defensive default when params is undefined */
44
+ this._mutable = (_b = params === null || params === void 0 ? void 0 : params.mutable) !== null && _b !== void 0 ? _b : false;
42
45
  }
43
46
  /**
44
- * {@inheritdoc FileTree.IFileTreeAccessors.resolveAbsolutePath}
47
+ * {@inheritDoc FileTree.IFileTreeAccessors.resolveAbsolutePath}
45
48
  */
46
49
  resolveAbsolutePath(...paths) {
47
50
  if (this.prefix && !path.isAbsolute(paths[0])) {
@@ -50,25 +53,25 @@ export class FsFileTreeAccessors {
50
53
  return path.resolve(...paths);
51
54
  }
52
55
  /**
53
- * {@inheritdoc FileTree.IFileTreeAccessors.getExtension}
56
+ * {@inheritDoc FileTree.IFileTreeAccessors.getExtension}
54
57
  */
55
58
  getExtension(itemPath) {
56
59
  return path.extname(itemPath);
57
60
  }
58
61
  /**
59
- * {@inheritdoc FileTree.IFileTreeAccessors.getBaseName}
62
+ * {@inheritDoc FileTree.IFileTreeAccessors.getBaseName}
60
63
  */
61
64
  getBaseName(itemPath, suffix) {
62
65
  return path.basename(itemPath, suffix);
63
66
  }
64
67
  /**
65
- * {@inheritdoc FileTree.IFileTreeAccessors.joinPaths}
68
+ * {@inheritDoc FileTree.IFileTreeAccessors.joinPaths}
66
69
  */
67
70
  joinPaths(...paths) {
68
71
  return path.join(...paths);
69
72
  }
70
73
  /**
71
- * {@inheritdoc FileTree.IFileTreeAccessors.getItem}
74
+ * {@inheritDoc FileTree.IFileTreeAccessors.getItem}
72
75
  */
73
76
  getItem(itemPath) {
74
77
  return captureResult(() => {
@@ -84,13 +87,13 @@ export class FsFileTreeAccessors {
84
87
  });
85
88
  }
86
89
  /**
87
- * {@inheritdoc FileTree.IFileTreeAccessors.getFileContents}
90
+ * {@inheritDoc FileTree.IFileTreeAccessors.getFileContents}
88
91
  */
89
92
  getFileContents(filePath) {
90
93
  return captureResult(() => fs.readFileSync(this.resolveAbsolutePath(filePath), 'utf8'));
91
94
  }
92
95
  /**
93
- * {@inheritdoc FileTree.IFileTreeAccessors.getFileContentType}
96
+ * {@inheritDoc FileTree.IFileTreeAccessors.getFileContentType}
94
97
  */
95
98
  getFileContentType(filePath, provided) {
96
99
  if (provided !== undefined) {
@@ -100,7 +103,7 @@ export class FsFileTreeAccessors {
100
103
  return this._inferContentType(filePath);
101
104
  }
102
105
  /**
103
- * {@inheritdoc FileTree.IFileTreeAccessors.getChildren}
106
+ * {@inheritDoc FileTree.IFileTreeAccessors.getChildren}
104
107
  */
105
108
  getChildren(dirPath) {
106
109
  return captureResult(() => {
@@ -118,5 +121,97 @@ export class FsFileTreeAccessors {
118
121
  return children;
119
122
  });
120
123
  }
124
+ /**
125
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.fileIsMutable}
126
+ */
127
+ fileIsMutable(path) {
128
+ const absolutePath = this.resolveAbsolutePath(path);
129
+ // Check if mutability is disabled
130
+ if (this._mutable === false) {
131
+ return failWithDetail(`${absolutePath}: mutability is disabled`, 'not-mutable');
132
+ }
133
+ // Check if path is excluded by filter
134
+ if (!isPathMutable(absolutePath, this._mutable)) {
135
+ return failWithDetail(`${absolutePath}: path is excluded by filter`, 'path-excluded');
136
+ }
137
+ // Check file system permissions
138
+ try {
139
+ // Check if file exists
140
+ if (fs.existsSync(absolutePath)) {
141
+ fs.accessSync(absolutePath, fs.constants.W_OK);
142
+ }
143
+ else {
144
+ // Check if parent directory is writable
145
+ const parentDir = absolutePath.substring(0, absolutePath.lastIndexOf('/'));
146
+ if (parentDir && fs.existsSync(parentDir)) {
147
+ fs.accessSync(parentDir, fs.constants.W_OK);
148
+ }
149
+ }
150
+ return succeedWithDetail(true, 'persistent');
151
+ /* c8 ignore next 3 - unreachable when running as root (CI), tested in mutableFsTree.test.ts */
152
+ }
153
+ catch (_a) {
154
+ return failWithDetail(`${absolutePath}: permission denied`, 'permission-denied');
155
+ }
156
+ }
157
+ /**
158
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.saveFileContents}
159
+ */
160
+ saveFileContents(path, contents) {
161
+ return this.fileIsMutable(path).asResult.onSuccess(() => {
162
+ const absolutePath = this.resolveAbsolutePath(path);
163
+ return captureResult(() => {
164
+ fs.writeFileSync(absolutePath, contents, 'utf8');
165
+ return contents;
166
+ });
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
+ }
185
+ /**
186
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.createDirectory}
187
+ */
188
+ createDirectory(dirPath) {
189
+ const absolutePath = this.resolveAbsolutePath(dirPath);
190
+ // Check if mutability is disabled
191
+ if (this._mutable === false) {
192
+ return fail(`${absolutePath}: mutability is disabled`);
193
+ }
194
+ return captureResult(() => {
195
+ fs.mkdirSync(absolutePath, { recursive: true });
196
+ return absolutePath;
197
+ });
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
+ }
121
216
  }
122
217
  //# sourceMappingURL=fsTree.js.map
@@ -19,13 +19,86 @@
19
19
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
20
  * SOFTWARE.
21
21
  */
22
- import { captureResult, fail, succeed } from '@fgv/ts-utils';
22
+ import { captureResult, fail, failWithDetail, succeed, succeedWithDetail } from '@fgv/ts-utils';
23
23
  import { DirectoryItem } from '../directoryItem';
24
24
  import { FileItem } from '../fileItem';
25
25
  import { InMemoryDirectory, InMemoryFile, TreeBuilder } from './treeBuilder';
26
+ import { isPathMutable } from '../filterSpec';
26
27
  /**
27
- * Implementation of {@link FileTree.IFileTreeAccessors} that uses an in-memory
28
- * tree to access files and directories.
28
+ * A mutable in-memory file that allows updating contents.
29
+ * @internal
30
+ */
31
+ class MutableInMemoryFile {
32
+ constructor(absolutePath, contents, contentType) {
33
+ this.absolutePath = absolutePath;
34
+ this._contents = contents;
35
+ this.contentType = contentType;
36
+ }
37
+ get contents() {
38
+ return this._contents;
39
+ }
40
+ setContents(contents) {
41
+ this._contents = contents;
42
+ }
43
+ }
44
+ /**
45
+ * A mutable in-memory directory that creates mutable files.
46
+ * @internal
47
+ */
48
+ class MutableInMemoryDirectory {
49
+ /* c8 ignore next 3 - internal getter used by tree traversal */
50
+ get children() {
51
+ return this._children;
52
+ }
53
+ constructor(absolutePath) {
54
+ this.absolutePath = absolutePath;
55
+ this._children = new Map();
56
+ }
57
+ getChildPath(name) {
58
+ if (this.absolutePath === '/') {
59
+ return `/${name}`;
60
+ }
61
+ return [this.absolutePath, name].join('/');
62
+ }
63
+ addFile(name, contents, contentType) {
64
+ /* c8 ignore next 3 - defensive: duplicate detection during construction */
65
+ if (this._children.has(name)) {
66
+ return fail(`${name}: already exists`);
67
+ }
68
+ const child = new MutableInMemoryFile(this.getChildPath(name), contents, contentType);
69
+ this._children.set(name, child);
70
+ return succeed(child);
71
+ }
72
+ getOrAddDirectory(name) {
73
+ const existing = this._children.get(name);
74
+ if (existing) {
75
+ if (existing instanceof MutableInMemoryDirectory) {
76
+ return succeed(existing);
77
+ }
78
+ return fail(`${name}: not a directory`);
79
+ }
80
+ const child = new MutableInMemoryDirectory(this.getChildPath(name));
81
+ this._children.set(name, child);
82
+ return succeed(child);
83
+ }
84
+ updateOrAddFile(name, contents, contentType) {
85
+ const existing = this._children.get(name);
86
+ if (existing) {
87
+ if (existing instanceof MutableInMemoryFile) {
88
+ existing.setContents(contents);
89
+ return succeed(existing);
90
+ }
91
+ return fail(`${name}: not a file`);
92
+ }
93
+ return this.addFile(name, contents, contentType);
94
+ }
95
+ removeChild(name) {
96
+ return this._children.delete(name);
97
+ }
98
+ }
99
+ /**
100
+ * Implementation of {@link FileTree.IMutableFileTreeAccessors} that uses an in-memory
101
+ * tree to access and modify files and directories.
29
102
  * @public
30
103
  */
31
104
  export class InMemoryTreeAccessors {
@@ -36,12 +109,18 @@ export class InMemoryTreeAccessors {
36
109
  * @public
37
110
  */
38
111
  constructor(files, params) {
39
- var _a, _b;
112
+ var _a, _b, _c, _d;
40
113
  this._tree = TreeBuilder.create(params === null || params === void 0 ? void 0 : params.prefix).orThrow();
41
114
  this._inferContentType = (_a = params === null || params === void 0 ? void 0 : params.inferContentType) !== null && _a !== void 0 ? _a : FileItem.defaultInferContentType;
115
+ this._mutable = (_b = params === null || params === void 0 ? void 0 : params.mutable) !== null && _b !== void 0 ? _b : false;
116
+ this._mutableByPath = new Map();
117
+ const prefix = (_c = params === null || params === void 0 ? void 0 : params.prefix) !== null && _c !== void 0 ? _c : '/';
118
+ this._mutableRoot = new MutableInMemoryDirectory(prefix.endsWith('/') ? prefix.slice(0, -1) || '/' : prefix);
119
+ this._mutableByPath.set(this._mutableRoot.absolutePath, this._mutableRoot);
42
120
  for (const file of files) {
43
- const contentType = (_b = file.contentType) !== null && _b !== void 0 ? _b : this._inferContentType(file.path).orDefault();
121
+ const contentType = (_d = file.contentType) !== null && _d !== void 0 ? _d : this._inferContentType(file.path).orDefault();
44
122
  this._tree.addFile(file.path, file.contents, contentType).orThrow();
123
+ this._addMutableFile(file.path, file.contents, contentType);
45
124
  }
46
125
  }
47
126
  /**
@@ -56,7 +135,7 @@ export class InMemoryTreeAccessors {
56
135
  return captureResult(() => new InMemoryTreeAccessors(files, params));
57
136
  }
58
137
  /**
59
- * {@inheritdoc FileTree.IFileTreeAccessors.resolveAbsolutePath}
138
+ * {@inheritDoc FileTree.IFileTreeAccessors.resolveAbsolutePath}
60
139
  */
61
140
  resolveAbsolutePath(...paths) {
62
141
  const parts = paths[0].startsWith('/') ? paths : [this._tree.prefix, ...paths];
@@ -64,7 +143,7 @@ export class InMemoryTreeAccessors {
64
143
  return `/${joined}`;
65
144
  }
66
145
  /**
67
- * {@inheritdoc FileTree.IFileTreeAccessors.getExtension}
146
+ * {@inheritDoc FileTree.IFileTreeAccessors.getExtension}
68
147
  */
69
148
  getExtension(path) {
70
149
  const parts = path.split('.');
@@ -74,7 +153,7 @@ export class InMemoryTreeAccessors {
74
153
  return `.${parts.pop()}`;
75
154
  }
76
155
  /**
77
- * {@inheritdoc FileTree.IFileTreeAccessors.getBaseName}
156
+ * {@inheritDoc FileTree.IFileTreeAccessors.getBaseName}
78
157
  */
79
158
  getBaseName(path, suffix) {
80
159
  var _a;
@@ -86,13 +165,15 @@ export class InMemoryTreeAccessors {
86
165
  return base;
87
166
  }
88
167
  /**
89
- * {@inheritdoc FileTree.IFileTreeAccessors.joinPaths}
168
+ * {@inheritDoc FileTree.IFileTreeAccessors.joinPaths}
90
169
  */
91
170
  joinPaths(...paths) {
92
- return paths.join('/');
171
+ var _a;
172
+ const joined = paths.flatMap((p) => p.split('/').filter((s) => s.length > 0)).join('/');
173
+ return ((_a = paths[0]) === null || _a === void 0 ? void 0 : _a.startsWith('/')) ? `/${joined}` : joined;
93
174
  }
94
175
  /**
95
- * {@inheritdoc FileTree.IFileTreeAccessors.getItem}
176
+ * {@inheritDoc FileTree.IFileTreeAccessors.getItem}
96
177
  */
97
178
  getItem(itemPath) {
98
179
  const existing = this._tree.byAbsolutePath.get(itemPath);
@@ -107,26 +188,24 @@ export class InMemoryTreeAccessors {
107
188
  return fail(`${itemPath}: not found`);
108
189
  }
109
190
  /**
110
- * {@inheritdoc FileTree.IFileTreeAccessors.getFileContents}
191
+ * {@inheritDoc FileTree.IFileTreeAccessors.getFileContents}
111
192
  */
112
193
  getFileContents(path) {
113
- const item = this._tree.byAbsolutePath.get(path);
194
+ const absolutePath = this.resolveAbsolutePath(path);
195
+ const item = this._mutableByPath.get(absolutePath);
114
196
  if (item === undefined) {
115
- return fail(`${path}: not found`);
197
+ return fail(`${absolutePath}: not found`);
116
198
  }
117
- /* c8 ignore next 3 - local coverage is 100% but build coverage has intermittent issues */
118
- if (!(item instanceof InMemoryFile)) {
119
- return fail(`${path}: not a file`);
199
+ if (!(item instanceof MutableInMemoryFile)) {
200
+ return fail(`${absolutePath}: not a file`);
120
201
  }
121
- // if the body is a string we don't want to add quotes
122
202
  if (typeof item.contents === 'string') {
123
203
  return succeed(item.contents);
124
204
  }
125
- /* c8 ignore next 2 - local coverage is 100% but build coverage has intermittent issues */
126
205
  return captureResult(() => JSON.stringify(item.contents));
127
206
  }
128
207
  /**
129
- * {@inheritdoc FileTree.IFileTreeAccessors.getFileContentType}
208
+ * {@inheritDoc FileTree.IFileTreeAccessors.getFileContentType}
130
209
  */
131
210
  getFileContentType(path, provided) {
132
211
  // If provided contentType is given, use it directly (highest priority)
@@ -149,7 +228,7 @@ export class InMemoryTreeAccessors {
149
228
  return this._inferContentType(path);
150
229
  }
151
230
  /**
152
- * {@inheritdoc FileTree.IFileTreeAccessors.getChildren}
231
+ * {@inheritDoc FileTree.IFileTreeAccessors.getChildren}
153
232
  */
154
233
  getChildren(path) {
155
234
  const item = this._tree.byAbsolutePath.get(path);
@@ -173,5 +252,184 @@ export class InMemoryTreeAccessors {
173
252
  return children;
174
253
  });
175
254
  }
255
+ _addMutableFile(path, contents, contentType) {
256
+ const absolutePath = this.resolveAbsolutePath(path);
257
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
258
+ /* c8 ignore next 3 - defensive: invalid path detection */
259
+ if (parts.length === 0) {
260
+ return fail(`${absolutePath}: invalid file path`);
261
+ }
262
+ let dir = this._mutableRoot;
263
+ while (parts.length > 1) {
264
+ const part = parts.shift();
265
+ const result = dir.getOrAddDirectory(part);
266
+ /* c8 ignore next 3 - defensive: directory conflict during construction */
267
+ if (result.isFailure()) {
268
+ return fail(result.message);
269
+ }
270
+ dir = result.value;
271
+ if (!this._mutableByPath.has(dir.absolutePath)) {
272
+ this._mutableByPath.set(dir.absolutePath, dir);
273
+ }
274
+ }
275
+ return dir.addFile(parts[0], contents, contentType).onSuccess((file) => {
276
+ this._mutableByPath.set(file.absolutePath, file);
277
+ return succeed(file);
278
+ });
279
+ }
280
+ /**
281
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.createDirectory}
282
+ */
283
+ createDirectory(dirPath) {
284
+ const absolutePath = this.resolveAbsolutePath(dirPath);
285
+ // Check if mutability is disabled
286
+ if (this._mutable === false) {
287
+ return fail(`${absolutePath}: mutability is disabled`);
288
+ }
289
+ // Add to the TreeBuilder (read layer)
290
+ const treeResult = this._tree.addDirectory(absolutePath);
291
+ /* c8 ignore next 3 - defensive: read layer failure would indicate internal inconsistency */
292
+ if (treeResult.isFailure()) {
293
+ return fail(treeResult.message);
294
+ }
295
+ // Add to the mutable layer
296
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
297
+ let dir = this._mutableRoot;
298
+ for (const part of parts) {
299
+ const result = dir.getOrAddDirectory(part);
300
+ /* c8 ignore next 3 - defensive: mutable layer should match read layer state */
301
+ if (result.isFailure()) {
302
+ return fail(result.message);
303
+ }
304
+ dir = result.value;
305
+ if (!this._mutableByPath.has(dir.absolutePath)) {
306
+ this._mutableByPath.set(dir.absolutePath, dir);
307
+ }
308
+ }
309
+ return succeed(absolutePath);
310
+ }
311
+ /**
312
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.fileIsMutable}
313
+ */
314
+ fileIsMutable(path) {
315
+ const absolutePath = this.resolveAbsolutePath(path);
316
+ // Check if mutability is disabled
317
+ if (this._mutable === false) {
318
+ return failWithDetail(`${absolutePath}: mutability is disabled`, 'not-mutable');
319
+ }
320
+ // Check if path is excluded by filter
321
+ if (!isPathMutable(absolutePath, this._mutable)) {
322
+ return failWithDetail(`${absolutePath}: path is excluded by filter`, 'path-excluded');
323
+ }
324
+ return succeedWithDetail(true, 'transient');
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
+ }
398
+ /**
399
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.saveFileContents}
400
+ */
401
+ saveFileContents(path, contents) {
402
+ const isMutable = this.fileIsMutable(path);
403
+ if (isMutable.isFailure()) {
404
+ return fail(isMutable.message);
405
+ }
406
+ const absolutePath = this.resolveAbsolutePath(path);
407
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
408
+ if (parts.length === 0) {
409
+ return fail(`${absolutePath}: invalid file path`);
410
+ }
411
+ // Navigate to parent directory, creating directories as needed
412
+ let dir = this._mutableRoot;
413
+ while (parts.length > 1) {
414
+ const part = parts.shift();
415
+ const result = dir.getOrAddDirectory(part);
416
+ if (result.isFailure()) {
417
+ return fail(result.message);
418
+ }
419
+ dir = result.value;
420
+ if (!this._mutableByPath.has(dir.absolutePath)) {
421
+ this._mutableByPath.set(dir.absolutePath, dir);
422
+ }
423
+ }
424
+ // Update or add the file in the mutable layer
425
+ return dir.updateOrAddFile(parts[0], contents).onSuccess((file) => {
426
+ this._mutableByPath.set(file.absolutePath, file);
427
+ // Also register in the read layer so getItem/getChildren can find it
428
+ if (!this._tree.byAbsolutePath.has(file.absolutePath)) {
429
+ this._tree.addFile(file.absolutePath, contents);
430
+ }
431
+ return succeed(contents);
432
+ });
433
+ }
176
434
  }
177
435
  //# sourceMappingURL=inMemoryTree.js.map
@@ -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.
@@ -169,5 +177,28 @@ export class TreeBuilder {
169
177
  return succeed(file);
170
178
  });
171
179
  }
180
+ /**
181
+ * Ensures a directory exists at the given absolute path, creating
182
+ * intermediate directories as needed.
183
+ * @param absolutePath - The absolute path of the directory.
184
+ * @returns `Success` with the directory if successful, or
185
+ * `Failure` with an error message otherwise.
186
+ * @public
187
+ */
188
+ addDirectory(absolutePath) {
189
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
190
+ let dir = this.root;
191
+ for (const part of parts) {
192
+ const result = dir.getOrAddDirectory(part);
193
+ if (result.isFailure()) {
194
+ return fail(result.message);
195
+ }
196
+ dir = result.value;
197
+ if (!this.byAbsolutePath.has(dir.absolutePath)) {
198
+ this.byAbsolutePath.set(dir.absolutePath, dir);
199
+ }
200
+ }
201
+ return succeed(dir);
202
+ }
172
203
  }
173
204
  //# sourceMappingURL=treeBuilder.js.map
@@ -25,6 +25,7 @@ export * from './fileTreeAccessors';
25
25
  export * from './fileTree';
26
26
  export * from './directoryItem';
27
27
  export * from './fileItem';
28
+ export * from './filterSpec';
28
29
  // Export in-memory implementations for web compatibility
29
30
  export * from './in-memory';
30
31
  export { inMemory } from './fileTreeHelpers.inMemory';
@@ -24,6 +24,7 @@ export * from './fileTreeAccessors';
24
24
  export * from './fileTree';
25
25
  export * from './directoryItem';
26
26
  export * from './fileItem';
27
+ export * from './filterSpec';
27
28
  // Export tree-shakeable helpers (filesystem ones will be shaken out if not used)
28
29
  export * from './fileTreeHelpers';
29
30
  export { inMemory } from './fileTreeHelpers.inMemory';
@@ -22,7 +22,7 @@
22
22
  import { DefaultJsonFsHelper, JsonFsHelper } from './jsonFsHelper';
23
23
  import { DefaultJsonLike } from './jsonLike';
24
24
  /**
25
- * {@inheritdoc JsonFile.JsonFsHelper.readJsonFileSync}
25
+ * {@inheritDoc JsonFile.JsonFsHelper.readJsonFileSync}
26
26
  * @public
27
27
  */
28
28
  export function readJsonFileSync(srcPath) {
@@ -48,7 +48,7 @@ export const DefaultJsonFsHelperConfig = {
48
48
  export class JsonFsHelper {
49
49
  /**
50
50
  * Construct a new {@link JsonFile.JsonFsHelper | JsonFsHelper}.
51
- * @param json - Optional {@link JsonFile.IJsonLike | IJsonLike} used to process strings
51
+ * @param init - Optional {@link JsonFile.JsonFsHelperInitOptions | init options} to construct
52
52
  * and JSON values.
53
53
  */
54
54
  constructor(init) {