diff --git a/.travis.yml b/.travis.yml index 22a20535..a07a08a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: node_js node_js: - # - "10.18.1" - - "node" + - "10.18.1" + # - "node" env: - NODE_ENV=TEST cache: diff --git a/__tests__/frozen.js b/__tests__/frozen.js index d1571db4..04439d45 100644 --- a/__tests__/frozen.js +++ b/__tests__/frozen.js @@ -2,7 +2,8 @@ import produce, { setUseProxies, setAutoFreeze, - enableAllPlugins + enableAllPlugins, + freeze } from "../src/immer" enableAllPlugins() @@ -246,3 +247,21 @@ function runTests(name, useProxies) { }) }) } + +test("freeze - shallow", () => { + const obj1 = {hello: {world: true}} + const res = freeze(obj1) + + expect(res).toBe(obj1) + expect(Object.isFrozen(res)).toBe(true) + expect(Object.isFrozen(res.hello)).toBe(false) +}) + +test("freeze - deep", () => { + const obj1 = {hello: {world: true}} + const res = freeze(obj1, true) + + expect(res).toBe(obj1) + expect(Object.isFrozen(res)).toBe(true) + expect(Object.isFrozen(res.hello)).toBe(true) +}) diff --git a/docs/api.md b/docs/api.md index 9e2b05d7..81711d29 100644 --- a/docs/api.md +++ b/docs/api.md @@ -21,6 +21,7 @@ title: API overview | `enableMapSet()` | Enables support for `Map` and `Set` collections. | [Installation](installation#pick-your-immer-version) | | `enablePatches()` | Enables support for JSON patches. | [Installation](installation#pick-your-immer-version) | | `finishDraft` | Given an draft created using `createDraft`, seals the draft and produces and returns the next immutable state that captures all the changes | [Async](async.md) | +| `freeze(obj, deep?)` | Freezes draftable objects. Returns the original object. By default freezes shallowly, but if the second argument is `true` it will freeze recursively. | | `Immer` | constructor that can be used to create a second "immer" instance (exposing all APIs listed in this instance), that doesn't share its settings with global instance. | | `immerable` | Symbol that can be added to a constructor or prototype, to indicate that Immer should treat the class as something that can be safely drafted | [Classes](complex-objects.md) | | `Immutable` | Exposed TypeScript type to convert mutable types to immutable types | | @@ -31,7 +32,7 @@ title: API overview | `Patch` | Exposed TypeScript type, describes the shape of an (inverse) patch object | [Patches](patches.md) | | `produce` | The core API of Immer, also exposed as the `default` export | [Produce](produce.md) | | `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.md) | -| `setAutoFreeze` | Enables / disables automatic freezing of the trees produces. By default enabled in development builds | [Freezing](freezing.md) | +| `setAutoFreeze` | Enables / disables automatic freezing of the trees produces. By default enabled. | [Freezing](freezing.md) | | `setUseProxies` | Can be used to disable or force the use of `Proxy` objects. Useful when filing bug reports. | | ## Importing immer diff --git a/docs/freezing.md b/docs/freezing.md index 23d1712d..5eef2471 100644 --- a/docs/freezing.md +++ b/docs/freezing.md @@ -16,8 +16,10 @@ title: Auto freezing Hosted on egghead.io -Immer automatically freezes any state trees that are modified using `produce`. This protects against accidental modifications of the state tree outside of a producer. This comes with a performance impact, so it is recommended to disable this option in production. By default, it is turned on during local development and turned off in production. Use `setAutoFreeze(true / false)` to explicitly turn this feature on or off. +Immer automatically freezes any state trees that are modified using `produce`. This protects against accidental modifications of the state tree outside of a producer. In most cases this provides the most optimal behavior, but `setAutoFreeze(true / false)` can be used to explicitly turn this feature on or off. Immer will never freeze (the contents of) non-enumerable, non-own or symbolic properties, unless their content was drafted. +_⚠️ Immer freezes everything recursively, for large data objects that won't be changed in the future this might be over-kill, in that case it can be more efficient to shallowly pre-freeze data using the `freeze` utility.⚠️_ + _⚠️ If auto freezing is enabled, recipes are not entirely side-effect free: Any plain object or array that ends up in the produced result, will be frozen, even when these objects were not frozen before the start of the producer! ⚠️_ diff --git a/docs/performance.md b/docs/performance.md index 5d8738dd..7c679d9f 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -44,7 +44,7 @@ Most important observation: ### Pre-freeze data -When adding a large data set to the state tree in an Immer producer (for example data received from a JSON endpoint), it is worth to call `Object.freeze(json)` on the root of the data to be added first. This will allow Immer to add the new data to the tree faster, as it will skip freezing it, or searching the tree for any changes (drafts) that might be made. +When adding a large data set to the state tree in an Immer producer (for example data received from a JSON endpoint), it is worth to call `freeze(json)` on the root of the data to be added first. This will allow Immer to add the new data to the tree faster, as it will skip _recursively_ freezing it, or searching the new data for any changes (drafts) that might be made. ### You can always opt-out diff --git a/src/core/immerClass.ts b/src/core/immerClass.ts index e6c1bdcb..3e1c53df 100644 --- a/src/core/immerClass.ts +++ b/src/core/immerClass.ts @@ -17,7 +17,6 @@ import { getPlugin, die, hasProxies, - isMinified, enterScope, revokeScope, leaveScope, @@ -36,7 +35,7 @@ interface ProducersFns { export class Immer implements ProducersFns { useProxies_: boolean = hasProxies - autoFreeze_: boolean = __DEV__ ? true /* istanbul ignore next */ : !isMinified + autoFreeze_: boolean = true constructor(config?: {useProxies?: boolean; autoFreeze?: boolean}) { if (typeof config?.useProxies === "boolean") diff --git a/src/immer.ts b/src/immer.ts index ea3fc1e1..53455494 100644 --- a/src/immer.ts +++ b/src/immer.ts @@ -16,7 +16,8 @@ export { isDraft, isDraftable, NOTHING as nothing, - DRAFTABLE as immerable + DRAFTABLE as immerable, + freeze } from "./internal" const immer = new Immer() diff --git a/src/types/index.js.flow b/src/types/index.js.flow index 29758fbd..1be017a2 100644 --- a/src/types/index.js.flow +++ b/src/types/index.js.flow @@ -109,3 +109,5 @@ declare export function enableES5(): void declare export function enableMapSet(): void declare export function enablePatches(): void declare export function enableAllPlugins(): void + +declare export function freeze(obj: T, freeze?: boolean): T diff --git a/src/utils/common.ts b/src/utils/common.ts index 67980747..16f94303 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -179,13 +179,22 @@ export function shallowCopy(base: any) { return Object.create(Object.getPrototypeOf(base), descriptors) } -export function freeze(obj: any, deep: boolean): void { - if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return +/** + * Freezes draftable objects. Returns the original object. + * By default freezes shallowly, but if the second argument is `true` it will freeze recursively. + * + * @param obj + * @param deep + */ +export function freeze(obj: T, deep?: boolean): T +export function freeze(obj: any, deep: boolean = false): T { + if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj if (getArchtype(obj) > 1 /* Map or Set */) { obj.set = obj.add = obj.clear = obj.delete = dontMutateFrozenCollections as any } Object.freeze(obj) if (deep) each(obj, (key, value) => freeze(value, true), true) + return obj } function dontMutateFrozenCollections() { diff --git a/src/utils/env.ts b/src/utils/env.ts index c846d09a..4a3d306f 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -12,10 +12,6 @@ export const hasProxies = typeof Proxy.revocable !== "undefined" && typeof Reflect !== "undefined" -/* istanbul ignore next */ -function mini() {} -export const isMinified = mini.name !== "mini" - /** * The sentinel value returned by producers to replace the draft with undefined. */ diff --git a/website/static/css/custom.css b/website/static/css/custom.css index 00eca270..57fe7327 100755 --- a/website/static/css/custom.css +++ b/website/static/css/custom.css @@ -22,16 +22,3 @@ a { a:hover { text-decoration: underline; } - -/* BLM */ -.slidingNav::before { - content: "Black Lives Matter"; - position: fixed; - left: 0; - width: 100vw; - top: 13px; - text-align: center; - pointer-events: none; - text-transform: uppercase; - font-weight: bold; -}