Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Immer 10 #1028

Merged
merged 52 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
503b4be
Do not replace array.length
Jul 31, 2022
31e61bf
feat: implement useStrictShallowCopy
May 18, 2022
de9569f
breaking change: Change useStrictShallowCopy to enabled by default
hrsh7th Jan 29, 2023
a9bc548
doc: Add setUseStrictShallowCopy to api.md
hrsh7th Jan 29, 2023
ecbe989
Remove promise based producer support #1015
mweststrate Jan 30, 2023
eb5ac78
Remove ES5 support WIP
mweststrate Jan 30, 2023
4df7e24
Merged ProxyType and ArchType
mweststrate Jan 30, 2023
3c199a3
Add todo comment
hrsh7th Feb 15, 2023
3819609
Change comment
hrsh7th Feb 15, 2023
99efdef
Improve documents
hrsh7th Feb 15, 2023
3cabd7b
Fix flowtype doc comment
hrsh7th Feb 15, 2023
da3de76
Fix test case comment
hrsh7th Feb 15, 2023
59971e1
[breaking] Remove `export default produce`
mweststrate Mar 23, 2023
ec72486
chore: Merge branch 'main' into immer-10
mweststrate Mar 23, 2023
cb5712a
[breaking] Merge branch 'array-remove' of github.com:kshramt/immer in…
mweststrate Mar 23, 2023
d5aba94
breaking: Emit remove instead of length replace patches when removing…
mweststrate Mar 23, 2023
07242c5
random bits of cleanup
mweststrate Mar 23, 2023
e69c766
Remove `enableAllPlugins` from API
mweststrate Mar 23, 2023
5540249
Enable Map / Set support by default
mweststrate Mar 23, 2023
7c055e7
Cleaned up production error snapshots
mweststrate Mar 23, 2023
6e9032b
chore: Upgraded to TS 5
mweststrate Mar 24, 2023
d533f24
Add large object perf test (from #941)
mweststrate Mar 24, 2023
e71aebd
feat: Introduce strictShallowCopy, See #941, hrsh7th-use-strict-shall…
mweststrate Mar 24, 2023
8195782
Fix performance tests
mweststrate Mar 24, 2023
a869118
cleanup current implementation
mweststrate Mar 24, 2023
1d93079
minor cleanup
mweststrate Mar 24, 2023
452a508
stored errors as array so that they become smaller and part of plugin
mweststrate Mar 24, 2023
a1fd163
Map / Set & old browser API cleanup
mweststrate Mar 24, 2023
27db1a1
Further simplification / optimization
mweststrate Mar 24, 2023
86fd429
some build stuff cleanup
mweststrate Mar 25, 2023
1690a55
Attempt at using ESM
mweststrate Mar 25, 2023
998b69d
More Jest/ESM fixes
mweststrate Mar 25, 2023
af92b4d
Fix prod tests
mweststrate Mar 25, 2023
5cbfc0f
fixed lockfile
mweststrate Mar 25, 2023
605ed40
Fix test:perf ESM
mweststrate Mar 26, 2023
df3bbe2
Some doc refinements
mweststrate Apr 3, 2023
bbd7fb2
Dropped module:type per https://github.com/immerjs/immer/issues/992#i…
mweststrate Apr 3, 2023
c8e7cf0
v10.0.0-beta.2
mweststrate Apr 3, 2023
6f501e6
v10.0.0-beta.3
mweststrate Apr 3, 2023
05b85f7
v10.0.0-beta.4
mweststrate Apr 3, 2023
931f963
Fixed build once again
mweststrate Apr 5, 2023
aa0e5ee
attempt2
mweststrate Apr 5, 2023
906b69b
Revert MapSet as plugin
mweststrate Apr 5, 2023
48204c9
fix issue where deleting non-existing properties fails
mweststrate Apr 6, 2023
3c1a668
Add tsup
markerikson Apr 10, 2023
17cfe4e
Output modern JS syntax
markerikson Apr 10, 2023
3ad1df5
Use tsup to build and modernize build artifacts
markerikson Apr 10, 2023
d897c6b
Micro-optimize Object.getPrototypeOf uses
markerikson Apr 10, 2023
d5be8f2
Remove tsdx
markerikson Apr 10, 2023
46cd1d5
Minor build fixes after tsup migration
mweststrate Apr 15, 2023
6f2a12b
Merge branch 'markerikson-feature/build-tsup' into immer-10
mweststrate Apr 15, 2023
41ebbbc
Restore mangleProps
mweststrate Apr 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 296 additions & 36 deletions __tests__/__prod_snapshots__/base.js.snap

Large diffs are not rendered by default.

492 changes: 416 additions & 76 deletions __tests__/__snapshots__/base.js.snap

Large diffs are not rendered by default.

62 changes: 42 additions & 20 deletions __tests__/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from "../src/immer"
import {each, shallowCopy, DRAFT_STATE} from "../src/internal"
import deepFreeze from "deep-freeze"
import cloneDeep from "lodash.clonedeep"
import * as lodash from "lodash"

jest.setTimeout(1000)
Expand All @@ -22,17 +21,24 @@ test("immer should have no dependencies", () => {
expect(require("../package.json").dependencies).toBeUndefined()
})

runBaseTest("proxy (no freeze)", false)
runBaseTest("proxy (autofreeze)", true)
runBaseTest("proxy (patch listener)", false, true)
runBaseTest("proxy (autofreeze)(patch listener)", true, true)
for (const autoFreeze of [true, false]) {
for (const useStrictShallowCopy of [true, false]) {
for (const useListener of [true, false]) {
const name = `${autoFreeze ? "auto-freeze=true" : "auto-freeze=false"}:${
useStrictShallowCopy ? "shallow-copy=true" : "shallow-copy=false"
}:${useListener ? "use-listener=true" : "use-listener=false"}`
runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener)
}
}
}

class Foo {}

function runBaseTest(name, autoFreeze, useListener) {
function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener) {
const listener = useListener ? function() {} : undefined
const {produce, produceWithPatches} = createPatchedImmer({
autoFreeze
autoFreeze,
useStrictShallowCopy
})

// When `useListener` is true, append a function to the arguments of every
Expand Down Expand Up @@ -890,10 +896,16 @@ function runBaseTest(name, autoFreeze, useListener) {
expect(s[test]).toBeTruthy()
s.foo = true
})
expect(nextState).toEqual({
[test]: true,
foo: true
})
if (useStrictShallowCopy) {
expect(nextState).toEqual({
[test]: true,
foo: true
})
} else {
expect(nextState).toEqual({
foo: true
mweststrate marked this conversation as resolved.
Show resolved Hide resolved
})
}
})

if (!global.USES_BUILD)
Expand All @@ -909,16 +921,26 @@ function runBaseTest(name, autoFreeze, useListener) {
value: 1,
enumerable: false
})
// Non-enumerable primitive property that won't modified.
Object.defineProperty(baseState, "baz", {
value: 1,
enumerable: false
})

// When using Proxy, even non-enumerable keys will be copied if it's changed.
const canReferNonEnumerableProperty = useStrictShallowCopy
const nextState = produce(baseState, s => {
expect(s.foo).toBeTruthy()
expect(isEnumerable(s, "foo")).toBeFalsy()
s.bar++
expect(isEnumerable(s, "foo")).toBeFalsy()
s.foo.a++
expect(isEnumerable(s, "foo")).toBeFalsy()
})
expect(nextState.foo).toBeTruthy()
expect(isEnumerable(nextState, "foo")).toBeFalsy()
if (canReferNonEnumerableProperty) expect(s.foo).toBeTruthy()
if (useStrictShallowCopy) expect(isEnumerable(s, "foo")).toBeFalsy()
if (canReferNonEnumerableProperty) s.bar++
if (useStrictShallowCopy) expect(isEnumerable(s, "foo")).toBeFalsy()
if (canReferNonEnumerableProperty) s.foo.a++
if (useStrictShallowCopy) expect(isEnumerable(s, "foo")).toBeFalsy()
})
if (canReferNonEnumerableProperty) expect(nextState.foo).toBeTruthy()
if (useStrictShallowCopy)
expect(isEnumerable(nextState, "foo")).toBeFalsy()
if (useStrictShallowCopy) expect(nextState.baz).toBeTruthy()
})

it("can work with own computed props", () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/map-set.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
immerable,
enablePatches
} from "../src/immer"
import {each, shallowCopy, isEnumerable, DRAFT_STATE} from "../src/common"
import {each, shallowCopy, isEnumerable, DRAFT_STATE} from "../src/utils/common"

enablePatches()

Expand Down
37 changes: 37 additions & 0 deletions __tests__/not-strict-copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {produce, setUseStrictShallowCopy} from "../src/immer"

describe("setUseStrictShallowCopy(true)", () => {
test("keep descriptors", () => {
setUseStrictShallowCopy(true)

const base: Record<string, unknown> = {}
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("ignore descriptors", () => {
setUseStrictShallowCopy(false)

const base: Record<string, unknown> = {}
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()
})
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"sideEffects": false,
"scripts": {
"test": "jest && yarn test:build && yarn test:flow",
"test:perf": "cd __performance_tests__ && babel-node add-data.js && babel-node todo.js && babel-node incremental.js",
"test:perf": "cd __performance_tests__ && babel-node add-data.js && babel-node todo.js && babel-node incremental.js && babel-node large-obj.js",
"test:flow": "yarn flow check __tests__/flow",
"test:build": "yarn build && NODE_ENV='production' yarn jest --config jest.config.build.js",
"watch": "jest --watch",
Expand Down
12 changes: 8 additions & 4 deletions src/core/current.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ function currentImpl(value: any): any {
if (!state.modified_) 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)
mweststrate marked this conversation as resolved.
Show resolved Hide resolved
}

each(copy, (key, childValue) => {
Expand All @@ -42,7 +46,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:
Expand All @@ -51,5 +55,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)
}
14 changes: 13 additions & 1 deletion src/core/immerClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ interface ProducersFns {

export class Immer implements ProducersFns {
autoFreeze_: boolean = true
useStrictShallowCopy_: boolean = false

constructor(config?: {autoFreeze?: boolean}) {
constructor(config?: {autoFreeze?: boolean; useStrictShallowCopy?: boolean}) {
if (typeof config?.autoFreeze === "boolean")
this.setAutoFreeze(config!.autoFreeze)
if (typeof config?.useStrictShallowCopy === "boolean")
this.setUseStrictShallowCopy(config!.useStrictShallowCopy)
}

/**
Expand Down Expand Up @@ -158,6 +161,15 @@ export class Immer implements ProducersFns {
this.autoFreeze_ = value
}

/**
* Pass true to enable strict shallow copy.
*
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
*/
setUseStrictShallowCopy(value: boolean) {
this.useStrictShallowCopy_ = value
}

applyPatches<T extends Objectish>(base: T, patches: Patch[]): T {
// If a patch replaces the entire state, take that replacement as base
// before applying patches
Expand Down
12 changes: 10 additions & 2 deletions src/core/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
createProxy,
ArchType
} from "../internal"
import {ImmerScope} from "./scope"
mweststrate marked this conversation as resolved.
Show resolved Hide resolved

interface ProxyBaseState extends ImmerBaseState {
assigned_: {
Expand Down Expand Up @@ -269,8 +270,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_
)
}
}
7 changes: 7 additions & 0 deletions src/immer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export const produceWithPatches: IProduceWithPatches = immer.produceWithPatches.
*/
export const setAutoFreeze = immer.setAutoFreeze.bind(immer)

/**
* Pass true to enable strict shallow copy.
*
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
*/
export const setUseStrictShallowCopy = immer.setUseStrictShallowCopy.bind(immer)

/**
* Apply an array of Immer patches to the first argument.
*
Expand Down
7 changes: 7 additions & 0 deletions src/types/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ declare export function setAutoFreeze(autoFreeze: boolean): void
*/
declare export function setUseProxies(useProxies: boolean): void
mweststrate marked this conversation as resolved.
Show resolved Hide resolved

/**
* Pass false to disable strict shallow copy.
*
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
*/
declare export function setUseStrictShallowCopy(useStrictShallowCopy: boolean): void

declare export function applyPatches<S>(state: S, patches: Patch[]): S

declare export function original<S>(value: S): S
Expand Down
13 changes: 12 additions & 1 deletion src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,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)
Expand Down
1 change: 1 addition & 0 deletions website/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ title: API overview
| `produceWithPatches` | Works the same as `produce`, but instead of just returning the produced object, it returns a tuple, consisting of `[result, patches, inversePatches]`. | [Patches](./patches.mdx) |
| `setAutoFreeze` | Enables / disables automatic freezing of the trees produces. By default enabled. | [Freezing](./freezing.mdx) |
| `setUseProxies` | Can be used to disable or force the use of `Proxy` objects. Useful when filing bug reports. | |
mweststrate marked this conversation as resolved.
Show resolved Hide resolved
| `setUseStrictShallowCopy` | Can be used to enable strict shallow copy. If enable, immer copies non-enumerable properties as much as possible. | [Classes](./complex-objects.md) |

## Importing immer

Expand Down
14 changes: 8 additions & 6 deletions website/docs/complex-objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ title: Classes
<div data-ea-publisher="immerjs" data-ea-type="image" className="horizontal bordered"></div>
</center>

By default, Immer does not strictly handle Plain object's non-eumerable properties such as getters/setters for performance reason. If you want this behavior to be strict, you can opt-in with `useStrictShallowCopy(true)`.

Plain objects (objects without a prototype), arrays, `Map`s and `Set`s are always drafted by Immer. Every other object must use the `immerable` symbol to mark itself as compatible with Immer. When one of these objects is mutated within a producer, its prototype is preserved between copies.

```js
Expand Down Expand Up @@ -59,12 +61,12 @@ console.log(clock2 instanceof Clock) // true
The semantics on how classes are drafted are as follows:

1. A draft of a class is a fresh object but with the same prototype as the original object.
2. When creating a draft, Immer will copy all _own_ properties from the base to the draft.This includes non-enumerable and symbolic properties.
3. _Own_ getters will be invoked during the copy process, just like `Object.assign` would.
4. Inherited getters and methods will remain as is and be inherited by the draft.
5. Immer will not invoke constructor functions.
6. The final instance will be constructed with the same mechanism as the draft was created.
7. Only getters that have a setter as well will be writable in the draft, as otherwise the value can't be copied back.
1. When creating a draft, Immer will copy all _own_ properties from the base to the draft.This includes non-enumerable and symbolic properties (if `useStrictShallowCopy(true)` was called).
1. _Own_ getters will be invoked during the copy process, just like `Object.assign` would.
1. Inherited getters and methods will remain as is and be inherited by the draft.
1. Immer will not invoke constructor functions.
1. The final instance will be constructed with the same mechanism as the draft was created.
1. Only getters that have a setter as well will be writable in the draft, as otherwise the value can't be copied back.

Because Immer will dereference own getters of objects into normal properties, it is possible to use objects that use getter/setter traps on their fields, like MobX and Vue do.

Expand Down