@lowdefy/helpers 5.5.1 → 5.6.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.
@@ -16,62 +16,121 @@
16
16
  const proto = Object.getPrototypeOf(val);
17
17
  return proto === Object.prototype || proto === null;
18
18
  }
19
+ // `||`, not `??`: an anonymous class has the empty string as its constructor
20
+ // name, not undefined, and a marker whose only payload is the type name must
21
+ // not be emitted as '[Object: ]'.
22
+ function classInstanceMarker(val) {
23
+ return `[Object: ${val.constructor?.name || 'unknown'}]`;
24
+ }
25
+ // A function is never passed through: `get` would hand a live closure over
26
+ // server state to app config. (serializer.copy only throws on a function at
27
+ // the top level; nested, it silently drops the key or turns it into null in
28
+ // an array - since this output is always an object, that vanished key, not a
29
+ // throw, was the real pre-fix failure.) Same empty-string fallback as
30
+ // classInstanceMarker - an anonymous function's `name` is '' rather than
31
+ // undefined.
32
+ function functionMarker(val) {
33
+ return `[Function: ${val.name || 'unknown'}]`;
34
+ }
35
+ // A bigint's digits are its whole content, not internals, so the marker
36
+ // includes the value rather than just naming the type.
37
+ function bigintMarker(val) {
38
+ return `[BigInt: ${val}]`;
39
+ }
40
+ // `.description` gives the bare description; `String(sym)` would wrap it as
41
+ // 'Symbol(description)', polluting the marker's payload. Same empty-string
42
+ // fallback as the other markers - a descriptionless symbol's `description` is
43
+ // undefined, and `Symbol('')`'s is '', so both must fall back to 'unknown'.
44
+ function symbolMarker(val) {
45
+ return `[Symbol: ${val.description || 'unknown'}]`;
46
+ }
19
47
  const MAX_CAUSE_DEPTH = 3;
20
48
  const MAX_OBJECT_DEPTH = 5;
21
- function cleanValue(val, seen, objectDepth, causeDepth) {
49
+ const NO_OMIT = [];
50
+ function cleanValue(val, seen, objectDepth, causeDepth, omit) {
22
51
  if (objectDepth > MAX_OBJECT_DEPTH) return '[Truncated]';
52
+ if (typeof val === 'function') return functionMarker(val);
53
+ if (typeof val === 'bigint') return bigintMarker(val);
54
+ if (typeof val === 'symbol') return symbolMarker(val);
23
55
  if (val === null || typeof val !== 'object') return val;
24
56
  if (seen.has(val)) return '[Circular]';
25
57
  seen.add(val);
26
58
  if (Array.isArray(val)) {
27
- return val.map((item)=>cleanValue(item, seen, objectDepth + 1, causeDepth));
59
+ return val.map((item)=>cleanValue(item, seen, objectDepth + 1, causeDepth, omit));
28
60
  }
29
61
  if (val instanceof Date) return val;
30
- if (val instanceof Error) return _extractErrorProps(val, seen, objectDepth, causeDepth);
31
- if (!isPlainObject(val)) return `[Object: ${val.constructor?.name ?? 'unknown'}]`;
62
+ if (val instanceof Error) return _extractErrorProps(val, seen, objectDepth, causeDepth, omit);
63
+ if (!isPlainObject(val)) return classInstanceMarker(val);
32
64
  const cleaned = {};
33
65
  for (const [k, v] of Object.entries(val)){
34
- const cv = cleanValue(v, seen, objectDepth + 1, causeDepth);
66
+ const cv = cleanValue(v, seen, objectDepth + 1, causeDepth, omit);
35
67
  if (cv !== undefined) cleaned[k] = cv;
36
68
  }
37
69
  return cleaned;
38
70
  }
39
- function _extractErrorProps(err, seen, objectDepth, causeDepth) {
71
+ function _extractErrorProps(err, seen, objectDepth, causeDepth, omit) {
40
72
  if (!err) return err;
41
73
  seen.add(err);
42
- const props = {
43
- message: err.message,
44
- name: err.name,
45
- stack: err.stack
46
- };
47
- if (err.cause !== undefined) {
48
- if (err.cause instanceof Error && !seen.has(err.cause) && causeDepth < MAX_CAUSE_DEPTH) {
49
- props.cause = _extractErrorProps(err.cause, seen, objectDepth, causeDepth + 1);
50
- } else if (!(err.cause instanceof Error)) {
51
- props.cause = cleanValue(err.cause, seen, objectDepth + 1, causeDepth);
74
+ // Called at every error node rather than once for the walk, so a caller's
75
+ // policy can key on the node itself - its class, or its own cause value.
76
+ // Which fields an audience may see is the caller's decision, never this
77
+ // walk's; this function only applies the keys it is handed. Omitted fields
78
+ // are skipped before they are built, so a dropped cause is never deep-copied
79
+ // and never marked in `seen`.
80
+ const omitted = omit?.(err) ?? NO_OMIT;
81
+ const props = {};
82
+ if (!omitted.includes('message')) props.message = err.message;
83
+ if (!omitted.includes('name')) props.name = err.name;
84
+ if (!omitted.includes('stack')) props.stack = err.stack;
85
+ if (err.cause !== undefined && !omitted.includes('cause')) {
86
+ if (!(err.cause instanceof Error)) {
87
+ props.cause = cleanValue(err.cause, seen, objectDepth + 1, causeDepth, omit);
88
+ } else if (seen.has(err.cause)) {
89
+ props.cause = '[Circular]';
90
+ } else if (causeDepth >= MAX_CAUSE_DEPTH) {
91
+ props.cause = '[Truncated]';
92
+ } else {
93
+ props.cause = _extractErrorProps(err.cause, seen, objectDepth, causeDepth + 1, omit);
52
94
  }
53
95
  }
54
96
  for (const key of Object.keys(err)){
55
- if (key === 'cause') continue;
97
+ if (key === 'cause' || omitted.includes(key)) continue;
56
98
  const value = err[key];
57
- if (value === null || typeof value !== 'object') {
99
+ if (typeof value === 'function') {
100
+ props[key] = functionMarker(value);
101
+ } else if (typeof value === 'bigint') {
102
+ props[key] = bigintMarker(value);
103
+ } else if (typeof value === 'symbol') {
104
+ props[key] = symbolMarker(value);
105
+ } else if (value === null || typeof value !== 'object') {
58
106
  props[key] = value;
59
107
  } else if (value instanceof Date) {
60
108
  props[key] = value;
61
109
  } else if (Array.isArray(value)) {
62
- props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth);
110
+ props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth, omit);
63
111
  } else if (value instanceof Error) {
64
- if (!seen.has(value)) {
65
- props[key] = _extractErrorProps(value, seen, objectDepth, causeDepth);
112
+ if (seen.has(value)) {
113
+ props[key] = '[Circular]';
114
+ } else {
115
+ props[key] = _extractErrorProps(value, seen, objectDepth, causeDepth, omit);
66
116
  }
67
117
  } else if (isPlainObject(value)) {
68
- props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth);
118
+ props[key] = cleanValue(value, seen, objectDepth + 1, causeDepth, omit);
119
+ } else {
120
+ // A class instance (Socket, Agent, ClientRequest, ...). Its internals are never exposed —
121
+ // only the type name — but the key must not vanish: an absent key is indistinguishable
122
+ // from "no such field", and this output is now read by `get` as app-visible config data.
123
+ props[key] = classInstanceMarker(value);
69
124
  }
70
- // Skip class instances (Socket, Agent, ClientRequest, etc.)
71
125
  }
72
126
  return props;
73
127
  }
74
- function extractErrorProps(err) {
75
- return _extractErrorProps(err, new Set(), 0, 0);
128
+ // omit: (err) => string[] - the field names to leave off THIS error node, called
129
+ // once per node. Returning 'message' or 'name' is legal but degrades the wire
130
+ // format rather than just trimming it: serializer's propsToError revives a cause
131
+ // as an Error only when it has a `message`, and looks the Lowdefy error class up
132
+ // by `name`. Omit either and a round-trip yields plain objects.
133
+ function extractErrorProps(err, { omit } = {}) {
134
+ return _extractErrorProps(err, new Set(), 0, 0, omit);
76
135
  }
77
136
  export default extractErrorProps;
package/dist/get.js CHANGED
@@ -32,95 +32,86 @@
32
32
  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
33
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34
34
  // THE SOFTWARE.
35
- import typeTest from './type.js';
35
+ import type from './type.js';
36
+ import extractErrorProps from './extractErrorProps.js';
36
37
  import serializer from './serializer.js';
37
- function join(segs, joinChar, options) {
38
- if (typeof options.join === 'function') {
39
- return options.join(segs);
40
- }
41
- return segs[0] + joinChar + segs[1];
42
- }
43
- function split(path, splitChar, options) {
44
- if (typeof options.split === 'function') {
45
- return options.split(path);
46
- }
47
- return path.split(splitChar);
38
+ import splitPath from './splitPath.js';
39
+ import { isReserved, ReservedKeyError } from './ReservedKeyError.js';
40
+ // "May I step into this value to reach a child?" Plain objects, arrays and errors are traversable;
41
+ // functions, Date, URL, Map, Set, RegExp, Promise, Buffer and typed arrays are not, because
42
+ // serializing their internals is information disclosure. Class instances are NOT excluded:
43
+ // type.isObject cannot distinguish them from plain objects, since kindOf maps every
44
+ // `[object Object]` tag to 'object', so an instance's own properties are reachable. Narrowing the
45
+ // predicate is a cross-cutting decision - mergeObjects sits on the same type.isObject boundary -
46
+ // and is tracked separately. Note the predicate cannot close the disclosure case anyway: when the
47
+ // live object is the endpoint of the path it is returned (and under copy, serialized) without
48
+ // isTraversable ever being consulted. Error is handled by forLookup, which converts it to plain
49
+ // data before a lookup.
50
+ // Kept local rather than shared with set/unset - a module imported by all three trips a Jest ESM
51
+ // module-cache bug in operators-js' mocked re-imports.
52
+ function isTraversable(value) {
53
+ return type.isObject(value) || Array.isArray(value) || value instanceof Error;
48
54
  }
49
- function isValid(key, target, options) {
50
- if (typeof options.isValid === 'function') {
51
- return options.isValid(key, target);
55
+ // A lookup on an Error reads the error's plain-data form, so `name` resolves (it is own on the
56
+ // extracted props, inherited on the instance) and an own key holding a class instance arrives as
57
+ // a marker rather than a live object. Applied only where a lookup happens, so an error that is
58
+ // the endpoint of the path is returned as an Error - `_actions: x.error` is unchanged.
59
+ function forLookup(value) {
60
+ if (value instanceof Error) {
61
+ return extractErrorProps(value);
52
62
  }
53
- return true;
54
- }
55
- function isValidObject(val) {
56
- return typeTest.isObject(val) || Array.isArray(val) || typeof val === 'function';
63
+ return value;
57
64
  }
58
65
  function getter(target, path, options) {
59
- if (typeTest.isNone(path) || !isValidObject(target)) {
60
- return typeof options.default !== 'undefined' ? options.default : undefined;
66
+ if (type.isNone(path) || !isTraversable(target)) {
67
+ return options.default;
61
68
  }
62
- if (typeof path === 'number') {
69
+ if (type.isNumber(path)) {
63
70
  path = String(path);
64
71
  }
65
- const isArray = Array.isArray(path);
66
- const isString = typeof path === 'string';
67
- const splitChar = options.separator || '.';
68
- const joinChar = options.joinChar || (typeof splitChar === 'string' ? splitChar : '.');
69
- if (isString && path in target) {
70
- return isValid(path, target, options) ? target[path] : options.default;
72
+ if (!type.isString(path)) {
73
+ return options.default;
71
74
  }
72
- const segs = isArray ? path : split(path, splitChar, options);
75
+ const segs = splitPath(path);
76
+ // Scanned before the walk so a reserved key is rejected even when it is an own property - the
77
+ // reserved rule is about illegal input, not about what the target holds. This also means a
78
+ // literal dotted key whose segments include a reserved name, such as 'a.constructor', throws
79
+ // rather than resolving; set and unset reject it identically. A rejoined candidate always
80
+ // contains a dot, so it can never equal a reserved name; scanning the split segments stays
81
+ // sufficient.
82
+ const reservedSeg = segs.find(isReserved);
83
+ if (!type.isNone(reservedSeg)) {
84
+ throw new ReservedKeyError(reservedSeg);
85
+ }
86
+ let current = forLookup(target);
73
87
  const len = segs.length;
74
88
  let idx = 0;
75
- do {
76
- let prop = segs[idx];
77
- if (typeof prop === 'number') {
78
- prop = String(prop);
89
+ while(idx < len){
90
+ let candidate = segs[idx];
91
+ let next = idx + 1;
92
+ // The strict segment wins if present. On a miss, grow the candidate one segment at a time
93
+ // looking for a literal dotted key - shortest-first, no backtracking, so segments are
94
+ // consumed monotonically and the walk stays a single forward pass.
95
+ while(!Object.hasOwn(current, candidate) && next < len){
96
+ candidate = `${candidate}.${segs[next]}`;
97
+ next += 1;
79
98
  }
80
- while(prop && prop.slice(-1) === '\\'){
81
- idx += 1;
82
- prop = join([
83
- prop.slice(0, -1),
84
- segs[idx] || ''
85
- ], joinChar, options);
99
+ if (!Object.hasOwn(current, candidate)) {
100
+ return options.default;
86
101
  }
87
- if (prop in target) {
88
- if (!isValid(prop, target, options)) {
89
- return options.default;
90
- }
91
- target = target[prop];
92
- } else {
93
- let hasProp = false;
94
- let n = idx + 1;
95
- while(n < len){
96
- prop = join([
97
- prop,
98
- segs[n]
99
- ], joinChar, options);
100
- n += 1;
101
- hasProp = prop in target;
102
- if (hasProp) {
103
- if (!isValid(prop, target, options)) {
104
- return options.default;
105
- }
106
- target = target[prop];
107
- idx = n - 1;
108
- break;
109
- }
110
- }
111
- if (!hasProp) {
102
+ current = current[candidate];
103
+ idx = next;
104
+ if (idx < len) {
105
+ if (!isTraversable(current)) {
112
106
  return options.default;
113
107
  }
108
+ current = forLookup(current);
114
109
  }
115
- // eslint-disable-next-line no-plusplus
116
- }while (++idx < len && isValidObject(target))
117
- if (idx === len) {
118
- return target;
119
110
  }
120
- return options.default;
111
+ return current;
121
112
  }
122
113
  function get(target, path, options) {
123
- if (!typeTest.isObject(options)) {
114
+ if (!type.isObject(options)) {
124
115
  options = {
125
116
  default: options
126
117
  };
package/dist/getKey.js ADDED
@@ -0,0 +1,36 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import type from './type.js';
16
+ import { isReserved, ReservedKeyError } from './ReservedKeyError.js';
17
+ // Reads a single literal key off a plain object. Dots are not path separators.
18
+ function getKey(target, key, defaultValue) {
19
+ if (!type.isObject(target)) {
20
+ throw new TypeError('getKey: target must be a plain object');
21
+ }
22
+ if (!type.isString(key)) {
23
+ throw new TypeError('getKey: key must be a string');
24
+ }
25
+ // Rejected even on null-proto targets, where the read would be safe: a caller seeing
26
+ // ReservedKeyError always knows the key was rejected, regardless of target shape.
27
+ if (isReserved(key)) {
28
+ throw new ReservedKeyError(key);
29
+ }
30
+ // Own-property read so inherited Object.prototype members never leak as values.
31
+ if (Object.hasOwn(target, key)) {
32
+ return target[key];
33
+ }
34
+ return defaultValue;
35
+ }
36
+ export default getKey;
package/dist/index.js CHANGED
@@ -17,19 +17,25 @@ import builtinMessages from './builtinMessages.js';
17
17
  import cachedPromises from './cachedPromises.js';
18
18
  import extractErrorProps from './extractErrorProps.js';
19
19
  import get from './get.js';
20
+ import getKey from './getKey.js';
20
21
  import getLocaleDateFormat from './getLocaleDateFormat.js';
21
22
  import getLocaleDecimalSeparator from './getLocaleDecimalSeparator.js';
22
23
  import getLocaleGroupSeparator from './getLocaleGroupSeparator.js';
24
+ import { isReserved, ReservedKeyError } from './ReservedKeyError.js';
25
+ import joinPath from './joinPath.js';
23
26
  import LRUCache from './LRUCache.js';
24
27
  import mergeObjects from './mergeObjects.js';
25
28
  import omit from './omit.js';
26
29
  import serializer from './serializer.js';
27
30
  import set from './set.js';
31
+ import setKey from './setKey.js';
32
+ import splitPath from './splitPath.js';
28
33
  import stableStringify from './stableStringify.js';
29
34
  import swap from './swap.js';
30
35
  import translate from './translate.js';
31
36
  import type from './type.js';
32
37
  import unset from './unset.js';
38
+ import unsetKey from './unsetKey.js';
33
39
  import urlQuery from './urlQuery.js';
34
40
  import wait from './wait.js';
35
- export { applyArrayIndices, builtinMessages, cachedPromises, extractErrorProps, get, getLocaleDateFormat, getLocaleDecimalSeparator, getLocaleGroupSeparator, LRUCache, mergeObjects, omit, serializer, set, stableStringify, swap, translate, type, unset, urlQuery, wait };
41
+ export { applyArrayIndices, builtinMessages, cachedPromises, extractErrorProps, get, getKey, getLocaleDateFormat, getLocaleDecimalSeparator, getLocaleGroupSeparator, isReserved, joinPath, LRUCache, mergeObjects, omit, ReservedKeyError, serializer, set, setKey, splitPath, stableStringify, swap, translate, type, unset, unsetKey, urlQuery, wait };
@@ -0,0 +1,24 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import type from './type.js';
16
+ function joinPath(segments) {
17
+ if (!type.isArray(segments)) {
18
+ throw new TypeError(`joinPath: segments must be an array. Received ${JSON.stringify(segments)}.`);
19
+ }
20
+ // Escape the escape character first, then the separator, so a segment ending in a
21
+ // backslash cannot swallow the dot that follows it when splitPath reads it back.
22
+ return segments.map((segment)=>String(segment).replace(/\\/g, '\\\\').replace(/\./g, '\\.')).join('.');
23
+ }
24
+ export default joinPath;
@@ -12,14 +12,62 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ // TODO: do we want this lodash dep in helpers?
16
- import merge from 'lodash.merge';
15
+ */ // Imported from the source module, not index.js, to avoid a circular import.
16
+ import { isReserved } from './ReservedKeyError.js';
17
17
  import type from './type.js';
18
- const mergeObjects = (objects)=>{
19
- let merged = objects;
18
+ // Plain objects and arrays are the config data model and are always copied; every other value is
19
+ // an opaque leaf, shared by reference. This restores what lodash.merge provided - it cloned
20
+ // plain-object and array source values into the destination while aliasing everything else. The
21
+ // split is not "is it a class instance" but type.isObject's own boundary: kindOf sorts a value as
22
+ // a plain object whenever it fails every built-in check and its Symbol.toStringTag tail check
23
+ // reads '[object Object]'. Date, RegExp, Map, Set, Error, Promise, typed arrays, Buffer, URL and
24
+ // any class that sets Symbol.toStringTag are shared by reference; a plain user class is not - it
25
+ // has no such tag, so it is copied and flattened into a bare object, losing its prototype.
26
+ //
27
+ // clone only walks Object.keys/array indices, so it carries own enumerable string keys forward
28
+ // and drops everything else: non-enumerable markers such as the build's `~k`/`~r`/`~l` keys, and
29
+ // symbol keys. Nothing merges those markers today, but a future caller that does would lose them
30
+ // silently. clone also always produces an object with Object.prototype, so a null-prototype
31
+ // object (e.g. Object.create(null), used deliberately for pollution hardening) loses that
32
+ // hardening if it passes through a merge; nothing does today.
33
+ function clone(value) {
34
+ if (type.isArray(value)) {
35
+ return value.map(clone);
36
+ }
37
+ if (!type.isObject(value)) {
38
+ return value;
39
+ }
40
+ const result = {};
41
+ for (const key of Object.keys(value)){
42
+ if (isReserved(key)) continue;
43
+ result[key] = clone(value[key]);
44
+ }
45
+ return result;
46
+ }
47
+ function deepMerge(target, source) {
48
+ const result = {};
49
+ for (const key of Object.keys(target)){
50
+ if (isReserved(key)) continue;
51
+ result[key] = clone(target[key]);
52
+ }
53
+ for (const key of Object.keys(source)){
54
+ // Value-side pollution guard. The reserved name arrives as data inside a merged value,
55
+ // not as a path a developer typed, so dropping it misroutes nothing — skip, don't throw.
56
+ if (isReserved(key)) continue;
57
+ if (type.isObject(result[key]) && type.isObject(source[key])) {
58
+ result[key] = deepMerge(result[key], source[key]);
59
+ } else {
60
+ // Everything that is not a plain object is a leaf, arrays included: a later array
61
+ // replaces an earlier one rather than index-merging into it.
62
+ result[key] = clone(source[key]);
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+ function mergeObjects(objects) {
20
68
  if (type.isArray(objects)) {
21
- merged = merge(...objects.filter((obj)=>type.isObject(obj)));
69
+ return objects.filter((obj)=>type.isObject(obj)).reduce((merged, obj)=>deepMerge(merged, obj), {});
22
70
  }
23
- return merged;
24
- };
71
+ return objects;
72
+ }
25
73
  export default mergeObjects;
@@ -41,7 +41,7 @@ function propsToError(data) {
41
41
  }
42
42
  return error;
43
43
  }
44
- const makeReplacer = (customReplacer, isoStringDates, skipMarkers)=>(key, value)=>{
44
+ const makeReplacer = ({ replacer, isoStringDates, skipMarkers, omitErrorProps } = {})=>(key, value)=>{
45
45
  let dateReplacer = (date)=>({
46
46
  '~d': date.valueOf()
47
47
  });
@@ -51,12 +51,14 @@ const makeReplacer = (customReplacer, isoStringDates, skipMarkers)=>(key, value)
51
51
  });
52
52
  }
53
53
  let newValue = value;
54
- if (customReplacer) {
55
- newValue = customReplacer(key, value);
54
+ if (replacer) {
55
+ newValue = replacer(key, value);
56
56
  }
57
57
  if (type.isError(newValue)) {
58
58
  return {
59
- '~e': extractErrorProps(newValue)
59
+ '~e': extractErrorProps(newValue, {
60
+ omit: omitErrorProps
61
+ })
60
62
  };
61
63
  }
62
64
  if (type.isObject(newValue)) {
@@ -217,7 +219,13 @@ const serialize = (json, options = {})=>{
217
219
  '~d': json.valueOf()
218
220
  };
219
221
  }
220
- return JSON.parse(JSON.stringify(json, makeReplacer(options.replacer, options.isoStringDates)));
222
+ // skipMarkers is deliberately not threaded here - serialize has never applied
223
+ // it, and its callers depend on markers surviving.
224
+ return JSON.parse(JSON.stringify(json, makeReplacer({
225
+ replacer: options.replacer,
226
+ isoStringDates: options.isoStringDates,
227
+ omitErrorProps: options.omitErrorProps
228
+ })));
221
229
  };
222
230
  const serializeToString = (json, options = {})=>{
223
231
  if (type.isUndefined(json)) return json;
@@ -229,11 +237,20 @@ const serializeToString = (json, options = {})=>{
229
237
  }
230
238
  if (options.stable) {
231
239
  return stableStringify(json, {
232
- replacer: makeReplacer(options.replacer, undefined, options.skipMarkers),
240
+ replacer: makeReplacer({
241
+ replacer: options.replacer,
242
+ skipMarkers: options.skipMarkers,
243
+ omitErrorProps: options.omitErrorProps
244
+ }),
233
245
  space: options.space
234
246
  });
235
247
  }
236
- return JSON.stringify(json, makeReplacer(options.replacer, options.isoStringDates, options.skipMarkers), options.space);
248
+ return JSON.stringify(json, makeReplacer({
249
+ replacer: options.replacer,
250
+ isoStringDates: options.isoStringDates,
251
+ skipMarkers: options.skipMarkers,
252
+ omitErrorProps: options.omitErrorProps
253
+ }), options.space);
237
254
  };
238
255
  const deserialize = (json, options = {})=>{
239
256
  if (type.isUndefined(json)) return json;
@@ -246,7 +263,10 @@ const deserializeFromString = (str, options = {})=>{
246
263
  const copy = (json, options = {})=>{
247
264
  if (type.isUndefined(json)) return undefined;
248
265
  if (type.isDate(json)) return new Date(json.valueOf());
249
- return JSON.parse(JSON.stringify(json, makeReplacer(options.replacer)), makeReviver(options.reviver));
266
+ return JSON.parse(JSON.stringify(json, makeReplacer({
267
+ replacer: options.replacer,
268
+ omitErrorProps: options.omitErrorProps
269
+ })), makeReviver(options.reviver));
250
270
  };
251
271
  const serializer = {
252
272
  copy,