@fgv/ts-res 5.0.1-9 → 5.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 (162) hide show
  1. package/dist/index.browser.js +53 -0
  2. package/dist/index.js +45 -0
  3. package/dist/packlets/bundle/bundleBuilder.js +130 -0
  4. package/dist/packlets/bundle/bundleLoader.js +101 -0
  5. package/dist/packlets/bundle/bundleNormalizer.js +138 -0
  6. package/dist/packlets/bundle/bundleUtils.js +113 -0
  7. package/dist/packlets/bundle/convert.js +75 -0
  8. package/dist/packlets/bundle/index.js +42 -0
  9. package/dist/packlets/bundle/model.js +23 -0
  10. package/dist/packlets/common/conditions.js +47 -0
  11. package/dist/packlets/common/convert.js +172 -0
  12. package/dist/packlets/common/helpers/conditions.js +66 -0
  13. package/dist/packlets/common/helpers/context.js +70 -0
  14. package/dist/packlets/common/helpers/index.js +26 -0
  15. package/dist/packlets/common/helpers/qualifierDefaultValues.js +66 -0
  16. package/dist/packlets/common/helpers/resources.js +93 -0
  17. package/dist/packlets/common/index.js +28 -0
  18. package/dist/packlets/common/resources.js +27 -0
  19. package/dist/packlets/common/validate/conditions.js +544 -0
  20. package/dist/packlets/common/validate/index.js +26 -0
  21. package/dist/packlets/common/validate/regularExpressions.js +84 -0
  22. package/dist/packlets/common/validate/resources.js +209 -0
  23. package/dist/packlets/conditions/condition.js +252 -0
  24. package/dist/packlets/conditions/conditionCollector.js +69 -0
  25. package/dist/packlets/conditions/conditionDecls.js +23 -0
  26. package/dist/packlets/conditions/conditionSet.js +247 -0
  27. package/dist/packlets/conditions/conditionSetCollector.js +89 -0
  28. package/dist/packlets/conditions/conditionSetDecls.js +23 -0
  29. package/dist/packlets/conditions/conditionToken.js +147 -0
  30. package/dist/packlets/conditions/convert/conditionSetDecls.js +51 -0
  31. package/dist/packlets/conditions/convert/decls.js +75 -0
  32. package/dist/packlets/conditions/convert/index.js +24 -0
  33. package/dist/packlets/conditions/index.js +31 -0
  34. package/dist/packlets/config/common.js +78 -0
  35. package/dist/packlets/config/configInitFactory.js +258 -0
  36. package/dist/packlets/config/convert.js +56 -0
  37. package/dist/packlets/config/index.browser.js +34 -0
  38. package/dist/packlets/config/index.js +28 -0
  39. package/dist/packlets/config/json.js +23 -0
  40. package/dist/packlets/config/predefined/default.js +138 -0
  41. package/dist/packlets/config/predefined/extended.js +190 -0
  42. package/dist/packlets/config/predefined/index.js +25 -0
  43. package/dist/packlets/config/systemConfiguration.js +147 -0
  44. package/dist/packlets/context/contextDecls.js +23 -0
  45. package/dist/packlets/context/contextToken.js +202 -0
  46. package/dist/packlets/context/convert/decls.js +91 -0
  47. package/dist/packlets/context/convert/index.js +23 -0
  48. package/dist/packlets/context/index.js +26 -0
  49. package/dist/packlets/decisions/abstractDecision.js +68 -0
  50. package/dist/packlets/decisions/abstractDecisionCollector.js +90 -0
  51. package/dist/packlets/decisions/candidate.js +76 -0
  52. package/dist/packlets/decisions/common.js +23 -0
  53. package/dist/packlets/decisions/concreteDecision.js +111 -0
  54. package/dist/packlets/decisions/decision.js +126 -0
  55. package/dist/packlets/decisions/index.js +28 -0
  56. package/dist/packlets/import/fsItem.js +134 -0
  57. package/dist/packlets/import/importContext.js +129 -0
  58. package/dist/packlets/import/importManager.js +129 -0
  59. package/dist/packlets/import/importable.js +32 -0
  60. package/dist/packlets/import/importers/collectionImporter.js +118 -0
  61. package/dist/packlets/import/importers/fsItemImporter.js +116 -0
  62. package/dist/packlets/import/importers/importer.js +23 -0
  63. package/dist/packlets/import/importers/index.browser.js +29 -0
  64. package/dist/packlets/import/importers/index.js +27 -0
  65. package/dist/packlets/import/importers/jsonImporter.js +94 -0
  66. package/dist/packlets/import/importers/pathImporter.js +84 -0
  67. package/dist/packlets/import/index.browser.js +34 -0
  68. package/dist/packlets/import/index.js +28 -0
  69. package/dist/packlets/qualifier-types/config/convert.js +124 -0
  70. package/dist/packlets/qualifier-types/config/index.js +25 -0
  71. package/dist/packlets/qualifier-types/config/json.js +32 -0
  72. package/dist/packlets/qualifier-types/convert.js +56 -0
  73. package/dist/packlets/qualifier-types/helpers.js +82 -0
  74. package/dist/packlets/qualifier-types/index.js +32 -0
  75. package/dist/packlets/qualifier-types/languageQualifierType.js +142 -0
  76. package/dist/packlets/qualifier-types/literalQualifierType.js +200 -0
  77. package/dist/packlets/qualifier-types/literalValueHierarchy.js +236 -0
  78. package/dist/packlets/qualifier-types/qualifierType.js +184 -0
  79. package/dist/packlets/qualifier-types/qualifierTypeCollector.js +66 -0
  80. package/dist/packlets/qualifier-types/territoryQualifierType.js +200 -0
  81. package/dist/packlets/qualifiers/convert/decls.js +70 -0
  82. package/dist/packlets/qualifiers/convert/index.js +24 -0
  83. package/dist/packlets/qualifiers/convert/qualifier.js +40 -0
  84. package/dist/packlets/qualifiers/index.js +29 -0
  85. package/dist/packlets/qualifiers/qualifier.js +108 -0
  86. package/dist/packlets/qualifiers/qualifierCollector.js +123 -0
  87. package/dist/packlets/qualifiers/qualifierDecl.js +23 -0
  88. package/dist/packlets/qualifiers/qualifierDefaultValueDecls.js +23 -0
  89. package/dist/packlets/qualifiers/qualifierDefaultValueToken.js +175 -0
  90. package/dist/packlets/resource-json/compiled/common.js +23 -0
  91. package/dist/packlets/resource-json/compiled/convert.js +132 -0
  92. package/dist/packlets/resource-json/compiled/index.js +26 -0
  93. package/dist/packlets/resource-json/compiled/json.js +2 -0
  94. package/dist/packlets/resource-json/convert.js +171 -0
  95. package/dist/packlets/resource-json/helpers.js +241 -0
  96. package/dist/packlets/resource-json/index.js +31 -0
  97. package/dist/packlets/resource-json/json.js +36 -0
  98. package/dist/packlets/resource-json/normalized.js +23 -0
  99. package/dist/packlets/resource-json/resourceDeclCollection.js +88 -0
  100. package/dist/packlets/resource-json/resourceDeclContainer.js +23 -0
  101. package/dist/packlets/resource-json/resourceDeclTree.js +94 -0
  102. package/dist/packlets/resource-types/config/convert.js +35 -0
  103. package/dist/packlets/resource-types/config/index.js +25 -0
  104. package/dist/packlets/resource-types/config/json.js +23 -0
  105. package/dist/packlets/resource-types/helpers.js +42 -0
  106. package/dist/packlets/resource-types/index.js +28 -0
  107. package/dist/packlets/resource-types/jsonResourceType.js +66 -0
  108. package/dist/packlets/resource-types/resourceType.js +95 -0
  109. package/dist/packlets/resource-types/resourceTypeCollector.js +61 -0
  110. package/dist/packlets/resources/candidateReducer.js +246 -0
  111. package/dist/packlets/resources/candidateValue.js +92 -0
  112. package/dist/packlets/resources/candidateValueCollector.js +94 -0
  113. package/dist/packlets/resources/common.js +23 -0
  114. package/dist/packlets/resources/deltaGenerator.js +338 -0
  115. package/dist/packlets/resources/index.js +31 -0
  116. package/dist/packlets/resources/resource.js +207 -0
  117. package/dist/packlets/resources/resourceBuilder.js +183 -0
  118. package/dist/packlets/resources/resourceCandidate.js +216 -0
  119. package/dist/packlets/resources/resourceManagerBuilder.js +890 -0
  120. package/dist/packlets/runtime/cacheListener.js +58 -0
  121. package/dist/packlets/runtime/cacheMetrics.js +149 -0
  122. package/dist/packlets/runtime/compiledResourceCollection.js +473 -0
  123. package/dist/packlets/runtime/conditionSetResolutionResult.js +134 -0
  124. package/dist/packlets/runtime/context/contextQualifierProvider.js +29 -0
  125. package/dist/packlets/runtime/context/contextQualifierProviderValidator.js +146 -0
  126. package/dist/packlets/runtime/context/index.js +26 -0
  127. package/dist/packlets/runtime/context/simpleContextQualifierProvider.js +196 -0
  128. package/dist/packlets/runtime/context/validatingSimpleContextQualifierProvider.js +60 -0
  129. package/dist/packlets/runtime/iResourceManager.js +23 -0
  130. package/dist/packlets/runtime/index.js +34 -0
  131. package/dist/packlets/runtime/resource-tree/common.js +23 -0
  132. package/dist/packlets/runtime/resource-tree/index.js +26 -0
  133. package/dist/packlets/runtime/resource-tree/readOnlyResourceTree.js +216 -0
  134. package/dist/packlets/runtime/resource-tree/resourceTreeChildren.js +97 -0
  135. package/dist/packlets/runtime/resource-tree/resourceTreeChildrenValidator.js +161 -0
  136. package/dist/packlets/runtime/resourceResolver.js +481 -0
  137. package/dist/packlets/runtime/resourceTreeResolver.js +220 -0
  138. package/dist/packlets/runtime/validate.js +47 -0
  139. package/dist/packlets/zip-archive/convert.js +100 -0
  140. package/dist/packlets/zip-archive/index.browser.js +54 -0
  141. package/dist/packlets/zip-archive/index.js +47 -0
  142. package/dist/packlets/zip-archive/json.js +23 -0
  143. package/dist/packlets/zip-archive/types.js +36 -0
  144. package/dist/packlets/zip-archive/zipArchiveCreator.js +191 -0
  145. package/dist/packlets/zip-archive/zipArchiveFormat.js +140 -0
  146. package/dist/packlets/zip-archive/zipArchiveLoader.js +282 -0
  147. package/dist/test/data/sample.json +32 -0
  148. package/dist/test/unit/qualifier-types/testQualifierType.js +76 -0
  149. package/dist/test/unit/resource-types/testDerivedResourceType.js +109 -0
  150. package/dist/test/unit/resources/deltaGenerator.helpers.js +282 -0
  151. package/dist/tsdoc-metadata.json +1 -1
  152. package/lib/index.browser.d.ts +24 -0
  153. package/lib/index.browser.js +116 -0
  154. package/lib/packlets/config/index.browser.d.ts +7 -0
  155. package/lib/packlets/config/index.browser.js +74 -0
  156. package/lib/packlets/import/importers/index.browser.d.ts +4 -0
  157. package/lib/packlets/import/importers/index.browser.js +45 -0
  158. package/lib/packlets/import/index.browser.d.ts +7 -0
  159. package/lib/packlets/import/index.browser.js +74 -0
  160. package/lib/packlets/zip-archive/index.browser.d.ts +27 -0
  161. package/lib/packlets/zip-archive/index.browser.js +102 -0
  162. package/package.json +21 -7
@@ -0,0 +1,338 @@
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
+ import { captureResult, fail, Logging, MessageAggregator, succeed } from '@fgv/ts-utils';
23
+ import { isJsonObject } from '@fgv/ts-json-base';
24
+ import { Diff } from '@fgv/ts-json';
25
+ /**
26
+ * Class for generating resource deltas between baseline and delta resolvers.
27
+ * Creates partial/augment candidates for updated resources and full/replace candidates for new resources.
28
+ * Uses Diff.jsonThreeWayDiff for efficient delta computation.
29
+ * @public
30
+ */
31
+ export class DeltaGenerator {
32
+ /**
33
+ * Constructor for a {@link Resources.DeltaGenerator | DeltaGenerator} object.
34
+ * @param params - Parameters to create a new {@link Resources.DeltaGenerator | DeltaGenerator}.
35
+ * @internal
36
+ */
37
+ constructor(params) {
38
+ var _a;
39
+ this._baselineResolver = params.baselineResolver;
40
+ this._deltaResolver = params.deltaResolver;
41
+ this._resourceManager = params.resourceManager;
42
+ this._logger = (_a = params.logger) !== null && _a !== void 0 ? _a : new Logging.NoOpLogger();
43
+ }
44
+ /**
45
+ * Creates a new {@link Resources.DeltaGenerator | DeltaGenerator} object.
46
+ * @param params - Parameters to create a new {@link Resources.DeltaGenerator | DeltaGenerator}.
47
+ * @returns `Success` with the new {@link Resources.DeltaGenerator | DeltaGenerator} object if successful,
48
+ * or `Failure` with an error message if not.
49
+ * @public
50
+ */
51
+ static create(params) {
52
+ return captureResult(() => new DeltaGenerator(params));
53
+ }
54
+ /**
55
+ * Generates deltas between baseline and delta resolvers.
56
+ * Creates a cloned resource manager with partial/augment candidates for updates
57
+ * and full/replace candidates for new resources.
58
+ *
59
+ * @param options - Options controlling delta generation behavior.
60
+ * @returns `Success` with the updated resource manager if successful,
61
+ * or `Failure` with an error message if not.
62
+ * @public
63
+ */
64
+ generate(options) {
65
+ var _a, _b;
66
+ this._logger.info('Starting delta generation');
67
+ const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : {};
68
+ const skipUnchanged = (_b = options === null || options === void 0 ? void 0 : options.skipUnchanged) !== null && _b !== void 0 ? _b : true;
69
+ return this._validateContext(context)
70
+ .onSuccess((validatedContext) => this._enumerateTargetResources(options === null || options === void 0 ? void 0 : options.resourceIds, validatedContext))
71
+ .onSuccess((resourceIds) => {
72
+ return this._cloneResourceManager()
73
+ .onSuccess((clonedManager) => this._generateDeltas(clonedManager, resourceIds, context, skipUnchanged))
74
+ .onSuccess((manager) => {
75
+ this._logger.info(`Delta generation completed successfully with ${manager.size} resources`);
76
+ return succeed(manager);
77
+ });
78
+ });
79
+ }
80
+ /**
81
+ * Validates the provided context declaration.
82
+ * @param context - The context declaration to validate.
83
+ * @returns `Success` with the validated context if successful, `Failure` otherwise.
84
+ * @internal
85
+ */
86
+ _validateContext(context) {
87
+ return this._resourceManager.validateContext(context);
88
+ }
89
+ /**
90
+ * Enumerates target resource IDs for delta generation.
91
+ * If specific resource IDs are provided, uses those. Otherwise, discovers all resources
92
+ * from both the baseline resource manager and the delta resolver to ensure comprehensive
93
+ * coverage of all potential resources.
94
+ *
95
+ * @param requestedIds - Optional array of specific resource IDs to target.
96
+ * @param context - The validated context to use for resource discovery.
97
+ * @returns `Success` with array of resource IDs if successful, `Failure` otherwise.
98
+ * @internal
99
+ */
100
+ _enumerateTargetResources(requestedIds, context) {
101
+ /* c8 ignore next 20 - functional code tested but coverage intermittently missed */
102
+ if (requestedIds && requestedIds.length > 0) {
103
+ this._logger.info(`Using ${requestedIds.length} specified resource IDs`);
104
+ // Validate the requested IDs
105
+ const validatedIds = [];
106
+ const errors = new MessageAggregator();
107
+ for (const id of requestedIds) {
108
+ const validationResult = this._resourceManager.getBuiltResource(id);
109
+ if (validationResult.isSuccess()) {
110
+ validatedIds.push(id);
111
+ }
112
+ else {
113
+ errors.addMessage(`Invalid resource ID "${id}": ${validationResult.message}`);
114
+ }
115
+ }
116
+ if (errors.hasMessages) {
117
+ return fail(errors.toString());
118
+ }
119
+ return succeed(validatedIds);
120
+ }
121
+ // Discover resources from both baseline and delta resolvers to ensure comprehensive coverage
122
+ this._logger.info('Discovering resources from both baseline and delta resolvers');
123
+ return this._discoverAllResourceIds().onSuccess((allResourceIds) => {
124
+ this._logger.info(`Discovered ${allResourceIds.length} unique resources across both resolvers`);
125
+ return succeed(allResourceIds);
126
+ });
127
+ }
128
+ /**
129
+ * Discovers all unique resource IDs from both baseline and delta resolvers.
130
+ * Creates a union of resource IDs from the baseline resource manager and delta resolver
131
+ * to ensure comprehensive coverage of all resources.
132
+ *
133
+ * @returns `Success` with array of unique resource IDs if successful, `Failure` otherwise.
134
+ * @internal
135
+ */
136
+ _discoverAllResourceIds() {
137
+ // Get resource IDs from baseline resource manager
138
+ return this._resourceManager.getAllBuiltResources().onSuccess((baselineResources) => {
139
+ const baselineResourceIds = baselineResources.map((r) => r.id);
140
+ this._logger.detail(`Found ${baselineResourceIds.length} resources in baseline`);
141
+ // Get resource IDs from delta resolver using the public interface
142
+ const deltaResourceIds = this._deltaResolver.resourceIds;
143
+ this._logger.detail(`Found ${deltaResourceIds.length} resources in delta`);
144
+ // Create a union of resource IDs from both sources
145
+ const allResourceIds = new Set();
146
+ // Add baseline resource IDs
147
+ baselineResourceIds.forEach((id) => {
148
+ allResourceIds.add(id);
149
+ });
150
+ // Add delta resource IDs
151
+ deltaResourceIds.forEach((id) => {
152
+ allResourceIds.add(id);
153
+ });
154
+ const uniqueResourceIds = Array.from(allResourceIds);
155
+ this._logger.detail(`Created union of ${uniqueResourceIds.length} unique resource IDs`);
156
+ return succeed(uniqueResourceIds);
157
+ });
158
+ }
159
+ /**
160
+ * Creates a clone of the resource manager for delta operations.
161
+ * @returns `Success` with the cloned resource manager if successful, `Failure` otherwise.
162
+ * @internal
163
+ */
164
+ _cloneResourceManager() {
165
+ this._logger.info('Cloning resource manager');
166
+ return this._resourceManager.clone();
167
+ }
168
+ /**
169
+ * Generates deltas for the specified resources and adds them to the cloned manager.
170
+ *
171
+ * @param clonedManager - The cloned resource manager to update.
172
+ * @param resourceIds - Array of resource IDs to process.
173
+ * @param context - The context to use for resource resolution.
174
+ * @param skipUnchanged - Whether to skip resources that haven't changed.
175
+ * @returns `Success` with the updated manager if successful, `Failure` otherwise.
176
+ * @internal
177
+ */
178
+ _generateDeltas(clonedManager, resourceIds, context, skipUnchanged) {
179
+ this._logger.info(`Generating deltas for ${resourceIds.length} resources`);
180
+ const errors = new MessageAggregator();
181
+ let processedCount = 0;
182
+ let changedCount = 0;
183
+ let newCount = 0;
184
+ let skippedCount = 0;
185
+ for (const resourceId of resourceIds) {
186
+ this._logger.detail(`Processing resource: ${resourceId}`);
187
+ const result = this._generateResourceDelta(clonedManager, resourceId, context, skipUnchanged);
188
+ /* c8 ignore next 3 - functional code tested but coverage intermittently missed */
189
+ if (result.isFailure()) {
190
+ errors.addMessage(`${resourceId}: ${result.message}`);
191
+ continue;
192
+ }
193
+ processedCount++;
194
+ // Track statistics based on result
195
+ const deltaResult = result.value;
196
+ if (deltaResult.type === 'skipped') {
197
+ skippedCount++;
198
+ }
199
+ else if (deltaResult.type === 'new') {
200
+ newCount++;
201
+ }
202
+ else {
203
+ changedCount++;
204
+ }
205
+ }
206
+ this._logger.info(`Processed ${processedCount} resources: ${changedCount} updated, ${newCount} new, ${skippedCount} skipped`);
207
+ /* c8 ignore next 2 - functional code tested but coverage intermittently missed */
208
+ if (errors.hasMessages) {
209
+ return fail(`Delta generation failed with errors:\n${errors.toString()}`);
210
+ }
211
+ return succeed(clonedManager);
212
+ }
213
+ /**
214
+ * Generates a delta for a single resource and adds appropriate candidates to the manager.
215
+ *
216
+ * @param manager - The resource manager to update.
217
+ * @param resourceId - The resource ID to process.
218
+ * @param context - The context to use for resource resolution.
219
+ * @param skipUnchanged - Whether to skip resources that haven't changed.
220
+ * @returns `Success` with the resource delta result if successful, `Failure` otherwise.
221
+ * @internal
222
+ */
223
+ _generateResourceDelta(manager, resourceId, context, skipUnchanged) {
224
+ // Resolve values from both resolvers
225
+ const baselineResult = this._baselineResolver.resolveComposedResourceValue(resourceId);
226
+ const deltaResult = this._deltaResolver.resolveComposedResourceValue(resourceId);
227
+ const baselineExists = baselineResult.isSuccess();
228
+ const deltaExists = deltaResult.isSuccess();
229
+ if (deltaExists && !baselineExists) {
230
+ // New resource - exists in delta but not in baseline
231
+ this._logger.detail(`${resourceId}: New resource detected (exists in delta only)`);
232
+ return this._createNewResourceCandidate(manager, resourceId, deltaResult.value, context).onSuccess(() => succeed({ type: 'new', resourceId }));
233
+ }
234
+ if (baselineExists && !deltaExists) {
235
+ // Baseline-only resource - exists in baseline but not in delta (potential deletion)
236
+ // For now, skip these resources since delete merge method is not yet implemented
237
+ // TODO: When 'delete' merge method is available, create deletion candidates here
238
+ this._logger.detail(`${resourceId}: Baseline-only resource detected, skipping (delete merge method not yet implemented)`);
239
+ return succeed({ type: 'skipped', resourceId });
240
+ }
241
+ /* c8 ignore next 4 - defense in depth should not happen in practice */
242
+ if (!baselineExists && !deltaExists) {
243
+ // Resource doesn't exist in either resolver - this shouldn't happen due to enumeration logic
244
+ return fail(`Resource ${resourceId} not found in either baseline or delta resolvers`);
245
+ }
246
+ // Both resolvers have the resource - check for changes
247
+ // At this point, both results are successful so values are guaranteed to exist
248
+ const baselineValue = baselineResult.value; // Safe: baselineExists is true
249
+ const deltaValue = deltaResult.value; // Safe: deltaExists is true
250
+ // Check if values are identical
251
+ if (skipUnchanged && this._areValuesIdentical(baselineValue, deltaValue)) {
252
+ this._logger.detail(`${resourceId}: No changes detected, skipping`);
253
+ return succeed({ type: 'skipped', resourceId });
254
+ }
255
+ // Updated resource - compute delta and create partial/augment candidate
256
+ this._logger.detail(`${resourceId}: Changes detected, computing delta`);
257
+ return this._createDeltaCandidate(manager, resourceId, baselineValue, deltaValue, context).onSuccess(() => succeed({ type: 'updated', resourceId }));
258
+ }
259
+ /**
260
+ * Checks if two JSON values are identical.
261
+ * @param value1 - First value to compare.
262
+ * @param value2 - Second value to compare.
263
+ * @returns True if values are identical, false otherwise.
264
+ * @internal
265
+ */
266
+ _areValuesIdentical(value1, value2) {
267
+ return JSON.stringify(value1) === JSON.stringify(value2);
268
+ }
269
+ /**
270
+ * Creates a new resource candidate for a newly discovered resource.
271
+ * Uses full/replace merge method since this is a completely new resource.
272
+ *
273
+ * @param manager - The resource manager to update.
274
+ * @param resourceId - The resource ID for the new resource.
275
+ * @param value - The resolved value for the new resource.
276
+ * @param context - The context used for resolution.
277
+ * @returns `Success` if the candidate was added successfully, `Failure` otherwise.
278
+ * @internal
279
+ */
280
+ _createNewResourceCandidate(manager, resourceId, value, context) {
281
+ /* c8 ignore next 2 - functional code tested but coverage intermittently missed */
282
+ if (!isJsonObject(value)) {
283
+ return fail(`Resource value must be a JSON object, got ${typeof value}`);
284
+ }
285
+ const candidateDecl = {
286
+ id: resourceId,
287
+ json: value,
288
+ conditions: Object.keys(context).length > 0 ? context : undefined,
289
+ isPartial: false,
290
+ mergeMethod: 'replace',
291
+ resourceTypeName: 'json' // Use 'json' resource type for new JSON resources
292
+ };
293
+ return manager.addLooseCandidate(candidateDecl).asResult.onSuccess(() => succeed(undefined));
294
+ }
295
+ /**
296
+ * Creates a delta candidate for an updated resource.
297
+ * Computes the difference between baseline and delta values and creates
298
+ * a partial/augment candidate with only the changed properties.
299
+ *
300
+ * @param manager - The resource manager to update.
301
+ * @param resourceId - The resource ID for the updated resource.
302
+ * @param baselineValue - The baseline resolved value.
303
+ * @param deltaValue - The delta resolved value.
304
+ * @param context - The context used for resolution.
305
+ * @returns `Success` if the candidate was added successfully, `Failure` otherwise.
306
+ * @internal
307
+ */
308
+ _createDeltaCandidate(manager, resourceId, baselineValue, deltaValue, context) {
309
+ // Compute three-way diff to get only the changed properties
310
+ return Diff.jsonThreeWayDiff(baselineValue, deltaValue).onSuccess((diff) => {
311
+ if (diff.identical) {
312
+ // This shouldn't happen if we checked for identity earlier, but be defensive
313
+ this._logger.warn(`${resourceId}: Diff reports identical values, skipping`);
314
+ return succeed(undefined);
315
+ }
316
+ // Use onlyInB (second object) which contains new/changed properties
317
+ const deltaChanges = diff.onlyInB;
318
+ /* c8 ignore next 2 - functional code tested but coverage intermittently missed */
319
+ if (!isJsonObject(deltaChanges)) {
320
+ return fail(`Delta changes must be a JSON object, got ${typeof deltaChanges}`);
321
+ }
322
+ // TODO: Handle deletions using 'augment' merge method with null values
323
+ // Note: The 'delete' merge type is not yet implemented in ts-res, so we use 'augment'
324
+ // with null values for deletion semantics (as used in ts-res-ui-components).
325
+ // When the 'delete' merge type is available, this should be updated to use it.
326
+ const candidateDecl = {
327
+ id: resourceId,
328
+ json: deltaChanges,
329
+ conditions: Object.keys(context).length > 0 ? context : undefined,
330
+ isPartial: true,
331
+ mergeMethod: 'augment',
332
+ resourceTypeName: undefined // Will be inferred by the manager
333
+ };
334
+ return manager.addLooseCandidate(candidateDecl).asResult.onSuccess(() => succeed(undefined));
335
+ });
336
+ }
337
+ }
338
+ //# sourceMappingURL=deltaGenerator.js.map
@@ -0,0 +1,31 @@
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
+ export * from './candidateReducer';
23
+ export * from './candidateValue';
24
+ export * from './candidateValueCollector';
25
+ export * from './deltaGenerator';
26
+ export * from './resourceCandidate';
27
+ export * from './resource';
28
+ export * from './resourceBuilder';
29
+ export * from './resourceManagerBuilder';
30
+ export * from './common';
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,207 @@
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
+ import { MessageAggregator, captureResult, fail, succeed } from '@fgv/ts-utils';
23
+ import { Helpers, Validate } from '../common';
24
+ import { ResourceCandidate } from './resourceCandidate';
25
+ import { CandidateReducer } from './candidateReducer';
26
+ import { ConcreteDecision } from '../decisions';
27
+ /**
28
+ * Represents a single logical resource, with a unique id and a set of possible
29
+ * candidate instances.
30
+ * @public
31
+ */
32
+ export class Resource {
33
+ /**
34
+ * Gets the resource type name as a string.
35
+ */
36
+ get resourceTypeName() {
37
+ return this.resourceType.key;
38
+ }
39
+ /**
40
+ * Constructor for a {@link Resources.Resource | Resource} object.
41
+ * @param params - {@link Resources.IResourceCreateParams | Parameters} used to create the resource.
42
+ * @public
43
+ */
44
+ constructor(params) {
45
+ const id = params.id ? Validate.toResourceId(params.id).orThrow() : undefined;
46
+ this.id = Resource._validateCandidateResourceIds(id, params.candidates).orThrow();
47
+ this.name = Helpers.getNameForResourceId(this.id).orThrow();
48
+ this.resourceType = ResourceCandidate.validateResourceTypes(params.candidates, params.resourceType)
49
+ .onSuccess((t) => {
50
+ /* c8 ignore next 3 - functional code path tested but coverage intermittently missed */
51
+ if (t === undefined) {
52
+ return fail(`${params.id}: no type specified and no candidates with types.`);
53
+ }
54
+ return succeed(t);
55
+ })
56
+ .orThrow();
57
+ this.candidates = Resource._validateAndNormalizeCandidates(params.candidates).orThrow();
58
+ this.decision = Resource._createOptimizedDecision(this.candidates, params.decisions).orThrow();
59
+ }
60
+ /**
61
+ * Creates a new {@link Resources.Resource | Resource} object.
62
+ * @param params - {@link Resources.IResourceCreateParams | Parameters} used to create the resource.
63
+ * @returns `Success` with the new {@link Resources.Resource | Resource} object if successful,
64
+ * or `Failure` with an error message if not.
65
+ * @public
66
+ */
67
+ static create(params) {
68
+ return captureResult(() => new Resource(params));
69
+ }
70
+ /**
71
+ * Gets the candidates for this resource that match the specified {@link Context.IValidatedContextDecl | context}.
72
+ * @param context - The {@link Context.IValidatedContextDecl | context} to match against.
73
+ * @param options - {@link Context.IContextMatchOptions | options} for the context match.
74
+ * @returns The array of {@link Resources.ResourceCandidate | candidates} that match the context.
75
+ */
76
+ getCandidatesForContext(context, options) {
77
+ return this.candidates.filter((c) => c.canMatchPartialContext(context, options));
78
+ }
79
+ /**
80
+ * Gets the {@link ResourceJson.Json.IChildResourceDecl | child resource declaration} for this resource.
81
+ * @param options - {@link ResourceJson.Helpers.IDeclarationOptions | options} for the declaration.
82
+ * @returns The {@link ResourceJson.Json.IChildResourceDecl | child resource declaration}.
83
+ */
84
+ toChildResourceDecl(options) {
85
+ /* c8 ignore next 1 - defense in depth */
86
+ options = options !== null && options !== void 0 ? options : {};
87
+ const matches = this._getMatchingCandidates(options);
88
+ const candidates = options.reduceQualifiers
89
+ ? CandidateReducer.reduceToChildResourceCandidateDecls(matches, options.filterForContext).orThrow()
90
+ : matches.map((c) => c.toChildResourceCandidateDecl());
91
+ return Object.assign({ resourceTypeName: this.resourceTypeName }, (candidates.length > 0 ? { candidates } : {}));
92
+ }
93
+ /**
94
+ * Gets the {@link ResourceJson.Json.ILooseResourceDecl | loose resource declaration} for this resource.
95
+ * @param options - {@link Resources.IResourceDeclarationOptions | options} for the declaration.
96
+ * @returns The {@link ResourceJson.Json.ILooseResourceDecl | loose resource declaration}.
97
+ */
98
+ toLooseResourceDecl(options) {
99
+ /* c8 ignore next 1 - defense in depth */
100
+ options = options !== null && options !== void 0 ? options : {};
101
+ const matches = this._getMatchingCandidates(options);
102
+ const candidates = options.reduceQualifiers
103
+ ? CandidateReducer.reduceToChildResourceCandidateDecls(matches, options.filterForContext).orThrow()
104
+ : matches.map((c) => c.toChildResourceCandidateDecl());
105
+ return Object.assign({ id: this.id, resourceTypeName: this.resourceTypeName }, (candidates.length > 0 ? { candidates } : {}));
106
+ }
107
+ /**
108
+ * Converts this resource to a compiled resource representation.
109
+ * @param options - Optional compilation options controlling the output format and filtering.
110
+ * @returns A compiled resource object that can be used for serialization or runtime processing.
111
+ * @public
112
+ */
113
+ toCompiled(options) {
114
+ const candidates = this._getMatchingCandidates(options).map((c) => ({
115
+ isPartial: c.isPartial,
116
+ mergeMethod: c.mergeMethod,
117
+ valueIndex: c.candidateValue.index
118
+ }));
119
+ return {
120
+ id: this.id,
121
+ type: this.resourceType.index,
122
+ decision: this.decision.baseDecision.index,
123
+ candidates
124
+ };
125
+ }
126
+ /**
127
+ * Validates that all candidates have the same id as the resource.
128
+ * @param resourceId - The expected id of the resource.
129
+ * @param candidates - The array of candidates to validate.
130
+ * @returns `Success` with the resource id if all candidates have the same id,
131
+ * `Failure` with an error message otherwise.
132
+ * @internal
133
+ */
134
+ static _validateCandidateResourceIds(resourceId, candidates) {
135
+ /* c8 ignore next 3 - functional code path tested but coverage intermittently missed */
136
+ if (!resourceId && candidates.length === 0) {
137
+ return fail('unknown: no resource id and no candidates.');
138
+ }
139
+ resourceId = resourceId !== null && resourceId !== void 0 ? resourceId : candidates[0].id;
140
+ const mismatched = candidates.filter((c) => c.id !== resourceId).map((c) => c.id);
141
+ /* c8 ignore next 3 - functional code path tested but coverage intermittently missed */
142
+ if (mismatched.length > 0) {
143
+ return fail(`${resourceId}: candidates with mismatched ids ${mismatched.join(', ')}.`);
144
+ }
145
+ return succeed(resourceId);
146
+ }
147
+ /**
148
+ * Creates an optimized decision for the given candidates using the decisions collector.
149
+ * @param candidates - The validated and normalized candidates to create a decision for.
150
+ * @param decisions - The {@link Decisions.AbstractDecisionCollector | AbstractDecisionCollector} to use.
151
+ * @returns `Success` with the {@link Decisions.ConcreteDecision | ConcreteDecision} if successful,
152
+ * `Failure` with an error message otherwise.
153
+ * @internal
154
+ */
155
+ static _createOptimizedDecision(candidates, decisions) {
156
+ const decisionCandidates = candidates.map((c) => ({
157
+ conditionSet: c.conditions,
158
+ value: c.json
159
+ }));
160
+ return ConcreteDecision.create({
161
+ decisions,
162
+ candidates: decisionCandidates
163
+ });
164
+ }
165
+ /**
166
+ * Validates and normalizes an array of {@link Resources.ResourceCandidate | candidates}. Fails if there
167
+ * are multiple candidates for the same set of conditions.
168
+ * @param candidates - The array of candidates to validate.
169
+ * @returns `Success` with the validated and sorted array of candidates if successful,
170
+ * `Failure` with an error message otherwise.
171
+ * @internal
172
+ */
173
+ static _validateAndNormalizeCandidates(candidates) {
174
+ const errors = new MessageAggregator();
175
+ const validated = new Map();
176
+ for (const candidate of candidates) {
177
+ const conditionSetString = candidate.conditions.toString();
178
+ const existing = validated.get(conditionSetString);
179
+ if (existing) {
180
+ /* c8 ignore next 3 - functional code path tested but coverage intermittently missed */
181
+ if (!ResourceCandidate.equal(candidate, existing)) {
182
+ errors.addMessage(`${candidate.id}: duplicate candidates for ${conditionSetString}.`);
183
+ }
184
+ }
185
+ else {
186
+ validated.set(conditionSetString, candidate);
187
+ }
188
+ }
189
+ return errors.returnOrReport(succeed(Array.from(validated.values()).sort(ResourceCandidate.compare).reverse()));
190
+ }
191
+ /**
192
+ * Gets the appropriate candidates based on filtering options.
193
+ * If a validated filter context is provided, returns only matching candidates.
194
+ * Otherwise returns all candidates.
195
+ * @param options - Options that may contain a validated filter context
196
+ * @returns The filtered array of candidates
197
+ * @internal
198
+ */
199
+ _getMatchingCandidates(options) {
200
+ /* c8 ignore next 5 - functional code path tested but coverage intermittently missed */
201
+ if (options === null || options === void 0 ? void 0 : options.filterForContext) {
202
+ return this.candidates.filter((candidate) => candidate.canMatchPartialContext(options.filterForContext, { partialContextMatch: true }));
203
+ }
204
+ return this.candidates;
205
+ }
206
+ }
207
+ //# sourceMappingURL=resource.js.map