• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

homer0 / packages / 5302605086

pending completion
5302605086

push

github

homer0
chore(monorepo): remove npm lockfile

720 of 720 branches covered (100.0%)

Branch coverage included in aggregate %.

2027 of 2027 relevant lines covered (100.0%)

63.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/packages/public/deep-assign/src/deepAssign.ts
1
export type DeepAssignArrayMode = 'merge' | 'shallowMerge' | 'concat' | 'overwrite';
2

3
export type DeepAssignOptions = {
4
  /**
5
   * Defines how array assignments should be handled.
6
   */
7
  arrayMode: DeepAssignArrayMode;
8
};
9

10
/**
11
 * It allows for deep merge (and copy) of objects and arrays using native spread syntax.
12
 *
13
 * This class exists just to scope the different functionalities and options needed for the
14
 * {@link DeepAssign#assign} method to work.
15
 */
16
export class DeepAssign {
2✔
17
  /**
18
   * Shortcut method to create a new instance and get its `assign` method.
19
   *
20
   * @param options  The options for the class constructor.
21
   */
22
  static fn(options: Partial<DeepAssignOptions> = {}): DeepAssign['assign'] {
2✔
23
    return new DeepAssign(options).assign;
5✔
24
  }
25
  /**
26
   * The options that define how {@link DeepAssign#assign} works.
27
   */
28
  readonly options: DeepAssignOptions;
22✔
29
  /**
30
   * @param options  Custom options for how the assignation it's going to work.
31
   * @throws {Error} If `options.arrayMode` is not a valid {@link DeepAssignArrayMode}.
32
   */
33
  constructor(options: Partial<DeepAssignOptions> = {}) {
12✔
34
    if (
22✔
35
      options.arrayMode &&
30✔
36
      !['merge', 'concat', 'overwrite', 'shallowMerge'].includes(options.arrayMode)
37
    ) {
38
      throw new Error(`Invalid array mode received: \`${options.arrayMode}\``);
1✔
39
    }
40

41
    this.options = {
21✔
42
      arrayMode: 'merge',
43
      ...options,
44
    };
45

46
    /**
47
     * @ignore
48
     */
49
    this.assign = this.assign.bind(this);
21✔
50
  }
51
  /**
52
   * Makes a deep merge of a list of objects and/or arrays.
53
   *
54
   * @param targets  The objects to merge; if one of them is not an object nor an
55
   *                 array, it will be ignored.
56
   * @template T  The type of the object that will be returned.
57
   * @throws {Error} If no targets are sent.
58
   */
59
  assign<T = unknown>(...targets: unknown[]): T {
60
    if (!targets.length) {
22✔
61
      throw new Error('No targets received');
1✔
62
    }
63

64
    return targets
21✔
65
      .filter((target) => this.isValidItem(target))
43✔
66
      .reduce(
67
        (acc, target) =>
68
          acc === null
42✔
69
            ? this.resolveFromEmpty(target, true)
70
            : this.resolve(acc, target, true),
71
        null,
72
      ) as T;
73
  }
74
  /**
75
   * Checks if an object is a plain `Object` and not an instance of some class.
76
   *
77
   * @param target  The object to validate.
78
   */
79
  protected isPlainObject(target: unknown): boolean {
80
    return target !== null && Object.getPrototypeOf(target).constructor.name === 'Object';
349✔
81
  }
82
  /**
83
   * Checks if an object can be used on a merge: only arrays and plain objects are
84
   * supported.
85
   *
86
   * @param target  The object to validate.
87
   */
88
  protected isValidItem(target: unknown): boolean {
89
    return Array.isArray(target) || this.isPlainObject(target);
43✔
90
  }
91
  /**
92
   * Merges two arrays into a new one. If the `concatArrays` option was set to `true` on
93
   * the constructor, the result will just be a concatenation with new references for the
94
   * items; but if the option was set to `false`, then the arrays will be merged over
95
   * their indexes.
96
   *
97
   * @param source  The base array.
98
   * @param target  The array that will be merged on top of `source`.
99
   * @param mode    The assignment strategy.
100
   */
101
  protected mergeArrays(
102
    source: unknown[],
103
    target: unknown[],
104
    mode: DeepAssignArrayMode,
105
  ): unknown[] {
106
    let result: unknown[];
107
    if (mode === 'concat') {
24✔
108
      result = [...source, ...target].map((targetItem) =>
4✔
109
        this.resolveFromEmpty(targetItem),
16✔
110
      );
111
    } else if (mode === 'overwrite') {
20✔
112
      result = target.slice().map((targetItem) => this.resolveFromEmpty(targetItem));
10✔
113
    } else if (mode === 'shallowMerge') {
16✔
114
      result = source.slice();
4✔
115
      target.forEach((targetItem, index) => {
4✔
116
        const resolved = this.resolveFromEmpty(targetItem);
10✔
117
        if (index < result.length) {
10✔
118
          result[index] = resolved;
4✔
119
        } else {
120
          result.push(this.resolveFromEmpty(targetItem));
6✔
121
        }
122
      });
123
    } else {
124
      result = source.slice();
12✔
125
      target.forEach((targetItem, index) => {
12✔
126
        if (index < result.length) {
28✔
127
          result[index] = this.resolve(result[index], targetItem);
10✔
128
        } else {
129
          result.push(this.resolveFromEmpty(targetItem));
18✔
130
        }
131
      });
132
    }
133

134
    return result;
24✔
135
  }
136
  /**
137
   * Merges two plain objects and their children.
138
   *
139
   * @param source  The base object.
140
   * @param target  The object which properties will be merged in top of `source`.
141
   */
142
  protected mergeObjects(source: unknown, target: unknown): unknown {
143
    const useSource = source as Record<string | symbol, unknown>;
58✔
144
    const useTarget = target as Record<string | symbol, unknown>;
58✔
145

146
    const keys = [...Object.getOwnPropertySymbols(useTarget), ...Object.keys(useTarget)];
58✔
147

148
    const subMerge = keys.reduce<Record<string | symbol, unknown>>(
58✔
149
      (acc, key) => ({
133✔
150
        ...acc,
151
        [key]: this.resolve(useSource[key], useTarget[key]),
152
      }),
153
      {},
154
    );
155

156
    return { ...useSource, ...useTarget, ...subMerge };
58✔
157
  }
158
  /**
159
   * This is the method the class calls when it has to merge two objects and it doesn't
160
   * know which types they are; the method takes care of validating compatibility and
161
   * calling either {@link DeepAssign#mergeObjects} or {@link DeepAssign#mergeArrays}.
162
   * If the objects are not compatible, or `source` is not defined, it will return a copy
163
   * of `target`.
164
   *
165
   * @param source           The base object.
166
   * @param target           The object that will be merged in top of `source`.
167
   * @param ignoreArrayMode  Whether or not to ignore the option that tells the class
168
   *                         how array assignments should be handled. This parameter
169
   *                         exists because, when called directly from
170
   *                         {@link DeepAssign#assign}, it doesn't make sense to use a
171
   *                         strategy different than 'merge'.
172
   */
173
  protected resolve(source: unknown, target: unknown, ignoreArrayMode = false): unknown {
178✔
174
    let result: unknown;
175
    const targetIsUndefined = typeof target === 'undefined';
212✔
176
    const sourceIsUndefined = typeof source === 'undefined';
212✔
177
    if (!targetIsUndefined && !sourceIsUndefined) {
212✔
178
      if (Array.isArray(target) && Array.isArray(source)) {
110✔
179
        const { arrayMode } = this.options;
24✔
180
        const useMode =
181
          ignoreArrayMode && !['merge', 'shallowMerge'].includes(arrayMode)
24✔
182
            ? 'merge'
183
            : arrayMode;
184
        result = this.mergeArrays(source, target, useMode);
24✔
185
      } else if (this.isPlainObject(target) && this.isPlainObject(source)) {
86✔
186
        result = this.mergeObjects(source, target);
58✔
187
      } else {
188
        result = target;
28✔
189
      }
190
    } else if (!targetIsUndefined) {
102✔
191
      result = this.resolveFromEmpty(target);
100✔
192
    }
193

194
    return result;
212✔
195
  }
196
  /**
197
   * This method is a helper for {@link DeepAssign#resolve}, and it's used for when the
198
   * class has the `target` but not the `source`: depending on the type of the `target`,
199
   * it calls resolves with an empty object of the same type; if the `target` can't be
200
   * merged, it just returns it as it was received, which means that is a type that
201
   * doesn't hold references.
202
   *
203
   * @param target                   The target to copy.
204
   * @param [ignoreArrayMode=false]  Whether or not to ignore the option that tells the
205
   *                                 class how array assignments should be handled.
206
   *                                 This parameter exists because, when called
207
   *                                 directly from {@link DeepAssign#assign}, it
208
   *                                 doesn't make sense to use a strategy different
209
   *                                 than 'merge'.
210
   */
211
  protected resolveFromEmpty(target: unknown, ignoreArrayMode = false): unknown {
160✔
212
    let result: unknown;
213
    if (Array.isArray(target)) {
181✔
214
      result = this.resolve([], target, ignoreArrayMode);
13✔
215
    } else if (this.isPlainObject(target)) {
168✔
216
      result = this.resolve({}, target);
35✔
217
    } else {
218
      result = target;
133✔
219
    }
220

221
    return result;
181✔
222
  }
223
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc