@fgv/ts-json-base 5.0.2 → 5.1.0-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.
Files changed (42) hide show
  1. package/dist/packlets/converters/converters.js +36 -14
  2. package/dist/packlets/file-tree/directoryItem.js +35 -4
  3. package/dist/packlets/file-tree/fileItem.js +37 -9
  4. package/dist/packlets/file-tree/fileTreeAccessors.js +24 -1
  5. package/dist/packlets/file-tree/filterSpec.js +74 -0
  6. package/dist/packlets/file-tree/fsTree.js +73 -12
  7. package/dist/packlets/file-tree/in-memory/inMemoryTree.js +204 -21
  8. package/dist/packlets/file-tree/in-memory/treeBuilder.js +23 -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 +290 -61
  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 +13 -5
  19. package/lib/packlets/file-tree/directoryItem.js +34 -3
  20. package/lib/packlets/file-tree/fileItem.d.ts +26 -13
  21. package/lib/packlets/file-tree/fileItem.js +36 -8
  22. package/lib/packlets/file-tree/fileTreeAccessors.d.ts +141 -1
  23. package/lib/packlets/file-tree/fileTreeAccessors.js +26 -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 +29 -13
  27. package/lib/packlets/file-tree/fsTree.js +72 -11
  28. package/lib/packlets/file-tree/in-memory/inMemoryTree.d.ts +29 -13
  29. package/lib/packlets/file-tree/in-memory/inMemoryTree.js +203 -20
  30. package/lib/packlets/file-tree/in-memory/treeBuilder.d.ts +9 -0
  31. package/lib/packlets/file-tree/in-memory/treeBuilder.js +23 -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 +18 -18
@@ -19,13 +19,83 @@
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
+ }
96
+ /**
97
+ * Implementation of {@link FileTree.IMutableFileTreeAccessors} that uses an in-memory
98
+ * tree to access and modify files and directories.
29
99
  * @public
30
100
  */
31
101
  export class InMemoryTreeAccessors {
@@ -36,12 +106,18 @@ export class InMemoryTreeAccessors {
36
106
  * @public
37
107
  */
38
108
  constructor(files, params) {
39
- var _a, _b;
109
+ var _a, _b, _c, _d;
40
110
  this._tree = TreeBuilder.create(params === null || params === void 0 ? void 0 : params.prefix).orThrow();
41
111
  this._inferContentType = (_a = params === null || params === void 0 ? void 0 : params.inferContentType) !== null && _a !== void 0 ? _a : FileItem.defaultInferContentType;
112
+ this._mutable = (_b = params === null || params === void 0 ? void 0 : params.mutable) !== null && _b !== void 0 ? _b : false;
113
+ this._mutableByPath = new Map();
114
+ const prefix = (_c = params === null || params === void 0 ? void 0 : params.prefix) !== null && _c !== void 0 ? _c : '/';
115
+ this._mutableRoot = new MutableInMemoryDirectory(prefix.endsWith('/') ? prefix.slice(0, -1) || '/' : prefix);
116
+ this._mutableByPath.set(this._mutableRoot.absolutePath, this._mutableRoot);
42
117
  for (const file of files) {
43
- const contentType = (_b = file.contentType) !== null && _b !== void 0 ? _b : this._inferContentType(file.path).orDefault();
118
+ const contentType = (_d = file.contentType) !== null && _d !== void 0 ? _d : this._inferContentType(file.path).orDefault();
44
119
  this._tree.addFile(file.path, file.contents, contentType).orThrow();
120
+ this._addMutableFile(file.path, file.contents, contentType);
45
121
  }
46
122
  }
47
123
  /**
@@ -56,7 +132,7 @@ export class InMemoryTreeAccessors {
56
132
  return captureResult(() => new InMemoryTreeAccessors(files, params));
57
133
  }
58
134
  /**
59
- * {@inheritdoc FileTree.IFileTreeAccessors.resolveAbsolutePath}
135
+ * {@inheritDoc FileTree.IFileTreeAccessors.resolveAbsolutePath}
60
136
  */
61
137
  resolveAbsolutePath(...paths) {
62
138
  const parts = paths[0].startsWith('/') ? paths : [this._tree.prefix, ...paths];
@@ -64,7 +140,7 @@ export class InMemoryTreeAccessors {
64
140
  return `/${joined}`;
65
141
  }
66
142
  /**
67
- * {@inheritdoc FileTree.IFileTreeAccessors.getExtension}
143
+ * {@inheritDoc FileTree.IFileTreeAccessors.getExtension}
68
144
  */
69
145
  getExtension(path) {
70
146
  const parts = path.split('.');
@@ -74,7 +150,7 @@ export class InMemoryTreeAccessors {
74
150
  return `.${parts.pop()}`;
75
151
  }
76
152
  /**
77
- * {@inheritdoc FileTree.IFileTreeAccessors.getBaseName}
153
+ * {@inheritDoc FileTree.IFileTreeAccessors.getBaseName}
78
154
  */
79
155
  getBaseName(path, suffix) {
80
156
  var _a;
@@ -86,13 +162,15 @@ export class InMemoryTreeAccessors {
86
162
  return base;
87
163
  }
88
164
  /**
89
- * {@inheritdoc FileTree.IFileTreeAccessors.joinPaths}
165
+ * {@inheritDoc FileTree.IFileTreeAccessors.joinPaths}
90
166
  */
91
167
  joinPaths(...paths) {
92
- return paths.join('/');
168
+ var _a;
169
+ const joined = paths.flatMap((p) => p.split('/').filter((s) => s.length > 0)).join('/');
170
+ return ((_a = paths[0]) === null || _a === void 0 ? void 0 : _a.startsWith('/')) ? `/${joined}` : joined;
93
171
  }
94
172
  /**
95
- * {@inheritdoc FileTree.IFileTreeAccessors.getItem}
173
+ * {@inheritDoc FileTree.IFileTreeAccessors.getItem}
96
174
  */
97
175
  getItem(itemPath) {
98
176
  const existing = this._tree.byAbsolutePath.get(itemPath);
@@ -107,26 +185,24 @@ export class InMemoryTreeAccessors {
107
185
  return fail(`${itemPath}: not found`);
108
186
  }
109
187
  /**
110
- * {@inheritdoc FileTree.IFileTreeAccessors.getFileContents}
188
+ * {@inheritDoc FileTree.IFileTreeAccessors.getFileContents}
111
189
  */
112
190
  getFileContents(path) {
113
- const item = this._tree.byAbsolutePath.get(path);
191
+ const absolutePath = this.resolveAbsolutePath(path);
192
+ const item = this._mutableByPath.get(absolutePath);
114
193
  if (item === undefined) {
115
- return fail(`${path}: not found`);
194
+ return fail(`${absolutePath}: not found`);
116
195
  }
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`);
196
+ if (!(item instanceof MutableInMemoryFile)) {
197
+ return fail(`${absolutePath}: not a file`);
120
198
  }
121
- // if the body is a string we don't want to add quotes
122
199
  if (typeof item.contents === 'string') {
123
200
  return succeed(item.contents);
124
201
  }
125
- /* c8 ignore next 2 - local coverage is 100% but build coverage has intermittent issues */
126
202
  return captureResult(() => JSON.stringify(item.contents));
127
203
  }
128
204
  /**
129
- * {@inheritdoc FileTree.IFileTreeAccessors.getFileContentType}
205
+ * {@inheritDoc FileTree.IFileTreeAccessors.getFileContentType}
130
206
  */
131
207
  getFileContentType(path, provided) {
132
208
  // If provided contentType is given, use it directly (highest priority)
@@ -149,7 +225,7 @@ export class InMemoryTreeAccessors {
149
225
  return this._inferContentType(path);
150
226
  }
151
227
  /**
152
- * {@inheritdoc FileTree.IFileTreeAccessors.getChildren}
228
+ * {@inheritDoc FileTree.IFileTreeAccessors.getChildren}
153
229
  */
154
230
  getChildren(path) {
155
231
  const item = this._tree.byAbsolutePath.get(path);
@@ -173,5 +249,112 @@ export class InMemoryTreeAccessors {
173
249
  return children;
174
250
  });
175
251
  }
252
+ _addMutableFile(path, contents, contentType) {
253
+ const absolutePath = this.resolveAbsolutePath(path);
254
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
255
+ /* c8 ignore next 3 - defensive: invalid path detection */
256
+ if (parts.length === 0) {
257
+ return fail(`${absolutePath}: invalid file path`);
258
+ }
259
+ let dir = this._mutableRoot;
260
+ while (parts.length > 1) {
261
+ const part = parts.shift();
262
+ const result = dir.getOrAddDirectory(part);
263
+ /* c8 ignore next 3 - defensive: directory conflict during construction */
264
+ if (result.isFailure()) {
265
+ return fail(result.message);
266
+ }
267
+ dir = result.value;
268
+ if (!this._mutableByPath.has(dir.absolutePath)) {
269
+ this._mutableByPath.set(dir.absolutePath, dir);
270
+ }
271
+ }
272
+ return dir.addFile(parts[0], contents, contentType).onSuccess((file) => {
273
+ this._mutableByPath.set(file.absolutePath, file);
274
+ return succeed(file);
275
+ });
276
+ }
277
+ /**
278
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.createDirectory}
279
+ */
280
+ createDirectory(dirPath) {
281
+ const absolutePath = this.resolveAbsolutePath(dirPath);
282
+ // Check if mutability is disabled
283
+ if (this._mutable === false) {
284
+ return fail(`${absolutePath}: mutability is disabled`);
285
+ }
286
+ // Add to the TreeBuilder (read layer)
287
+ const treeResult = this._tree.addDirectory(absolutePath);
288
+ /* c8 ignore next 3 - defensive: read layer failure would indicate internal inconsistency */
289
+ if (treeResult.isFailure()) {
290
+ return fail(treeResult.message);
291
+ }
292
+ // Add to the mutable layer
293
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
294
+ let dir = this._mutableRoot;
295
+ for (const part of parts) {
296
+ const result = dir.getOrAddDirectory(part);
297
+ /* c8 ignore next 3 - defensive: mutable layer should match read layer state */
298
+ if (result.isFailure()) {
299
+ return fail(result.message);
300
+ }
301
+ dir = result.value;
302
+ if (!this._mutableByPath.has(dir.absolutePath)) {
303
+ this._mutableByPath.set(dir.absolutePath, dir);
304
+ }
305
+ }
306
+ return succeed(absolutePath);
307
+ }
308
+ /**
309
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.fileIsMutable}
310
+ */
311
+ fileIsMutable(path) {
312
+ const absolutePath = this.resolveAbsolutePath(path);
313
+ // Check if mutability is disabled
314
+ if (this._mutable === false) {
315
+ return failWithDetail(`${absolutePath}: mutability is disabled`, 'not-mutable');
316
+ }
317
+ // Check if path is excluded by filter
318
+ if (!isPathMutable(absolutePath, this._mutable)) {
319
+ return failWithDetail(`${absolutePath}: path is excluded by filter`, 'path-excluded');
320
+ }
321
+ return succeedWithDetail(true, 'transient');
322
+ }
323
+ /**
324
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.saveFileContents}
325
+ */
326
+ saveFileContents(path, contents) {
327
+ const isMutable = this.fileIsMutable(path);
328
+ if (isMutable.isFailure()) {
329
+ return fail(isMutable.message);
330
+ }
331
+ const absolutePath = this.resolveAbsolutePath(path);
332
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
333
+ if (parts.length === 0) {
334
+ return fail(`${absolutePath}: invalid file path`);
335
+ }
336
+ // Navigate to parent directory, creating directories as needed
337
+ let dir = this._mutableRoot;
338
+ while (parts.length > 1) {
339
+ const part = parts.shift();
340
+ const result = dir.getOrAddDirectory(part);
341
+ if (result.isFailure()) {
342
+ return fail(result.message);
343
+ }
344
+ dir = result.value;
345
+ if (!this._mutableByPath.has(dir.absolutePath)) {
346
+ this._mutableByPath.set(dir.absolutePath, dir);
347
+ }
348
+ }
349
+ // Update or add the file in the mutable layer
350
+ return dir.updateOrAddFile(parts[0], contents).onSuccess((file) => {
351
+ this._mutableByPath.set(file.absolutePath, file);
352
+ // Also register in the read layer so getItem/getChildren can find it
353
+ if (!this._tree.byAbsolutePath.has(file.absolutePath)) {
354
+ this._tree.addFile(file.absolutePath, contents);
355
+ }
356
+ return succeed(contents);
357
+ });
358
+ }
176
359
  }
177
360
  //# sourceMappingURL=inMemoryTree.js.map
@@ -169,5 +169,28 @@ export class TreeBuilder {
169
169
  return succeed(file);
170
170
  });
171
171
  }
172
+ /**
173
+ * Ensures a directory exists at the given absolute path, creating
174
+ * intermediate directories as needed.
175
+ * @param absolutePath - The absolute path of the directory.
176
+ * @returns `Success` with the directory if successful, or
177
+ * `Failure` with an error message otherwise.
178
+ * @public
179
+ */
180
+ addDirectory(absolutePath) {
181
+ const parts = absolutePath.split('/').filter((p) => p.length > 0);
182
+ let dir = this.root;
183
+ for (const part of parts) {
184
+ const result = dir.getOrAddDirectory(part);
185
+ if (result.isFailure()) {
186
+ return fail(result.message);
187
+ }
188
+ dir = result.value;
189
+ if (!this.byAbsolutePath.has(dir.absolutePath)) {
190
+ this.byAbsolutePath.set(dir.absolutePath, dir);
191
+ }
192
+ }
193
+ return succeed(dir);
194
+ }
172
195
  }
173
196
  //# 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) {
@@ -120,26 +120,26 @@ export const jsonValue = new Validation.Base.GenericValidator({
120
120
  }
121
121
  });
122
122
  /**
123
- * A {@link Validation.Classes.StringValidator | StringValidator} which validates a string in place.
123
+ * A `StringValidator` which validates a string in place.
124
124
  * Accepts {@link Validators.IJsonValidatorContext | IJsonValidatorContext} but ignores it.
125
125
  * @public
126
126
  */
127
127
  export const string = new Validation.Classes.StringValidator();
128
128
  /**
129
- * A {@link Validation.Classes.NumberValidator | NumberValidator} which validates a number in place.
129
+ * A `NumberValidator` which validates a number in place.
130
130
  * Accepts {@link Validators.IJsonValidatorContext | IJsonValidatorContext} but ignores it.
131
131
  * @public
132
132
  */
133
133
  export const number = new Validation.Classes.NumberValidator();
134
134
  /**
135
- * A {@link Validation.Classes.BooleanValidator | BooleanValidator} which validates a boolean in place.
136
- * Accepts {@link Validators.IJsonValidatorContext | IJsonValidatorContext} but ignores it.
135
+ * A `BooleanValidator` which validates a boolean in place.
136
+ * Accepts `IJsonValidatorContext` but ignores it.
137
137
  * @public
138
138
  */
139
139
  export const boolean = new Validation.Classes.BooleanValidator();
140
140
  /**
141
141
  * Helper to create a validator for a literal value.
142
- * Accepts {@link Validators.IJsonValidatorContext | IJsonValidatorContext} but ignores it.
142
+ * Accepts `IJsonValidatorContext` but ignores it.
143
143
  * Mirrors the behavior of `@fgv/ts-utils`.
144
144
  * @public
145
145
  */
@@ -151,17 +151,17 @@ export function literal(value) {
151
151
  });
152
152
  }
153
153
  /**
154
- * Helper function to create a {@link Validator | Validator} which validates `unknown` to one of a set of
154
+ * Helper function to create a `Validator` which validates `unknown` to one of a set of
155
155
  * supplied enumerated values. Anything else fails.
156
156
  *
157
157
  * @remarks
158
- * This JSON variant accepts an {@link Validators.IJsonValidatorContext | IJsonValidatorContext} OR
158
+ * This JSON variant accepts an `IJsonValidatorContext` OR
159
159
  * a `ReadonlyArray<T>` as its validation context. If the context is an array, it is used to override the
160
160
  * allowed values for that validation; otherwise, the original `values` supplied at creation time are used.
161
161
  *
162
162
  * @param values - Array of allowed values.
163
163
  * @param message - Optional custom failure message.
164
- * @returns A new {@link Validator | Validator} returning `<T>`.
164
+ * @returns A new `Validator` returning `<T>`.
165
165
  * @public
166
166
  */
167
167
  export function enumeratedValue(values, message) {