UNPKG

5.87 kBPlain TextView Raw
1import {
2 DRAFT_STATE,
3 DRAFTABLE,
4 Objectish,
5 Drafted,
6 AnyObject,
7 AnyMap,
8 AnySet,
9 ImmerState,
10 ArchType,
11 die,
12 StrictMode
13} from "../internal"
14
15export const getPrototypeOf = Object.getPrototypeOf
16
17/** Returns true if the given value is an Immer draft */
18/*#__PURE__*/
19export function isDraft(value: any): boolean {
20 return !!value && !!value[DRAFT_STATE]
21}
22
23/** Returns true if the given value can be drafted by Immer */
24/*#__PURE__*/
25export function isDraftable(value: any): boolean {
26 if (!value) return false
27 return (
28 isPlainObject(value) ||
29 Array.isArray(value) ||
30 !!value[DRAFTABLE] ||
31 !!value.constructor?.[DRAFTABLE] ||
32 isMap(value) ||
33 isSet(value)
34 )
35}
36
37const objectCtorString = Object.prototype.constructor.toString()
38/*#__PURE__*/
39export function isPlainObject(value: any): boolean {
40 if (!value || typeof value !== "object") return false
41 const proto = getPrototypeOf(value)
42 if (proto === null) {
43 return true
44 }
45 const Ctor =
46 Object.hasOwnProperty.call(proto, "constructor") && proto.constructor
47
48 if (Ctor === Object) return true
49
50 return (
51 typeof Ctor == "function" &&
52 Function.toString.call(Ctor) === objectCtorString
53 )
54}
55
56/** Get the underlying object that is represented by the given draft */
57/*#__PURE__*/
58export function original<T>(value: T): T | undefined
59export function original(value: Drafted<any>): any {
60 if (!isDraft(value)) die(15, value)
61 return value[DRAFT_STATE].base_
62}
63
64/**
65 * Each iterates a map, set or array.
66 * Or, if any other kind of object, all of its own properties.
67 * Regardless whether they are enumerable or symbols
68 */
69export function each<T extends Objectish>(
70 obj: T,
71 iter: (key: string | number, value: any, source: T) => void
72): void
73export function each(obj: any, iter: any) {
74 if (getArchtype(obj) === ArchType.Object) {
75 Reflect.ownKeys(obj).forEach(key => {
76 iter(key, obj[key], obj)
77 })
78 } else {
79 obj.forEach((entry: any, index: any) => iter(index, entry, obj))
80 }
81}
82
83/*#__PURE__*/
84export function getArchtype(thing: any): ArchType {
85 const state: undefined | ImmerState = thing[DRAFT_STATE]
86 return state
87 ? state.type_
88 : Array.isArray(thing)
89 ? ArchType.Array
90 : isMap(thing)
91 ? ArchType.Map
92 : isSet(thing)
93 ? ArchType.Set
94 : ArchType.Object
95}
96
97/*#__PURE__*/
98export function has(thing: any, prop: PropertyKey): boolean {
99 return getArchtype(thing) === ArchType.Map
100 ? thing.has(prop)
101 : Object.prototype.hasOwnProperty.call(thing, prop)
102}
103
104/*#__PURE__*/
105export function get(thing: AnyMap | AnyObject, prop: PropertyKey): any {
106 // @ts-ignore
107 return getArchtype(thing) === ArchType.Map ? thing.get(prop) : thing[prop]
108}
109
110/*#__PURE__*/
111export function set(thing: any, propOrOldValue: PropertyKey, value: any) {
112 const t = getArchtype(thing)
113 if (t === ArchType.Map) thing.set(propOrOldValue, value)
114 else if (t === ArchType.Set) {
115 thing.add(value)
116 } else thing[propOrOldValue] = value
117}
118
119/*#__PURE__*/
120export function is(x: any, y: any): boolean {
121 // From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js
122 if (x === y) {
123 return x !== 0 || 1 / x === 1 / y
124 } else {
125 return x !== x && y !== y
126 }
127}
128
129/*#__PURE__*/
130export function isMap(target: any): target is AnyMap {
131 return target instanceof Map
132}
133
134/*#__PURE__*/
135export function isSet(target: any): target is AnySet {
136 return target instanceof Set
137}
138/*#__PURE__*/
139export function latest(state: ImmerState): any {
140 return state.copy_ || state.base_
141}
142
143/*#__PURE__*/
144export function shallowCopy(base: any, strict: StrictMode) {
145 if (isMap(base)) {
146 return new Map(base)
147 }
148 if (isSet(base)) {
149 return new Set(base)
150 }
151 if (Array.isArray(base)) return Array.prototype.slice.call(base)
152
153 const isPlain = isPlainObject(base)
154
155 if (strict === true || (strict === "class_only" && !isPlain)) {
156 // Perform a strict copy
157 const descriptors = Object.getOwnPropertyDescriptors(base)
158 delete descriptors[DRAFT_STATE as any]
159 let keys = Reflect.ownKeys(descriptors)
160 for (let i = 0; i < keys.length; i++) {
161 const key: any = keys[i]
162 const desc = descriptors[key]
163 if (desc.writable === false) {
164 desc.writable = true
165 desc.configurable = true
166 }
167 // like object.assign, we will read any _own_, get/set accessors. This helps in dealing
168 // with libraries that trap values, like mobx or vue
169 // unlike object.assign, non-enumerables will be copied as well
170 if (desc.get || desc.set)
171 descriptors[key] = {
172 configurable: true,
173 writable: true, // could live with !!desc.set as well here...
174 enumerable: desc.enumerable,
175 value: base[key]
176 }
177 }
178 return Object.create(getPrototypeOf(base), descriptors)
179 } else {
180 // perform a sloppy copy
181 const proto = getPrototypeOf(base)
182 if (proto !== null && isPlain) {
183 return {...base} // assumption: better inner class optimization than the assign below
184 }
185 const obj = Object.create(proto)
186 return Object.assign(obj, base)
187 }
188}
189
190/**
191 * Freezes draftable objects. Returns the original object.
192 * By default freezes shallowly, but if the second argument is `true` it will freeze recursively.
193 *
194 * @param obj
195 * @param deep
196 */
197export function freeze<T>(obj: T, deep?: boolean): T
198export function freeze<T>(obj: any, deep: boolean = false): T {
199 if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj
200 if (getArchtype(obj) > 1 /* Map or Set */) {
201 obj.set = obj.add = obj.clear = obj.delete = dontMutateFrozenCollections as any
202 }
203 Object.freeze(obj)
204 if (deep)
205 // See #590, don't recurse into non-enumerable / Symbol properties when freezing
206 // So use Object.entries (only string-like, enumerables) instead of each()
207 Object.entries(obj).forEach(([key, value]) => freeze(value, true))
208 return obj
209}
210
211function dontMutateFrozenCollections() {
212 die(2)
213}
214
215export function isFrozen(obj: any): boolean {
216 return Object.isFrozen(obj)
217}