From e0b7c01c4ce039b7a68b5cb3cd97a7242962b7ab Mon Sep 17 00:00:00 2001
From: Michel Weststrate <mweststrate@gmail.com>
Date: Sat, 20 Mar 2021 21:37:31 +0000
Subject: [PATCH] fix: #768 `immerable` field being lost during patch value
 cloning (#771)

---
 __tests__/empty.ts      | 56 +++++++++++++++++++++++++++++++++++++++--
 __tests__/tsconfig.json |  3 ++-
 src/core/immerClass.ts  |  6 ++---
 src/plugins/patches.ts  |  2 ++
 4 files changed, 61 insertions(+), 6 deletions(-)

diff --git a/__tests__/empty.ts b/__tests__/empty.ts
index 798114d7..10295f8e 100644
--- a/__tests__/empty.ts
+++ b/__tests__/empty.ts
@@ -2,9 +2,11 @@ import {
 	produce,
 	produceWithPatches,
 	setUseProxies,
-	enableAllPlugins
+	enableAllPlugins,
+	immerable,
+	applyPatches
 } from "../src/immer"
-import {DRAFT_STATE} from "../src/internal"
+import {DRAFT_STATE, Patch} from "../src/internal"
 
 enableAllPlugins()
 
@@ -149,3 +151,53 @@ function createBaseState() {
 	}
 	return data
 }
+
+describe("#768", () => {
+	class Stock {
+		[immerable] = true
+
+		constructor(public price: number) {}
+
+		pushPrice(price: number) {
+			this.price = price
+		}
+	}
+
+	type State = {
+		stock: Stock
+	}
+
+	test("bla", () => {
+		// Set up conditions to produce the error
+		const errorProducingPatch = [
+			{
+				op: "replace",
+				path: ["stock"],
+				value: new Stock(200)
+			}
+		] as Patch[]
+
+		// Start with modified state
+		const state = {
+			stock: new Stock(100)
+		}
+
+		expect(state.stock.price).toEqual(100)
+		expect(state.stock[immerable]).toBeTruthy()
+		// Use patch to "replace" stocks
+		debugger
+		const resetState: State = applyPatches(state, errorProducingPatch)
+		expect(state.stock.price).toEqual(100)
+		expect(resetState.stock.price).toEqual(200)
+		expect(resetState.stock[immerable]).toBeTruthy()
+
+		// Problems come in when resetState is modified
+		const updatedState = produce(resetState, draft => {
+			draft.stock.pushPrice(300)
+		})
+		expect(state.stock.price).toEqual(100)
+		expect(updatedState.stock.price).toEqual(300)
+		expect(updatedState.stock[immerable]).toBeTruthy()
+		expect(resetState.stock.price).toEqual(200)
+	})
+})
diff --git a/__tests__/tsconfig.json b/__tests__/tsconfig.json
index 19c0864b..4b76f62e 100644
--- a/__tests__/tsconfig.json
+++ b/__tests__/tsconfig.json
@@ -3,6 +3,7 @@
   "compilerOptions": {
     "lib": ["es2015"],
     "strict": true,
-    "noUnusedLocals": false
+    "noUnusedLocals": false,
+    "target": "ES5"
   }
 }
diff --git a/src/core/immerClass.ts b/src/core/immerClass.ts
index 1818e909..a2954a16 100644
--- a/src/core/immerClass.ts
+++ b/src/core/immerClass.ts
@@ -185,7 +185,7 @@ export class Immer implements ProducersFns {
 		this.useProxies_ = value
 	}
 
-	applyPatches(base: Objectish, patches: Patch[]) {
+	applyPatches<T extends Objectish>(base: Objectish, patches: Patch[]): T {
 		// If a patch replaces the entire state, take that replacement as base
 		// before applying patches
 		let i: number
@@ -200,12 +200,12 @@ export class Immer implements ProducersFns {
 		const applyPatchesImpl = getPlugin("Patches").applyPatches_
 		if (isDraft(base)) {
 			// N.B: never hits if some patch a replacement, patches are never drafts
-			return applyPatchesImpl(base, patches)
+			return applyPatchesImpl(base, patches) as any
 		}
 		// Otherwise, produce a copy of the base state.
 		return this.produce(base, (draft: Drafted) =>
 			applyPatchesImpl(draft, patches.slice(i + 1))
-		)
+		) as any
 	}
 }
 
diff --git a/src/plugins/patches.ts b/src/plugins/patches.ts
index a06a526a..bc3ba282 100644
--- a/src/plugins/patches.ts
+++ b/src/plugins/patches.ts
@@ -1,3 +1,4 @@
+import {immerable} from "../immer"
 import {
 	ImmerState,
 	Patch,
@@ -287,6 +288,7 @@ export function enablePatches() {
 		if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue))
 		const cloned = Object.create(Object.getPrototypeOf(obj))
 		for (const key in obj) cloned[key] = deepClonePatchValue(obj[key])
+		if (has(obj, immerable)) cloned[immerable] = obj[immerable]
 		return cloned
 	}