diff --git a/__tests__/not-strict-copy.ts b/__tests__/not-strict-copy.ts new file mode 100644 index 000000000..d21e9126f --- /dev/null +++ b/__tests__/not-strict-copy.ts @@ -0,0 +1,39 @@ +import produce, {enableAllPlugins, setUseStrictShallowCopy} from "../src/immer" + +enableAllPlugins() + +describe("setUseStrictShallowCopy(true)", () => { + test("keep property", () => { + setUseStrictShallowCopy(true) + + const base: Record = {} + Object.defineProperty(base, "foo", { + value: "foo", + writable: false, + configurable: false + }) + const copy = produce(base, (draft: any) => { + draft.bar = "bar" + }) + expect(Object.getOwnPropertyDescriptor(copy, "foo")).toStrictEqual( + Object.getOwnPropertyDescriptor(base, "foo") + ) + }) +}) + +describe("setUseStrictShallowCopy(false)", () => { + test("keep property", () => { + setUseStrictShallowCopy(false) + + const base: Record = {} + Object.defineProperty(base, "foo", { + value: "foo", + writable: false, + configurable: false + }) + const copy = produce(base, (draft: any) => { + draft.bar = "bar" + }) + expect(Object.getOwnPropertyDescriptor(copy, "foo")).toBeUndefined() + }) +}) diff --git a/src/core/current.ts b/src/core/current.ts index 3a2019886..cfe11fea2 100644 --- a/src/core/current.ts +++ b/src/core/current.ts @@ -33,10 +33,14 @@ function currentImpl(value: any): any { return state.base_ // Optimization: avoid generating new drafts during copying state.finalized_ = true - copy = copyHelper(value, archType) + copy = copyHelper( + value, + archType, + state.scope_.immer_.useStrictShallowCopy_ + ) state.finalized_ = false } else { - copy = copyHelper(value, archType) + copy = copyHelper(value, archType, true) } each(copy, (key, childValue) => { @@ -47,7 +51,7 @@ function currentImpl(value: any): any { return archType === Archtype.Set ? new Set(copy) : copy } -function copyHelper(value: any, archType: number): any { +function copyHelper(value: any, archType: number, strict: boolean): any { // creates a shallow copy, even if it is a map or set switch (archType) { case Archtype.Map: @@ -56,5 +60,5 @@ function copyHelper(value: any, archType: number): any { // Set will be cloned as array temporarily, so that we can replace individual items return Array.from(value) } - return shallowCopy(value) + return shallowCopy(value, strict) } diff --git a/src/core/finalize.ts b/src/core/finalize.ts index ad95b1249..0e9b506aa 100644 --- a/src/core/finalize.ts +++ b/src/core/finalize.ts @@ -83,7 +83,10 @@ function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) { const result = // For ES5, create a good copy from the draft first, with added keys and without deleted keys. state.type_ === ProxyType.ES5Object || state.type_ === ProxyType.ES5Array - ? (state.copy_ = shallowCopy(state.draft_)) + ? (state.copy_ = shallowCopy( + state.draft_, + rootScope.immer_.useStrictShallowCopy_ + )) : state.copy_ // Finalize all children of the copy // For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628 diff --git a/src/core/immerClass.ts b/src/core/immerClass.ts index 2a6da9278..04e13bc62 100644 --- a/src/core/immerClass.ts +++ b/src/core/immerClass.ts @@ -37,11 +37,19 @@ export class Immer implements ProducersFns { autoFreeze_: boolean = true - constructor(config?: {useProxies?: boolean; autoFreeze?: boolean}) { + useStrictShallowCopy_: boolean = true + + constructor(config?: { + useProxies?: boolean + autoFreeze?: boolean + useStrictShallowCopy?: boolean + }) { if (typeof config?.useProxies === "boolean") this.setUseProxies(config!.useProxies) if (typeof config?.autoFreeze === "boolean") this.setAutoFreeze(config!.autoFreeze) + if (typeof config?.useStrictShallowCopy === "boolean") + this.setUseStrictShallowCopy(config!.useStrictShallowCopy) } /** @@ -195,6 +203,15 @@ export class Immer implements ProducersFns { this.useProxies_ = value } + /** + * Pass false to disable strict shallow copy. + * + * By default, immer copies the object descriptors on creating new object. + */ + setUseStrictShallowCopy(value: boolean) { + this.useStrictShallowCopy_ = value + } + applyPatches(base: T, patches: Patch[]): T { // If a patch replaces the entire state, take that replacement as base // before applying patches diff --git a/src/core/proxy.ts b/src/core/proxy.ts index 7e6503a90..b6bffbad2 100644 --- a/src/core/proxy.ts +++ b/src/core/proxy.ts @@ -17,6 +17,7 @@ import { createProxy, ProxyType } from "../internal" +import {ImmerScope} from "./scope" interface ProxyBaseState extends ImmerBaseState { assigned_: { @@ -273,8 +274,15 @@ export function markChanged(state: ImmerState) { } } -export function prepareCopy(state: {base_: any; copy_: any}) { +export function prepareCopy(state: { + base_: any + copy_: any + scope_: ImmerScope +}) { if (!state.copy_) { - state.copy_ = shallowCopy(state.base_) + state.copy_ = shallowCopy( + state.base_, + state.scope_.immer_.useStrictShallowCopy_ + ) } } diff --git a/src/immer.ts b/src/immer.ts index 0eacffe61..0570b2e16 100644 --- a/src/immer.ts +++ b/src/immer.ts @@ -67,6 +67,13 @@ export const setAutoFreeze = immer.setAutoFreeze.bind(immer) */ export const setUseProxies = immer.setUseProxies.bind(immer) +/** + * Pass false to disable strict shallow copy. + * + * By default, immer copies the object descriptors on creating new object. + */ +export const setUseStrictShallowCopy = immer.setUseStrictShallowCopy.bind(immer) + /** * Apply an array of Immer patches to the first argument. * diff --git a/src/types/index.js.flow b/src/types/index.js.flow index 4640267e2..d95534979 100644 --- a/src/types/index.js.flow +++ b/src/types/index.js.flow @@ -84,6 +84,13 @@ declare export function setAutoFreeze(autoFreeze: boolean): void */ declare export function setUseProxies(useProxies: boolean): void +/** + * Pass false to disable strict shallow copy. + * + * By default, immer copies the object descriptors on creating new object. + */ +declare export function setUseStrictShallowCopy(useStrictShallowCopy: boolean): void + declare export function applyPatches(state: S, patches: Patch[]): S declare export function original(value: S): S diff --git a/src/utils/common.ts b/src/utils/common.ts index dae5064eb..34a07ecd1 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -162,8 +162,19 @@ export function latest(state: ImmerState): any { } /*#__PURE__*/ -export function shallowCopy(base: any) { +export function shallowCopy(base: any, strict: boolean) { if (Array.isArray(base)) return Array.prototype.slice.call(base) + + if (!strict && isPlainObject(base)) { + const keys = Object.keys(base) + const obj: any = Object.create(Object.getPrototypeOf(base)) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + obj[key] = base[key] + } + return obj + } + const descriptors = getOwnPropertyDescriptors(base) delete descriptors[DRAFT_STATE as any] let keys = ownKeys(descriptors)