@speclynx/apidom-overlay 5.0.2 → 5.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speclynx/apidom-overlay",
3
- "version": "5.0.2",
3
+ "version": "5.1.1",
4
4
  "description": "Apply Overlay 1.x.y documents to API definitions.",
5
5
  "keywords": [
6
6
  "apidom",
@@ -53,12 +53,12 @@
53
53
  "license": "Apache-2.0",
54
54
  "dependencies": {
55
55
  "@babel/runtime-corejs3": "^8.0.0",
56
- "@speclynx/apidom-core": "5.0.2",
57
- "@speclynx/apidom-datamodel": "5.0.2",
58
- "@speclynx/apidom-error": "5.0.2",
59
- "@speclynx/apidom-json-path": "5.0.2",
60
- "@speclynx/apidom-ns-overlay-1": "5.0.2",
61
- "@speclynx/apidom-reference": "5.0.2",
56
+ "@speclynx/apidom-core": "5.1.1",
57
+ "@speclynx/apidom-datamodel": "5.1.1",
58
+ "@speclynx/apidom-error": "5.1.1",
59
+ "@speclynx/apidom-json-path": "5.1.1",
60
+ "@speclynx/apidom-ns-overlay-1": "5.1.1",
61
+ "@speclynx/apidom-reference": "5.1.1",
62
62
  "type-fest": "^5.8.0"
63
63
  },
64
64
  "files": [
@@ -71,5 +71,5 @@
71
71
  "README.md",
72
72
  "CHANGELOG.md"
73
73
  ],
74
- "gitHead": "01792b567baea3a47cb08d09e478178aa04afda9"
74
+ "gitHead": "b661f6f038953d118ad09eec0d7da7927db46f5c"
75
75
  }
@@ -25,6 +25,18 @@ const defaultApplyOptions = {
25
25
  strict: false
26
26
  };
27
27
 
28
+ /**
29
+ * Merges caller options over the defaults. A partial options object leaves the
30
+ * fields it omits undefined, which must still mean the documented defaults.
31
+ * `immutable` is spelled out because it is the only default that is not falsy,
32
+ * so a caller forwarding an undefined value must not end up mutating in place.
33
+ */
34
+ const withDefaults = options => ({
35
+ ...defaultApplyOptions,
36
+ ...options,
37
+ immutable: options.immutable ?? defaultApplyOptions.immutable
38
+ });
39
+
28
40
  /**
29
41
  * Default customMerge that enforces Overlay spec type compatibility.
30
42
  * Throws OverlayError on incompatible type combinations.
@@ -85,12 +97,37 @@ const resolveParent = (normalizedPath, root) => {
85
97
  * @public
86
98
  */
87
99
  const applyUpdateAction = (normalizedPaths, updateValue, targetElement, options = defaultApplyOptions) => {
88
- let result = options.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
100
+ const opts = withDefaults(options);
101
+ let result = opts.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
89
102
  for (const normalizedPath of normalizedPaths) {
103
+ /**
104
+ * Every matched node gets its own copy of the update value. deepmerge
105
+ * shallow clones members, so the merged result shares key elements and
106
+ * non-mergeable values with its source regardless of the clone option —
107
+ * without this each target would alias every other target, and a copy
108
+ * action would alias the copy source in the target document.
109
+ */
110
+ const clonedUpdateValue = (0, _apidomDatamodel.cloneDeep)(updateValue);
90
111
  const resolved = resolveParent(normalizedPath, result);
91
112
  if (resolved === null) {
92
- // targeting root
93
- result = mergeValue(result, updateValue, options);
113
+ /**
114
+ * Targeting root. There is no parent slot to write the merged element
115
+ * into, so the merge is transplanted back onto the target itself — every
116
+ * other target shape updates the document in place and the root must not
117
+ * be the silent exception. Merging always yields the target's own
118
+ * constructor, so the transplant is type-safe. Two cases cannot be
119
+ * written in place and fall back to rebinding: a primitive root replaced
120
+ * by a differently typed value, and a frozen root, whose properties
121
+ * cannot be assigned.
122
+ */
123
+ const merged = mergeValue(result, clonedUpdateValue, opts);
124
+ if (merged.constructor === result.constructor && !result.isFrozen) {
125
+ result.content = merged.content;
126
+ result.meta = merged.meta;
127
+ result.attributes = merged.attributes;
128
+ } else {
129
+ result = merged;
130
+ }
94
131
  continue;
95
132
  }
96
133
  const {
@@ -100,12 +137,12 @@ const applyUpdateAction = (normalizedPaths, updateValue, targetElement, options
100
137
  if ((0, _apidomDatamodel.isObjectElement)(parent)) {
101
138
  const current = parent.get(key);
102
139
  if (current !== undefined) {
103
- parent.set(key, mergeValue(current, updateValue, options));
140
+ parent.set(key, mergeValue(current, clonedUpdateValue, opts));
104
141
  }
105
142
  } else if ((0, _apidomDatamodel.isArrayElement)(parent)) {
106
143
  const current = parent.get(key);
107
144
  if (current !== undefined) {
108
- parent.content[key] = mergeValue(current, updateValue, options);
145
+ parent.content[key] = mergeValue(current, clonedUpdateValue, opts);
109
146
  }
110
147
  }
111
148
  }
@@ -125,7 +162,13 @@ const applyCopyAction = (normalizedPaths, copyExpression, targetElement, options
125
162
  if (sourceMatches.length > 1) {
126
163
  throw new _OverlayError.default(`Copy source JSONPath "${copyExpression}" must select a single node, but selected ${sourceMatches.length}`);
127
164
  }
128
- const sourceValue = sourceMatches[0];
165
+
166
+ /**
167
+ * Snapshot the copy source. Matched targets are updated in document order and
168
+ * the source may sit under one of them, so a live reference would let later
169
+ * targets copy a source that an earlier target already modified.
170
+ */
171
+ const sourceValue = (0, _apidomDatamodel.cloneDeep)(sourceMatches[0]);
129
172
  return applyUpdateAction(normalizedPaths, sourceValue, targetElement, options);
130
173
  };
131
174
 
@@ -135,7 +178,8 @@ const applyCopyAction = (normalizedPaths, copyExpression, targetElement, options
135
178
  */
136
179
  exports.applyCopyAction = applyCopyAction;
137
180
  const applyRemoveAction = (normalizedPaths, targetElement, options = defaultApplyOptions) => {
138
- const result = options.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
181
+ const opts = withDefaults(options);
182
+ const result = opts.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
139
183
  // reverse document order so higher array indices are removed first (avoids index shifting)
140
184
  const sorted = [...normalizedPaths].reverse();
141
185
  for (const normalizedPath of sorted) {
@@ -158,14 +202,20 @@ const applyRemoveAction = (normalizedPaths, targetElement, options = defaultAppl
158
202
 
159
203
  /**
160
204
  * Applies a single overlay action to a target element.
161
- * Immutable — returns a new element with the action applied.
205
+ *
206
+ * Immutable by default. The target is cloned once up front and the action is
207
+ * applied to the clone, so the returned element is always independent of the
208
+ * input, including when the action changes nothing. Pass `immutable: false` to
209
+ * apply the action in place; the one shape that cannot be applied in place is
210
+ * a `target: $` action replacing a primitive root with a differently typed
211
+ * value, so the return value is authoritative in both modes.
162
212
  *
163
213
  * @public
164
214
  */
165
215
  exports.applyRemoveAction = applyRemoveAction;
166
216
  const applyAction = (actionElement, targetElement, options = defaultApplyOptions) => {
167
- const internalOpts = options;
168
- const tracer = internalOpts.tracer ?? (options.trace ? new _TraceBuilder.default(options.trace) : undefined);
217
+ const opts = withDefaults(options);
218
+ const tracer = opts.tracer ?? (opts.trace ? new _TraceBuilder.default(opts.trace) : undefined);
169
219
  const actionValidation = (0, _validate.validateAction)(actionElement);
170
220
  if (!actionValidation.valid) throw actionValidation.error;
171
221
  const targetExpression = (0, _apidomCore.toValue)(actionElement.target);
@@ -178,7 +228,7 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
178
228
  }
179
229
  });
180
230
  if (matches.length === 0) {
181
- if (options.strict) {
231
+ if (opts.strict) {
182
232
  const error = new _OverlayError.default(`Action target "${targetExpression}" matched zero nodes (strict mode)`, {
183
233
  action: actionElement
184
234
  });
@@ -198,13 +248,20 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
198
248
  matchCount: 0,
199
249
  normalizedPaths: []
200
250
  });
201
- return targetElement;
251
+ return opts.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
202
252
  }
253
+
254
+ // clone once, then apply the action mutably to the clone
255
+ const workingElement = opts.immutable ? (0, _apidomDatamodel.cloneDeep)(targetElement) : targetElement;
256
+ const mutatingOptions = {
257
+ ...opts,
258
+ immutable: false
259
+ };
203
260
  try {
204
261
  // remove action
205
262
  const removeValue = actionElement.removeField;
206
263
  if (removeValue !== undefined && (0, _apidomCore.toValue)(removeValue) === true) {
207
- const result = applyRemoveAction(normalizedPaths, targetElement, options);
264
+ const result = applyRemoveAction(normalizedPaths, workingElement, mutatingOptions);
208
265
  tracer?.action({
209
266
  target: targetExpression,
210
267
  type: 'remove',
@@ -218,7 +275,7 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
218
275
  if (actionElement.update !== undefined) {
219
276
  const nodesValidation = (0, _validate.validateTargetNodes)(matches);
220
277
  if (!nodesValidation.valid) throw nodesValidation.error;
221
- const result = applyUpdateAction(normalizedPaths, actionElement.update, targetElement, options);
278
+ const result = applyUpdateAction(normalizedPaths, actionElement.update, workingElement, mutatingOptions);
222
279
  tracer?.action({
223
280
  target: targetExpression,
224
281
  type: 'update',
@@ -233,7 +290,7 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
233
290
  const nodesValidation = (0, _validate.validateTargetNodes)(matches);
234
291
  if (!nodesValidation.valid) throw nodesValidation.error;
235
292
  const copyExpression = (0, _apidomCore.toValue)(actionElement.copy);
236
- const result = applyCopyAction(normalizedPaths, copyExpression, targetElement, options);
293
+ const result = applyCopyAction(normalizedPaths, copyExpression, workingElement, mutatingOptions);
237
294
  tracer?.action({
238
295
  target: targetExpression,
239
296
  type: 'copy',
@@ -248,7 +305,7 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
248
305
  matchCount: matches.length,
249
306
  normalizedPaths
250
307
  });
251
- return targetElement;
308
+ return workingElement;
252
309
  } catch (error) {
253
310
  // enrich OverlayError with action context, preserving original stack
254
311
  if (error instanceof _OverlayError.default && !error.action) {
@@ -268,7 +325,22 @@ const applyAction = (actionElement, targetElement, options = defaultApplyOptions
268
325
 
269
326
  /**
270
327
  * Applies an entire overlay document to a target element.
271
- * Immutable — returns a new element with all actions applied sequentially.
328
+ *
329
+ * Immutable by default. The target is cloned once up front and all actions are
330
+ * applied to the clone sequentially, so the returned element is always
331
+ * independent of the input, including when no action changes anything. Pass
332
+ * `immutable: false` to apply the actions in place; the one shape that cannot
333
+ * be applied in place is a `target: $` action replacing a primitive root with
334
+ * a differently typed value, so the return value is authoritative in both
335
+ * modes.
336
+ *
337
+ * When the target is a `ParseResultElement`, a new parse result is returned
338
+ * carrying the new tree as its result element; the caller's parse result and
339
+ * its result element are left untouched. Every other child — annotations, and
340
+ * an `api` element that is not the result element itself — is shared with the
341
+ * input rather than copied, since no overlay action can reach it.
342
+ * Under `immutable: false` the caller's parse result is updated in place
343
+ * instead, with its result element replaced by the new tree.
272
344
  *
273
345
  * @public
274
346
  */
@@ -278,31 +350,73 @@ const applyOverlay = (overlayElement, targetElement, options = defaultApplyOptio
278
350
  if (!(0, _apidomNsOverlay.isOverlay1Element)(overlay)) {
279
351
  throw new _OverlayError.default('First argument must be an Overlay 1.x document');
280
352
  }
353
+ const opts = withDefaults(options);
354
+ let parseResult;
355
+ let resultElement;
281
356
  let target;
282
357
  if (targetElement instanceof _apidomDatamodel.ParseResultElement) {
283
358
  if (targetElement.result === undefined) {
284
359
  throw new _OverlayError.default('Target ParseResultElement contains no result element');
285
360
  }
286
- target = targetElement.result;
361
+ /**
362
+ * Only the result element changes, so the wrapper is cloned shallowly: the
363
+ * clone gets its own content list, which the new tree replaces the result
364
+ * element in, and every child the overlay never touches is shared with the
365
+ * input instead of copied.
366
+ */
367
+ parseResult = opts.immutable ? (0, _apidomDatamodel.cloneShallow)(targetElement) : targetElement;
368
+ resultElement = parseResult.result;
369
+ target = resultElement;
287
370
  } else {
288
371
  target = targetElement;
289
372
  }
290
- if (!(0, _apidomDatamodel.isArrayElement)(overlay.actions)) {
291
- return targetElement;
292
- }
293
- const tracer = options.trace ? new _TraceBuilder.default(options.trace) : undefined;
373
+ target = opts.immutable ? (0, _apidomDatamodel.cloneDeep)(target) : target;
374
+
375
+ /**
376
+ * Constructed only once the target has been validated: the constructor writes
377
+ * a success verdict into the caller's trace object, which must not survive a
378
+ * throw from the checks above.
379
+ */
380
+ const tracer = opts.trace ? new _TraceBuilder.default(opts.trace) : undefined;
294
381
  const internalOptions = tracer ? {
295
- ...options,
382
+ ...opts,
296
383
  tracer
297
- } : options;
298
- for (const action of overlay.actions) {
299
- target = applyAction(action, target, internalOptions);
384
+ } : opts;
385
+ // clone once, then apply the actions mutably to the clone
386
+ const mutatingOptions = {
387
+ ...internalOptions,
388
+ immutable: false
389
+ };
390
+ if ((0, _apidomDatamodel.isArrayElement)(overlay.actions)) {
391
+ for (const action of overlay.actions) {
392
+ target = applyAction(action, target, mutatingOptions);
393
+ }
300
394
  }
301
- if (targetElement instanceof _apidomDatamodel.ParseResultElement) {
302
- if (target !== targetElement.result) {
303
- targetElement.replaceResult(target);
395
+ if (parseResult !== undefined) {
396
+ /**
397
+ * The result marker is a class on the element itself, and replacing a
398
+ * primitive root swaps in the update value's meta wholesale, so an action
399
+ * can strip it. Restore it before the element goes back into the wrapper,
400
+ * otherwise the returned parse result has no result element at all.
401
+ */
402
+ if (!(0, _apidomDatamodel.includesClasses)(target, ['result'])) {
403
+ target.classes.push('result');
404
+ }
405
+
406
+ /**
407
+ * replaceResult locates the slot by scanning for the marker, so the element
408
+ * being replaced has to carry one too. An earlier action that stripped the
409
+ * marker off the element still sitting in the wrapper would otherwise leave
410
+ * that lookup with nothing to find, and the new tree would be dropped on
411
+ * the floor.
412
+ */
413
+ if (target !== resultElement) {
414
+ if (!(0, _apidomDatamodel.includesClasses)(resultElement, ['result'])) {
415
+ resultElement.classes.push('result');
416
+ }
417
+ parseResult.replaceResult(target);
304
418
  }
305
- return targetElement;
419
+ return parseResult;
306
420
  }
307
421
  return target;
308
422
  };
@@ -1,4 +1,4 @@
1
- import { isObjectElement, isArrayElement, cloneDeep, ParseResultElement } from '@speclynx/apidom-datamodel';
1
+ import { isObjectElement, isArrayElement, includesClasses, cloneDeep, cloneShallow, ParseResultElement } from '@speclynx/apidom-datamodel';
2
2
  import { deepmerge, toValue } from '@speclynx/apidom-core';
3
3
  import { evaluate, NormalizedPath } from '@speclynx/apidom-json-path';
4
4
  import { isOverlay1Element } from '@speclynx/apidom-ns-overlay-1';
@@ -16,6 +16,18 @@ const defaultApplyOptions = {
16
16
  strict: false
17
17
  };
18
18
 
19
+ /**
20
+ * Merges caller options over the defaults. A partial options object leaves the
21
+ * fields it omits undefined, which must still mean the documented defaults.
22
+ * `immutable` is spelled out because it is the only default that is not falsy,
23
+ * so a caller forwarding an undefined value must not end up mutating in place.
24
+ */
25
+ const withDefaults = options => ({
26
+ ...defaultApplyOptions,
27
+ ...options,
28
+ immutable: options.immutable ?? defaultApplyOptions.immutable
29
+ });
30
+
19
31
  /**
20
32
  * Default customMerge that enforces Overlay spec type compatibility.
21
33
  * Throws OverlayError on incompatible type combinations.
@@ -76,12 +88,37 @@ const resolveParent = (normalizedPath, root) => {
76
88
  * @public
77
89
  */
78
90
  export const applyUpdateAction = (normalizedPaths, updateValue, targetElement, options = defaultApplyOptions) => {
79
- let result = options.immutable ? cloneDeep(targetElement) : targetElement;
91
+ const opts = withDefaults(options);
92
+ let result = opts.immutable ? cloneDeep(targetElement) : targetElement;
80
93
  for (const normalizedPath of normalizedPaths) {
94
+ /**
95
+ * Every matched node gets its own copy of the update value. deepmerge
96
+ * shallow clones members, so the merged result shares key elements and
97
+ * non-mergeable values with its source regardless of the clone option —
98
+ * without this each target would alias every other target, and a copy
99
+ * action would alias the copy source in the target document.
100
+ */
101
+ const clonedUpdateValue = cloneDeep(updateValue);
81
102
  const resolved = resolveParent(normalizedPath, result);
82
103
  if (resolved === null) {
83
- // targeting root
84
- result = mergeValue(result, updateValue, options);
104
+ /**
105
+ * Targeting root. There is no parent slot to write the merged element
106
+ * into, so the merge is transplanted back onto the target itself — every
107
+ * other target shape updates the document in place and the root must not
108
+ * be the silent exception. Merging always yields the target's own
109
+ * constructor, so the transplant is type-safe. Two cases cannot be
110
+ * written in place and fall back to rebinding: a primitive root replaced
111
+ * by a differently typed value, and a frozen root, whose properties
112
+ * cannot be assigned.
113
+ */
114
+ const merged = mergeValue(result, clonedUpdateValue, opts);
115
+ if (merged.constructor === result.constructor && !result.isFrozen) {
116
+ result.content = merged.content;
117
+ result.meta = merged.meta;
118
+ result.attributes = merged.attributes;
119
+ } else {
120
+ result = merged;
121
+ }
85
122
  continue;
86
123
  }
87
124
  const {
@@ -91,12 +128,12 @@ export const applyUpdateAction = (normalizedPaths, updateValue, targetElement, o
91
128
  if (isObjectElement(parent)) {
92
129
  const current = parent.get(key);
93
130
  if (current !== undefined) {
94
- parent.set(key, mergeValue(current, updateValue, options));
131
+ parent.set(key, mergeValue(current, clonedUpdateValue, opts));
95
132
  }
96
133
  } else if (isArrayElement(parent)) {
97
134
  const current = parent.get(key);
98
135
  if (current !== undefined) {
99
- parent.content[key] = mergeValue(current, updateValue, options);
136
+ parent.content[key] = mergeValue(current, clonedUpdateValue, opts);
100
137
  }
101
138
  }
102
139
  }
@@ -115,7 +152,13 @@ export const applyCopyAction = (normalizedPaths, copyExpression, targetElement,
115
152
  if (sourceMatches.length > 1) {
116
153
  throw new OverlayError(`Copy source JSONPath "${copyExpression}" must select a single node, but selected ${sourceMatches.length}`);
117
154
  }
118
- const sourceValue = sourceMatches[0];
155
+
156
+ /**
157
+ * Snapshot the copy source. Matched targets are updated in document order and
158
+ * the source may sit under one of them, so a live reference would let later
159
+ * targets copy a source that an earlier target already modified.
160
+ */
161
+ const sourceValue = cloneDeep(sourceMatches[0]);
119
162
  return applyUpdateAction(normalizedPaths, sourceValue, targetElement, options);
120
163
  };
121
164
 
@@ -124,7 +167,8 @@ export const applyCopyAction = (normalizedPaths, copyExpression, targetElement,
124
167
  * @public
125
168
  */
126
169
  export const applyRemoveAction = (normalizedPaths, targetElement, options = defaultApplyOptions) => {
127
- const result = options.immutable ? cloneDeep(targetElement) : targetElement;
170
+ const opts = withDefaults(options);
171
+ const result = opts.immutable ? cloneDeep(targetElement) : targetElement;
128
172
  // reverse document order so higher array indices are removed first (avoids index shifting)
129
173
  const sorted = [...normalizedPaths].reverse();
130
174
  for (const normalizedPath of sorted) {
@@ -147,13 +191,19 @@ export const applyRemoveAction = (normalizedPaths, targetElement, options = defa
147
191
 
148
192
  /**
149
193
  * Applies a single overlay action to a target element.
150
- * Immutable — returns a new element with the action applied.
194
+ *
195
+ * Immutable by default. The target is cloned once up front and the action is
196
+ * applied to the clone, so the returned element is always independent of the
197
+ * input, including when the action changes nothing. Pass `immutable: false` to
198
+ * apply the action in place; the one shape that cannot be applied in place is
199
+ * a `target: $` action replacing a primitive root with a differently typed
200
+ * value, so the return value is authoritative in both modes.
151
201
  *
152
202
  * @public
153
203
  */
154
204
  export const applyAction = (actionElement, targetElement, options = defaultApplyOptions) => {
155
- const internalOpts = options;
156
- const tracer = internalOpts.tracer ?? (options.trace ? new TraceBuilder(options.trace) : undefined);
205
+ const opts = withDefaults(options);
206
+ const tracer = opts.tracer ?? (opts.trace ? new TraceBuilder(opts.trace) : undefined);
157
207
  const actionValidation = validateAction(actionElement);
158
208
  if (!actionValidation.valid) throw actionValidation.error;
159
209
  const targetExpression = toValue(actionElement.target);
@@ -166,7 +216,7 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
166
216
  }
167
217
  });
168
218
  if (matches.length === 0) {
169
- if (options.strict) {
219
+ if (opts.strict) {
170
220
  const error = new OverlayError(`Action target "${targetExpression}" matched zero nodes (strict mode)`, {
171
221
  action: actionElement
172
222
  });
@@ -186,13 +236,20 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
186
236
  matchCount: 0,
187
237
  normalizedPaths: []
188
238
  });
189
- return targetElement;
239
+ return opts.immutable ? cloneDeep(targetElement) : targetElement;
190
240
  }
241
+
242
+ // clone once, then apply the action mutably to the clone
243
+ const workingElement = opts.immutable ? cloneDeep(targetElement) : targetElement;
244
+ const mutatingOptions = {
245
+ ...opts,
246
+ immutable: false
247
+ };
191
248
  try {
192
249
  // remove action
193
250
  const removeValue = actionElement.removeField;
194
251
  if (removeValue !== undefined && toValue(removeValue) === true) {
195
- const result = applyRemoveAction(normalizedPaths, targetElement, options);
252
+ const result = applyRemoveAction(normalizedPaths, workingElement, mutatingOptions);
196
253
  tracer?.action({
197
254
  target: targetExpression,
198
255
  type: 'remove',
@@ -206,7 +263,7 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
206
263
  if (actionElement.update !== undefined) {
207
264
  const nodesValidation = validateTargetNodes(matches);
208
265
  if (!nodesValidation.valid) throw nodesValidation.error;
209
- const result = applyUpdateAction(normalizedPaths, actionElement.update, targetElement, options);
266
+ const result = applyUpdateAction(normalizedPaths, actionElement.update, workingElement, mutatingOptions);
210
267
  tracer?.action({
211
268
  target: targetExpression,
212
269
  type: 'update',
@@ -221,7 +278,7 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
221
278
  const nodesValidation = validateTargetNodes(matches);
222
279
  if (!nodesValidation.valid) throw nodesValidation.error;
223
280
  const copyExpression = toValue(actionElement.copy);
224
- const result = applyCopyAction(normalizedPaths, copyExpression, targetElement, options);
281
+ const result = applyCopyAction(normalizedPaths, copyExpression, workingElement, mutatingOptions);
225
282
  tracer?.action({
226
283
  target: targetExpression,
227
284
  type: 'copy',
@@ -236,7 +293,7 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
236
293
  matchCount: matches.length,
237
294
  normalizedPaths
238
295
  });
239
- return targetElement;
296
+ return workingElement;
240
297
  } catch (error) {
241
298
  // enrich OverlayError with action context, preserving original stack
242
299
  if (error instanceof OverlayError && !error.action) {
@@ -256,7 +313,22 @@ export const applyAction = (actionElement, targetElement, options = defaultApply
256
313
 
257
314
  /**
258
315
  * Applies an entire overlay document to a target element.
259
- * Immutable — returns a new element with all actions applied sequentially.
316
+ *
317
+ * Immutable by default. The target is cloned once up front and all actions are
318
+ * applied to the clone sequentially, so the returned element is always
319
+ * independent of the input, including when no action changes anything. Pass
320
+ * `immutable: false` to apply the actions in place; the one shape that cannot
321
+ * be applied in place is a `target: $` action replacing a primitive root with
322
+ * a differently typed value, so the return value is authoritative in both
323
+ * modes.
324
+ *
325
+ * When the target is a `ParseResultElement`, a new parse result is returned
326
+ * carrying the new tree as its result element; the caller's parse result and
327
+ * its result element are left untouched. Every other child — annotations, and
328
+ * an `api` element that is not the result element itself — is shared with the
329
+ * input rather than copied, since no overlay action can reach it.
330
+ * Under `immutable: false` the caller's parse result is updated in place
331
+ * instead, with its result element replaced by the new tree.
260
332
  *
261
333
  * @public
262
334
  */
@@ -265,31 +337,73 @@ export const applyOverlay = (overlayElement, targetElement, options = defaultApp
265
337
  if (!isOverlay1Element(overlay)) {
266
338
  throw new OverlayError('First argument must be an Overlay 1.x document');
267
339
  }
340
+ const opts = withDefaults(options);
341
+ let parseResult;
342
+ let resultElement;
268
343
  let target;
269
344
  if (targetElement instanceof ParseResultElement) {
270
345
  if (targetElement.result === undefined) {
271
346
  throw new OverlayError('Target ParseResultElement contains no result element');
272
347
  }
273
- target = targetElement.result;
348
+ /**
349
+ * Only the result element changes, so the wrapper is cloned shallowly: the
350
+ * clone gets its own content list, which the new tree replaces the result
351
+ * element in, and every child the overlay never touches is shared with the
352
+ * input instead of copied.
353
+ */
354
+ parseResult = opts.immutable ? cloneShallow(targetElement) : targetElement;
355
+ resultElement = parseResult.result;
356
+ target = resultElement;
274
357
  } else {
275
358
  target = targetElement;
276
359
  }
277
- if (!isArrayElement(overlay.actions)) {
278
- return targetElement;
279
- }
280
- const tracer = options.trace ? new TraceBuilder(options.trace) : undefined;
360
+ target = opts.immutable ? cloneDeep(target) : target;
361
+
362
+ /**
363
+ * Constructed only once the target has been validated: the constructor writes
364
+ * a success verdict into the caller's trace object, which must not survive a
365
+ * throw from the checks above.
366
+ */
367
+ const tracer = opts.trace ? new TraceBuilder(opts.trace) : undefined;
281
368
  const internalOptions = tracer ? {
282
- ...options,
369
+ ...opts,
283
370
  tracer
284
- } : options;
285
- for (const action of overlay.actions) {
286
- target = applyAction(action, target, internalOptions);
371
+ } : opts;
372
+ // clone once, then apply the actions mutably to the clone
373
+ const mutatingOptions = {
374
+ ...internalOptions,
375
+ immutable: false
376
+ };
377
+ if (isArrayElement(overlay.actions)) {
378
+ for (const action of overlay.actions) {
379
+ target = applyAction(action, target, mutatingOptions);
380
+ }
287
381
  }
288
- if (targetElement instanceof ParseResultElement) {
289
- if (target !== targetElement.result) {
290
- targetElement.replaceResult(target);
382
+ if (parseResult !== undefined) {
383
+ /**
384
+ * The result marker is a class on the element itself, and replacing a
385
+ * primitive root swaps in the update value's meta wholesale, so an action
386
+ * can strip it. Restore it before the element goes back into the wrapper,
387
+ * otherwise the returned parse result has no result element at all.
388
+ */
389
+ if (!includesClasses(target, ['result'])) {
390
+ target.classes.push('result');
391
+ }
392
+
393
+ /**
394
+ * replaceResult locates the slot by scanning for the marker, so the element
395
+ * being replaced has to carry one too. An earlier action that stripped the
396
+ * marker off the element still sitting in the wrapper would otherwise leave
397
+ * that lookup with nothing to find, and the new tree would be dropped on
398
+ * the floor.
399
+ */
400
+ if (target !== resultElement) {
401
+ if (!includesClasses(resultElement, ['result'])) {
402
+ resultElement.classes.push('result');
403
+ }
404
+ parseResult.replaceResult(target);
291
405
  }
292
- return targetElement;
406
+ return parseResult;
293
407
  }
294
408
  return target;
295
409
  };
@@ -11,15 +11,27 @@ var _apidom = require("./apidom.cjs");
11
11
  /**
12
12
  * Applies a single overlay action (POJO) to a target (POJO).
13
13
  *
14
+ * The refracted target never escapes this function and the result is converted
15
+ * straight back to a POJO, so it is applied in place.
16
+ *
14
17
  * @public
15
18
  */
16
- const applyAction = (action, target, options) => (0, _apidomCore.toValue)((0, _apidom.applyAction)((0, _apidomNsOverlay.refractAction)(action), (0, _apidomDatamodel.refract)(target), options));
19
+ const applyAction = (action, target, options) => (0, _apidomCore.toValue)((0, _apidom.applyAction)((0, _apidomNsOverlay.refractAction)(action), (0, _apidomDatamodel.refract)(target), {
20
+ ...options,
21
+ immutable: false
22
+ }));
17
23
 
18
24
  /**
19
25
  * Applies an entire overlay document (POJO) to a target (POJO).
20
26
  *
27
+ * The refracted target never escapes this function and the result is converted
28
+ * straight back to a POJO, so it is applied in place.
29
+ *
21
30
  * @public
22
31
  */
23
32
  exports.applyAction = applyAction;
24
- const applyOverlay = (overlay, target, options) => (0, _apidomCore.toValue)((0, _apidom.applyOverlay)((0, _apidomNsOverlay.refractOverlay1)(overlay), (0, _apidomDatamodel.refract)(target), options));
33
+ const applyOverlay = (overlay, target, options) => (0, _apidomCore.toValue)((0, _apidom.applyOverlay)((0, _apidomNsOverlay.refractOverlay1)(overlay), (0, _apidomDatamodel.refract)(target), {
34
+ ...options,
35
+ immutable: false
36
+ }));
25
37
  exports.applyOverlay = applyOverlay;