@fgv/ts-json-base 5.0.2-0 → 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 (43) hide show
  1. package/CHANGELOG.json +21 -0
  2. package/dist/packlets/converters/converters.js +36 -14
  3. package/dist/packlets/file-tree/directoryItem.js +35 -4
  4. package/dist/packlets/file-tree/fileItem.js +37 -9
  5. package/dist/packlets/file-tree/fileTreeAccessors.js +24 -1
  6. package/dist/packlets/file-tree/filterSpec.js +74 -0
  7. package/dist/packlets/file-tree/fsTree.js +73 -12
  8. package/dist/packlets/file-tree/in-memory/inMemoryTree.js +204 -21
  9. package/dist/packlets/file-tree/in-memory/treeBuilder.js +23 -0
  10. package/dist/packlets/file-tree/index.browser.js +1 -0
  11. package/dist/packlets/file-tree/index.js +1 -0
  12. package/dist/packlets/json-file/file.js +1 -1
  13. package/dist/packlets/json-file/jsonFsHelper.js +1 -1
  14. package/dist/packlets/validators/validators.js +8 -8
  15. package/dist/ts-json-base.d.ts +290 -61
  16. package/dist/tsdoc-metadata.json +1 -1
  17. package/lib/packlets/converters/converters.d.ts +20 -13
  18. package/lib/packlets/converters/converters.js +36 -13
  19. package/lib/packlets/file-tree/directoryItem.d.ts +13 -5
  20. package/lib/packlets/file-tree/directoryItem.js +34 -3
  21. package/lib/packlets/file-tree/fileItem.d.ts +26 -13
  22. package/lib/packlets/file-tree/fileItem.js +36 -8
  23. package/lib/packlets/file-tree/fileTreeAccessors.d.ts +141 -1
  24. package/lib/packlets/file-tree/fileTreeAccessors.js +26 -0
  25. package/lib/packlets/file-tree/filterSpec.d.ts +10 -0
  26. package/lib/packlets/file-tree/filterSpec.js +77 -0
  27. package/lib/packlets/file-tree/fsTree.d.ts +29 -13
  28. package/lib/packlets/file-tree/fsTree.js +72 -11
  29. package/lib/packlets/file-tree/in-memory/inMemoryTree.d.ts +29 -13
  30. package/lib/packlets/file-tree/in-memory/inMemoryTree.js +203 -20
  31. package/lib/packlets/file-tree/in-memory/treeBuilder.d.ts +9 -0
  32. package/lib/packlets/file-tree/in-memory/treeBuilder.js +23 -0
  33. package/lib/packlets/file-tree/index.browser.d.ts +1 -0
  34. package/lib/packlets/file-tree/index.browser.js +1 -0
  35. package/lib/packlets/file-tree/index.d.ts +1 -0
  36. package/lib/packlets/file-tree/index.js +1 -0
  37. package/lib/packlets/json-file/file.d.ts +1 -1
  38. package/lib/packlets/json-file/file.js +1 -1
  39. package/lib/packlets/json-file/jsonFsHelper.d.ts +1 -1
  40. package/lib/packlets/json-file/jsonFsHelper.js +1 -1
  41. package/lib/packlets/validators/validators.d.ts +9 -9
  42. package/lib/packlets/validators/validators.js +8 -8
  43. package/package.json +18 -18
package/CHANGELOG.json CHANGED
@@ -1,6 +1,27 @@
1
1
  {
2
2
  "name": "@fgv/ts-json-base",
3
3
  "entries": [
4
+ {
5
+ "version": "5.0.2",
6
+ "tag": "@fgv/ts-json-base_v5.0.2",
7
+ "date": "Wed, 17 Dec 2025 18:08:09 GMT",
8
+ "comments": {
9
+ "none": [
10
+ {
11
+ "comment": "dual-publish"
12
+ },
13
+ {
14
+ "comment": "add converters that take json context"
15
+ },
16
+ {
17
+ "comment": "minor cleanup"
18
+ },
19
+ {
20
+ "comment": "export JsonCompatible for web consumers as well"
21
+ }
22
+ ]
23
+ }
24
+ },
4
25
  {
5
26
  "version": "5.0.1",
6
27
  "tag": "@fgv/ts-json-base_v5.0.1",
@@ -19,7 +19,7 @@
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 { Conversion, Converters as BaseConverters, StringConverter, fail, succeed } from '@fgv/ts-utils';
22
+ import { Conversion, Converters as BaseConverters, StringConverter, captureResult, fail, succeed } from '@fgv/ts-utils';
23
23
  import { isJsonArray, isJsonObject } from '../json';
24
24
  /**
25
25
  * An converter which converts a supplied `unknown` value to a valid {@link JsonPrimitive | JsonPrimitive}.
@@ -45,7 +45,7 @@ export const jsonPrimitive = new Conversion.BaseConverter((from, __self, ctx) =>
45
45
  * An copying converter which converts a supplied `unknown` value into
46
46
  * a valid {@link JsonObject | JsonObject}. Fails by default if any properties or array elements
47
47
  * are `undefined` - this default behavior can be overridden by supplying an appropriate
48
- * {@link Converters.IJsonConverterContext | context} at runtime.
48
+ * `IJsonConverterContext` at runtime.
49
49
  *
50
50
  * Guaranteed to return a new object.
51
51
  * @public
@@ -81,7 +81,7 @@ export const jsonObject = new Conversion.BaseConverter((from, __self, ctx) => {
81
81
  * An copying converter which converts a supplied `unknown` value to
82
82
  * a valid {@link JsonArray | JsonArray}. Fails by default if any properties or array elements
83
83
  * are `undefined` - this default behavior can be overridden by supplying an appropriate
84
- * {@link Converters.IJsonConverterContext | context} at runtime.
84
+ * `IJsonConverterContext` at runtime.
85
85
  *
86
86
  * Guaranteed to return a new array.
87
87
  * @public
@@ -119,7 +119,7 @@ export const jsonArray = new Conversion.BaseConverter((from, __self, ctx) => {
119
119
  * An copying converter which converts a supplied `unknown` value to a
120
120
  * valid {@link JsonValue | JsonValue}. Fails by default if any properties or array elements
121
121
  * are `undefined` - this default behavior can be overridden by supplying an appropriate
122
- * {@link Converters.IJsonConverterContext | context} at runtime.
122
+ * `IJsonConverterContext` at runtime.
123
123
  * @public
124
124
  */
125
125
  export const jsonValue = new Conversion.BaseConverter((from, __self, ctx) => {
@@ -132,28 +132,28 @@ export const jsonValue = new Conversion.BaseConverter((from, __self, ctx) => {
132
132
  return jsonPrimitive.convert(from, ctx);
133
133
  });
134
134
  /**
135
- * A {@link Converter | Converter} which converts `unknown` to a `string`.
136
- * Accepts {@link Converters.IJsonConverterContext | IJsonConverterContext} but ignores it.
135
+ * A `StringConverter` which converts `unknown` to a `string`.
136
+ * Accepts `IJsonConverterContext` but ignores it.
137
137
  * @public
138
138
  */
139
139
  export const string = new StringConverter();
140
140
  /**
141
- * A {@link Converter | Converter} which converts `unknown` to a `number`.
142
- * Accepts {@link Converters.IJsonConverterContext | IJsonConverterContext} but ignores it.
141
+ * A `Converter` which converts `unknown` to a `number`.
142
+ * Accepts `IJsonConverterContext` but ignores it.
143
143
  * Mirrors the behavior of `@fgv/ts-utils`.
144
144
  * @public
145
145
  */
146
146
  export const number = new Conversion.BaseConverter((from) => BaseConverters.number.convert(from));
147
147
  /**
148
- * A {@link Converter | Converter} which converts `unknown` to a `boolean`.
149
- * Accepts {@link Converters.IJsonConverterContext | IJsonConverterContext} but ignores it.
148
+ * A `Converter` which converts `unknown` to a `boolean`.
149
+ * Accepts `IJsonConverterContext` but ignores it.
150
150
  * Mirrors the behavior of `@fgv/ts-utils`.
151
151
  * @public
152
152
  */
153
153
  export const boolean = new Conversion.BaseConverter((from) => BaseConverters.boolean.convert(from));
154
154
  /**
155
155
  * Helper to create a converter for a literal value.
156
- * Accepts {@link Converters.IJsonConverterContext | IJsonConverterContext} but ignores it.
156
+ * Accepts `IJsonConverterContext` but ignores it.
157
157
  * Mirrors the behavior of `@fgv/ts-utils`.
158
158
  * @public
159
159
  */
@@ -161,17 +161,17 @@ export function literal(value) {
161
161
  return BaseConverters.literal(value);
162
162
  }
163
163
  /**
164
- * Helper function to create a {@link Converter | Converter} which converts `unknown` to one of a set of
164
+ * Helper function to create a `Converter` which converts `unknown` to one of a set of
165
165
  * supplied enumerated values. Anything else fails.
166
166
  *
167
167
  * @remarks
168
- * This JSON variant accepts an {@link Converters.IJsonConverterContext | IJsonConverterContext} OR
168
+ * This JSON variant accepts an `IJsonConverterContext` OR
169
169
  * a `ReadonlyArray<T>` as its conversion context. If the context is an array, it is used to override the
170
170
  * allowed values for that conversion; otherwise, the original `values` supplied at creation time are used.
171
171
  *
172
172
  * @param values - Array of allowed values.
173
173
  * @param message - Optional custom failure message.
174
- * @returns A new {@link Converter | Converter} returning `<T>`.
174
+ * @returns A new `Converter` returning `<T>`.
175
175
  * @public
176
176
  */
177
177
  export function enumeratedValue(values, message) {
@@ -184,4 +184,26 @@ export function enumeratedValue(values, message) {
184
184
  return fail(message !== null && message !== void 0 ? message : `Invalid enumerated value ${JSON.stringify(from)}`);
185
185
  });
186
186
  }
187
+ /**
188
+ * Creates a converter that parses JSON string content and then applies the supplied converter.
189
+ * @param converter - Converter to apply to the parsed JSON
190
+ * @returns Converter that parses JSON then validates
191
+ * @public
192
+ */
193
+ export function jsonConverter(converter) {
194
+ return new Conversion.BaseConverter((from) => {
195
+ if (typeof from !== 'string') {
196
+ return fail('Input must be a string');
197
+ }
198
+ const parseResult = captureResult(() => JSON.parse(from));
199
+ if (parseResult.isFailure()) {
200
+ return fail(`Failed to parse JSON: ${parseResult.message}`);
201
+ }
202
+ const parsed = parseResult.value;
203
+ if (typeof parsed !== 'object' || parsed === null) {
204
+ return fail('Failed to parse JSON: JSON content must be an object');
205
+ }
206
+ return converter.convert(parsed);
207
+ });
208
+ }
187
209
  //# sourceMappingURL=converters.js.map
@@ -19,14 +19,15 @@
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 } from '@fgv/ts-utils';
22
+ import { captureResult, fail, succeed } from '@fgv/ts-utils';
23
+ import { isMutableAccessors } from './fileTreeAccessors';
23
24
  /**
24
25
  * Class representing a directory in a file tree.
25
26
  * @public
26
27
  */
27
28
  export class DirectoryItem {
28
29
  /**
29
- * {@inheritdoc FileTree.IFileTreeDirectoryItem.name}
30
+ * {@inheritDoc FileTree.IFileTreeDirectoryItem.name}
30
31
  */
31
32
  get name() {
32
33
  return this._hal.getBaseName(this.absolutePath);
@@ -40,7 +41,7 @@ export class DirectoryItem {
40
41
  */
41
42
  constructor(path, hal) {
42
43
  /**
43
- * {@inheritdoc FileTree.IFileTreeDirectoryItem."type"}
44
+ * {@inheritDoc FileTree.IFileTreeDirectoryItem."type"}
44
45
  */
45
46
  this.type = 'directory';
46
47
  this._hal = hal;
@@ -58,10 +59,40 @@ export class DirectoryItem {
58
59
  return captureResult(() => new DirectoryItem(path, hal));
59
60
  }
60
61
  /**
61
- * {@inheritdoc FileTree.IFileTreeDirectoryItem.getChildren}
62
+ * {@inheritDoc FileTree.IFileTreeDirectoryItem.getChildren}
62
63
  */
63
64
  getChildren() {
64
65
  return this._hal.getChildren(this.absolutePath);
65
66
  }
67
+ /**
68
+ * {@inheritDoc FileTree.IFileTreeDirectoryItem.createChildFile}
69
+ */
70
+ createChildFile(name, contents) {
71
+ if (!isMutableAccessors(this._hal)) {
72
+ return fail(`${this.absolutePath}: mutation not supported`);
73
+ }
74
+ const filePath = this._hal.joinPaths(this.absolutePath, name);
75
+ return this._hal.saveFileContents(filePath, contents).onSuccess(() => this._hal.getItem(filePath).onSuccess((item) => {
76
+ /* 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
+ }
80
+ return succeed(item);
81
+ }));
82
+ }
83
+ /**
84
+ * {@inheritDoc FileTree.IFileTreeDirectoryItem.createChildDirectory}
85
+ */
86
+ createChildDirectory(name) {
87
+ if (!isMutableAccessors(this._hal)) {
88
+ return fail(`${this.absolutePath}: mutation not supported`);
89
+ }
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`);
93
+ }
94
+ const dirPath = this._hal.joinPaths(this.absolutePath, name);
95
+ return this._hal.createDirectory(dirPath).onSuccess(() => DirectoryItem.create(dirPath, this._hal));
96
+ }
66
97
  }
67
98
  //# sourceMappingURL=directoryItem.js.map
@@ -19,32 +19,33 @@
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, succeed } from '@fgv/ts-utils';
22
+ import { captureResult, fail, failWithDetail, succeed, Success } from '@fgv/ts-utils';
23
+ import { isMutableAccessors } from './fileTreeAccessors';
23
24
  /**
24
25
  * Class representing a file in a file tree.
25
26
  * @public
26
27
  */
27
28
  export class FileItem {
28
29
  /**
29
- * {@inheritdoc FileTree.IFileTreeFileItem.name}
30
+ * {@inheritDoc FileTree.IFileTreeFileItem.name}
30
31
  */
31
32
  get name() {
32
33
  return this._hal.getBaseName(this.absolutePath);
33
34
  }
34
35
  /**
35
- * {@inheritdoc FileTree.IFileTreeFileItem.baseName}
36
+ * {@inheritDoc FileTree.IFileTreeFileItem.baseName}
36
37
  */
37
38
  get baseName() {
38
39
  return this._hal.getBaseName(this.absolutePath, this.extension);
39
40
  }
40
41
  /**
41
- * {@inheritdoc FileTree.IFileTreeFileItem.extension}
42
+ * {@inheritDoc FileTree.IFileTreeFileItem.extension}
42
43
  */
43
44
  get extension() {
44
45
  return this._hal.getExtension(this.absolutePath);
45
46
  }
46
47
  /**
47
- * {@inheritdoc FileTree.IFileTreeFileItem.contentType}
48
+ * {@inheritDoc FileTree.IFileTreeFileItem.contentType}
48
49
  */
49
50
  get contentType() {
50
51
  return this._contentType;
@@ -58,7 +59,7 @@ export class FileItem {
58
59
  */
59
60
  constructor(path, hal) {
60
61
  /**
61
- * {@inheritdoc FileTree.IFileTreeFileItem."type"}
62
+ * {@inheritDoc FileTree.IFileTreeFileItem."type"}
62
63
  */
63
64
  this.type = 'file';
64
65
  this._hal = hal;
@@ -75,6 +76,16 @@ export class FileItem {
75
76
  static create(path, hal) {
76
77
  return captureResult(() => new FileItem(path, hal));
77
78
  }
79
+ /**
80
+ * {@inheritDoc FileTree.IFileTreeFileItem.getIsMutable}
81
+ */
82
+ getIsMutable() {
83
+ if (isMutableAccessors(this._hal)) {
84
+ return this._hal.fileIsMutable(this.absolutePath);
85
+ }
86
+ /* c8 ignore next 2 - defensive: all current accessor implementations support mutation interface */
87
+ return failWithDetail(`${this.absolutePath}: mutation not supported`, 'not-supported');
88
+ }
78
89
  getContents(converter) {
79
90
  return this._hal
80
91
  .getFileContents(this.absolutePath)
@@ -87,7 +98,7 @@ export class FileItem {
87
98
  });
88
99
  }
89
100
  /**
90
- * {@inheritdoc FileTree.IFileTreeFileItem.getRawContents}
101
+ * {@inheritDoc FileTree.IFileTreeFileItem.getRawContents}
91
102
  */
92
103
  getRawContents() {
93
104
  return this._hal.getFileContents(this.absolutePath);
@@ -99,15 +110,32 @@ export class FileItem {
99
110
  setContentType(contentType) {
100
111
  this._contentType = contentType;
101
112
  }
113
+ /**
114
+ * {@inheritDoc FileTree.IFileTreeFileItem.setContents}
115
+ */
116
+ setContents(json) {
117
+ return captureResult(() => JSON.stringify(json, null, 2)).onSuccess((contents) => this.setRawContents(contents).onSuccess(() => Success.with(json)));
118
+ }
119
+ /**
120
+ * {@inheritDoc FileTree.IFileTreeFileItem.setRawContents}
121
+ */
122
+ setRawContents(contents) {
123
+ if (isMutableAccessors(this._hal)) {
124
+ return this._hal.saveFileContents(this.absolutePath, contents);
125
+ }
126
+ /* c8 ignore next 2 - defensive: all current accessor implementations support mutation interface */
127
+ return fail(`${this.absolutePath}: mutation not supported`);
128
+ }
102
129
  /**
103
130
  * Default function to infer the content type of a file.
104
131
  * @param filePath - The path of the file.
132
+ * @param provided - Optional supplied content type.
105
133
  * @returns `Success` with the content type of the file if successful, or
106
134
  * `Failure` with an error message otherwise.
107
135
  * @remarks This default implementation always returns `Success` with `undefined`.
108
136
  * @public
109
137
  */
110
- static defaultInferContentType(__filePath, __provided) {
138
+ static defaultInferContentType(filePath, provided) {
111
139
  return succeed(undefined);
112
140
  }
113
141
  /**
@@ -119,7 +147,7 @@ export class FileItem {
119
147
  * @remarks This default implementation always returns `Success` with `undefined`.
120
148
  * @public
121
149
  */
122
- static defaultAcceptContentType(__filePath, provided) {
150
+ static defaultAcceptContentType(filePath, provided) {
123
151
  return succeed(provided);
124
152
  }
125
153
  }
@@ -19,5 +19,28 @@
19
19
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
20
  * SOFTWARE.
21
21
  */
22
- export {};
22
+ /**
23
+ * Type guard to check if accessors support mutation.
24
+ * @param accessors - The accessors to check.
25
+ * @returns `true` if the accessors implement {@link FileTree.IMutableFileTreeAccessors}.
26
+ * @public
27
+ */
28
+ export function isMutableAccessors(accessors) {
29
+ const mutable = accessors;
30
+ return typeof mutable.fileIsMutable === 'function' && typeof mutable.saveFileContents === 'function';
31
+ }
32
+ /**
33
+ * Type guard to check if accessors support persistence.
34
+ * @param accessors - The accessors to check.
35
+ * @returns `true` if the accessors implement {@link FileTree.IPersistentFileTreeAccessors}.
36
+ * @public
37
+ */
38
+ export function isPersistentAccessors(accessors) {
39
+ const persistent = accessors;
40
+ /* c8 ignore next 6 - no current accessor implements IPersistentFileTreeAccessors */
41
+ return (isMutableAccessors(accessors) &&
42
+ typeof persistent.syncToDisk === 'function' &&
43
+ typeof persistent.isDirty === 'function' &&
44
+ typeof persistent.getDirtyPaths === 'function');
45
+ }
23
46
  //# sourceMappingURL=fileTreeAccessors.js.map
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Copyright (c) 2025 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ /**
23
+ * Checks if a path matches a single pattern (string or RegExp).
24
+ * @param path - The path to check.
25
+ * @param pattern - The pattern to match against.
26
+ * @returns `true` if the path matches the pattern.
27
+ * @internal
28
+ */
29
+ function matchesPattern(path, pattern) {
30
+ if (typeof pattern === 'string') {
31
+ return path === pattern || path.startsWith(pattern + '/') || path.includes(pattern);
32
+ }
33
+ return pattern.test(path);
34
+ }
35
+ /**
36
+ * Checks if a path matches any pattern in an array.
37
+ * @param path - The path to check.
38
+ * @param patterns - The patterns to match against.
39
+ * @returns `true` if the path matches any pattern.
40
+ * @internal
41
+ */
42
+ function matchesAny(path, patterns) {
43
+ if (!patterns || patterns.length === 0) {
44
+ return false;
45
+ }
46
+ return patterns.some((pattern) => matchesPattern(path, pattern));
47
+ }
48
+ /**
49
+ * Checks if a path is allowed by a mutability configuration.
50
+ * @param path - The path to check.
51
+ * @param mutable - The mutability configuration.
52
+ * @returns `true` if the path is mutable according to the configuration.
53
+ * @public
54
+ */
55
+ export function isPathMutable(path, mutable) {
56
+ if (mutable === undefined || mutable === false) {
57
+ return false;
58
+ }
59
+ if (mutable === true) {
60
+ return true;
61
+ }
62
+ const { include, exclude } = mutable;
63
+ // If exclude patterns are specified and path matches, it's not mutable
64
+ if (matchesAny(path, exclude)) {
65
+ return false;
66
+ }
67
+ // If include patterns are specified, path must match at least one
68
+ if (include && include.length > 0) {
69
+ return matchesAny(path, include);
70
+ }
71
+ // No include patterns means all paths (not excluded) are mutable
72
+ return true;
73
+ }
74
+ //# sourceMappingURL=filterSpec.js.map
@@ -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,63 @@ 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
+ }
152
+ catch (_a) {
153
+ return failWithDetail(`${absolutePath}: permission denied`, 'permission-denied');
154
+ }
155
+ }
156
+ /**
157
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.saveFileContents}
158
+ */
159
+ saveFileContents(path, contents) {
160
+ return this.fileIsMutable(path).asResult.onSuccess(() => {
161
+ const absolutePath = this.resolveAbsolutePath(path);
162
+ return captureResult(() => {
163
+ fs.writeFileSync(absolutePath, contents, 'utf8');
164
+ return contents;
165
+ });
166
+ });
167
+ }
168
+ /**
169
+ * {@inheritDoc FileTree.IMutableFileTreeAccessors.createDirectory}
170
+ */
171
+ createDirectory(dirPath) {
172
+ const absolutePath = this.resolveAbsolutePath(dirPath);
173
+ // Check if mutability is disabled
174
+ if (this._mutable === false) {
175
+ return fail(`${absolutePath}: mutability is disabled`);
176
+ }
177
+ return captureResult(() => {
178
+ fs.mkdirSync(absolutePath, { recursive: true });
179
+ return absolutePath;
180
+ });
181
+ }
121
182
  }
122
183
  //# sourceMappingURL=fsTree.js.map