Skip to content

Commit 31684f2

Browse files
authoredNov 17, 2020
chore: fix some build issues (#701)
* clearer error when plugin is missing * chore: fix travis build not failing, fixes #688 (?) * fix: make plugin loading idempotent, fixes #692 * docs: Organize performance and pitfalls, and document nested produce behavior. Fixes #694 * chore: fix prod-build-issues
1 parent 678e541 commit 31684f2

15 files changed

+224
-145
lines changed
 

‎.travis.yml

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
language: node_js
22
node_js:
3-
- "10.18.1"
4-
# - "node"
3+
# - "10.18.1"
4+
- "node"
55
env:
66
- NODE_ENV=TEST
77
cache:
@@ -12,12 +12,13 @@ before_script:
1212
- yarn global add if-node-version
1313
script:
1414
- yarn build || travis_terminate 1
15-
- if-node-version 10 || { yarn test && travis_terminate 0; }
16-
- yarn coveralls
15+
- yarn test || travis_terminate 1
16+
- yarn coveralls || travis_terminate 0
1717
jobs:
1818
include:
1919
- stage: deploy
2020
if: branch == master && !fork
2121
script:
22-
- NODE_ENV= yarn build
22+
- yarn test || travis_terminate 1
23+
- NODE_ENV= yarn build || travis_terminate 1
2324
- yarn semantic-release

‎__tests__/__prod_snapshots__/base.js.snap

+32-48
Large diffs are not rendered by default.
+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`applyPatches throws when \`op\` is not "add", "replace", nor "remove" 1`] = `"[Immer] minified error nr: 17 copy. Find the full error at: https://bit.ly/3cXEKWf"`;
3+
exports[`applyPatches throws when \`op\` is not "add", "replace", nor "remove" 1`] = `"[Immer] minified error nr: 17 'copy'. Find the full error at: https://bit.ly/3cXEKWf"`;
44

5-
exports[`applyPatches throws when \`path\` cannot be resolved 1`] = `"[Immer] minified error nr: 15 a/b. Find the full error at: https://bit.ly/3cXEKWf"`;
5+
exports[`applyPatches throws when \`path\` cannot be resolved 1`] = `"[Immer] minified error nr: 15 'a/b'. Find the full error at: https://bit.ly/3cXEKWf"`;
66

7-
exports[`applyPatches throws when \`path\` cannot be resolved 2`] = `"[Immer] minified error nr: 15 a/b/c. Find the full error at: https://bit.ly/3cXEKWf"`;
7+
exports[`applyPatches throws when \`path\` cannot be resolved 2`] = `"[Immer] minified error nr: 15 'a/b/c'. Find the full error at: https://bit.ly/3cXEKWf"`;
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`ES5 plugins should throw if no proxies are available error when using ES5 1`] = `"[Immer] minified error nr: 19 ES5. Find the full error at: https://bit.ly/3cXEKWf"`;
3+
exports[`ES5 plugins should throw if no proxies are available error when using ES5 1`] = `"[Immer] minified error nr: 18 'ES5'. Find the full error at: https://bit.ly/3cXEKWf"`;
44

5-
exports[`error when using Maps 1`] = `"[Immer] minified error nr: 19 MapSet. Find the full error at: https://bit.ly/3cXEKWf"`;
5+
exports[`error when using Maps 1`] = `"[Immer] minified error nr: 18 'MapSet'. Find the full error at: https://bit.ly/3cXEKWf"`;
66

7-
exports[`error when using patches - 1 1`] = `"[Immer] minified error nr: 19 Patches. Find the full error at: https://bit.ly/3cXEKWf"`;
7+
exports[`error when using patches - 1 1`] = `"[Immer] minified error nr: 18 'Patches'. Find the full error at: https://bit.ly/3cXEKWf"`;
88

9-
exports[`error when using patches - 2 1`] = `"[Immer] minified error nr: 19 Patches. Find the full error at: https://bit.ly/3cXEKWf"`;
9+
exports[`error when using patches - 2 1`] = `"[Immer] minified error nr: 18 'Patches'. Find the full error at: https://bit.ly/3cXEKWf"`;
1010

11-
exports[`error when using patches - 3 1`] = `"[Immer] minified error nr: 19 Patches. Find the full error at: https://bit.ly/3cXEKWf"`;
11+
exports[`error when using patches - 3 1`] = `"[Immer] minified error nr: 18 'Patches'. Find the full error at: https://bit.ly/3cXEKWf"`;

‎__tests__/base.js

+56-51
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jest.setTimeout(1000)
1616

1717
enableAllPlugins()
1818

19+
const isProd = process.env.NODE_ENV === "production"
20+
1921
test("immer should have no dependencies", () => {
2022
expect(require("../package.json").dependencies).toBeUndefined()
2123
})
@@ -1145,65 +1147,66 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
11451147
})
11461148

11471149
// NOTE: ES5 drafts only protect existing properties when revoked.
1148-
it("revokes the draft once produce returns", () => {
1149-
const expectRevoked = (fn, shouldThrow = true) => {
1150-
if (shouldThrow) expect(fn).toThrowErrorMatchingSnapshot()
1151-
else expect(fn).not.toThrow()
1152-
}
1150+
if (!isProd)
1151+
it("revokes the draft once produce returns", () => {
1152+
const expectRevoked = (fn, shouldThrow = true) => {
1153+
if (shouldThrow) expect(fn).toThrowErrorMatchingSnapshot()
1154+
else expect(fn).not.toThrow()
1155+
}
11531156

1154-
// Test object drafts:
1155-
let draft
1156-
produce({a: 1, b: 1}, s => {
1157-
draft = s
1158-
delete s.b
1159-
})
1157+
// Test object drafts:
1158+
let draft
1159+
produce({a: 1, b: 1}, s => {
1160+
draft = s
1161+
delete s.b
1162+
})
11601163

1161-
// Access known property on object draft.
1162-
expectRevoked(() => {
1163-
draft.a
1164-
})
1164+
// Access known property on object draft.
1165+
expectRevoked(() => {
1166+
draft.a
1167+
})
11651168

1166-
// Assign known property on object draft.
1167-
expectRevoked(() => {
1168-
draft.a = true
1169-
})
1169+
// Assign known property on object draft.
1170+
expectRevoked(() => {
1171+
draft.a = true
1172+
})
11701173

1171-
// Access unknown property on object draft.
1172-
expectRevoked(() => {
1173-
draft.z
1174-
}, useProxies)
1174+
// Access unknown property on object draft.
1175+
expectRevoked(() => {
1176+
draft.z
1177+
}, useProxies)
11751178

1176-
// Assign unknown property on object draft.
1177-
expectRevoked(() => {
1178-
draft.z = true
1179-
}, useProxies)
1179+
// Assign unknown property on object draft.
1180+
expectRevoked(() => {
1181+
draft.z = true
1182+
}, useProxies)
11801183

1181-
// Test array drafts:
1182-
produce([1, 2], s => {
1183-
draft = s
1184-
s.pop()
1185-
})
1184+
// Test array drafts:
1185+
produce([1, 2], s => {
1186+
draft = s
1187+
s.pop()
1188+
})
11861189

1187-
// Access known index of an array draft.
1188-
expectRevoked(() => {
1189-
draft[0]
1190-
})
1190+
// Access known index of an array draft.
1191+
expectRevoked(() => {
1192+
draft[0]
1193+
})
11911194

1192-
// Assign known index of an array draft.
1193-
expectRevoked(() => {
1194-
draft[0] = true
1195-
})
1195+
// Assign known index of an array draft.
1196+
expectRevoked(() => {
1197+
draft[0] = true
1198+
})
11961199

1197-
// Access unknown index of an array draft.
1198-
expectRevoked(() => {
1199-
draft[1]
1200-
}, useProxies)
1200+
// Access unknown index of an array draft.
1201+
expectRevoked(() => {
1202+
draft[1]
1203+
}, useProxies)
12011204

1202-
// Assign unknown index of an array draft.
1203-
expectRevoked(() => {
1204-
draft[1] = true
1205-
}, useProxies)
1206-
})
1205+
// Assign unknown index of an array draft.
1206+
expectRevoked(() => {
1207+
draft[1] = true
1208+
}, useProxies)
1209+
})
12071210

12081211
it("can access a child draft that was created before the draft was modified", () => {
12091212
produce({a: {}}, s => {
@@ -1697,7 +1700,7 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
16971700
},
16981701
e => {
16991702
expect(e).toBe(err)
1700-
expect(() => draft.a).toThrowErrorMatchingSnapshot()
1703+
if (!isProd) expect(() => draft.a).toThrowErrorMatchingSnapshot()
17011704
}
17021705
)
17031706
})
@@ -2418,7 +2421,9 @@ function testLiteralTypes(produce) {
24182421
draft.foo = true
24192422
})
24202423
).toThrowError(
2421-
"produce can only be called on things that are draftable"
2424+
isProd
2425+
? "[Immer] minified error nr: 21"
2426+
: "produce can only be called on things that are draftable"
24222427
)
24232428
})
24242429
} else {

‎__tests__/current.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ enableAllPlugins()
1414
runTests("proxy", true)
1515
runTests("es5", false)
1616

17+
const isProd = process.env.NODE_ENV === "production"
18+
1719
function runTests(name, useProxies) {
1820
describe("current - " + name, () => {
1921
beforeAll(() => {
@@ -24,7 +26,11 @@ function runTests(name, useProxies) {
2426
it("must be called on draft", () => {
2527
expect(() => {
2628
current({})
27-
}).toThrowError("[Immer] 'current' expects a draft, got: [object Object]")
29+
}).toThrowError(
30+
isProd
31+
? "[Immer] minified error nr: 22 '[object Object]'. Find the full error at: https://bit.ly/3cXEKWf"
32+
: "[Immer] 'current' expects a draft, got: [object Object]"
33+
)
2834
})
2935

3036
it("can handle simple arrays", () => {

‎__tests__/manual.js

+12-9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010

1111
enableAllPlugins()
1212

13+
const isProd = process.env.NODE_ENV === "production"
14+
1315
runTests("proxy", true)
1416
runTests("es5", false)
1517

@@ -41,16 +43,17 @@ function runTests(name, useProxies) {
4143
expect(state).toEqual([{}, {}, {}])
4244
})
4345

44-
it("cannot modify after finish", () => {
45-
const state = {a: 1}
46+
if (!isProd)
47+
it("cannot modify after finish", () => {
48+
const state = {a: 1}
4649

47-
const draft = createDraft(state)
48-
draft.a = 2
49-
expect(finishDraft(draft)).toEqual({a: 2})
50-
expect(() => {
51-
draft.a = 3
52-
}).toThrowErrorMatchingSnapshot()
53-
})
50+
const draft = createDraft(state)
51+
draft.a = 2
52+
expect(finishDraft(draft)).toEqual({a: 2})
53+
expect(() => {
54+
draft.a = 3
55+
}).toThrowErrorMatchingSnapshot()
56+
})
5457

5558
it("should support patches drafts", () => {
5659
const state = {a: 1}

‎__tests__/map-set.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
original,
66
isDraft,
77
immerable,
8-
enableAllPlugins
8+
enableAllPlugins,
9+
enableMapSet
910
} from "../src/immer"
1011
import {each, shallowCopy, isEnumerable, DRAFT_STATE} from "../src/common"
1112

@@ -280,5 +281,19 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
280281
})
281282
expect(result).toBe(set)
282283
})
284+
285+
test("#692 - idempotent plugin loading", () => {
286+
let mapType1
287+
produce(new Map(), draft => {
288+
mapType1 = draft.constructor
289+
})
290+
291+
enableMapSet()
292+
let mapType2
293+
produce(new Map(), draft => {
294+
mapType2 = draft.constructor
295+
})
296+
expect(mapType1).toBe(mapType2)
297+
})
283298
})
284299
}

‎__tests__/original.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import produce, {original, setUseProxies, enableAllPlugins} from "../src/immer"
33

44
enableAllPlugins()
55

6+
const isProd = process.env.NODE_ENV === "production"
7+
68
describe("original", () => {
79
const baseState = {
810
a: [],
@@ -40,20 +42,28 @@ describe("original", () => {
4042
draftState.c = {}
4143
draftState.d = 3
4244
expect(() => original(draftState.c)).toThrowErrorMatchingInlineSnapshot(
43-
`"[Immer] 'original' expects a draft, got: [object Object]"`
45+
isProd
46+
? `"[Immer] minified error nr: 23 '[object Object]'. Find the full error at: https://bit.ly/3cXEKWf"`
47+
: `"[Immer] 'original' expects a draft, got: [object Object]"`
4448
)
4549
expect(() => original(draftState.d)).toThrowErrorMatchingInlineSnapshot(
46-
`"[Immer] 'original' expects a draft, got: 3"`
50+
isProd
51+
? `"[Immer] minified error nr: 23 '3'. Find the full error at: https://bit.ly/3cXEKWf"`
52+
: `"[Immer] 'original' expects a draft, got: 3"`
4753
)
4854
})
4955
})
5056

5157
it("should return undefined for an object that is not proxied", () => {
5258
expect(() => original({})).toThrowErrorMatchingInlineSnapshot(
53-
`"[Immer] 'original' expects a draft, got: [object Object]"`
59+
isProd
60+
? `"[Immer] minified error nr: 23 '[object Object]'. Find the full error at: https://bit.ly/3cXEKWf"`
61+
: `"[Immer] 'original' expects a draft, got: [object Object]"`
5462
)
5563
expect(() => original(3)).toThrowErrorMatchingInlineSnapshot(
56-
`"[Immer] 'original' expects a draft, got: 3"`
64+
isProd
65+
? `"[Immer] minified error nr: 23 '3'. Find the full error at: https://bit.ly/3cXEKWf"`
66+
: `"[Immer] 'original' expects a draft, got: 3"`
5767
)
5868
})
5969
})

‎docs/performance.md

+15-3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ Most important observation:
4242

4343
## Performance tips
4444

45-
- 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.
46-
- Immer will convert anything you read in a draft recursively into a draft as well. If you have expensive side effect free operations on a draft that involves a lot of reading, for example finding an index using `find(Index)` in a very large array, you can speed this up by first doing the search, and only call the `produce` function once you know the index. Thereby preventing Immer to turn everything that was searched for in a draft. Or, perform the search on the original value of a draft, by using `original(someDraft)`, which boils to the same thing.
47-
- Always try to pull produce 'up', for example `for (let x of y) produce(base, d => d.push(x))` is exponentially slower than `produce(base, d => { for (let x of y) d.push(x)})`
45+
### Pre-freeze data
46+
47+
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.
48+
49+
### You can always opt-out
50+
51+
Realize that immer is opt-in everywhere, so it is perfectly fine to manually write super performance critical reducers, and use immer for all the normal ones. Even from within a producer you opt-out from Immer for certain parts of your logic by using utilies `original` or `current` and perform some of your operations on plain JavaScript objects.
52+
53+
### For expensive search operations, read from the original state, not the draft
54+
55+
Immer will convert anything you read in a draft recursively into a draft as well. If you have expensive side effect free operations on a draft that involves a lot of reading, for example finding an index using `find(Index)` in a very large array, you can speed this up by first doing the search, and only call the `produce` function once you know the index. Thereby preventing Immer to turn everything that was searched for in a draft. Or, alternatively, perform the search on the original value of a draft, by using `original(someDraft)`, which boils to the same thing.
56+
57+
### Pull produce as far up as possible
58+
59+
Always try to pull produce 'up', for example `for (let x of y) produce(base, d => d.push(x))` is exponentially slower than `produce(base, d => { for (let x of y) d.push(x)})`

‎docs/pitfalls.md

+47-10
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,37 @@ title: Pitfalls
77
<div data-ea-publisher="immerjs" data-ea-type="image" class="horizontal bordered"></div>
88
</center>
99

10-
1. For performance tips, see [Performance Tips](https://immerjs.github.io/immer/docs/performance/#performance-tips).
11-
1. Don't redefine draft like, `draft = myCoolNewState`. Instead, either modify the `draft` or return a new state. See [Returning data from producers](https://immerjs.github.io/immer/docs/return).
12-
1. Immer assumes your state to be a unidirectional tree. That is, no object should appear twice in the tree, and there should be no circular references.
13-
1. Since Immer uses proxies, reading huge amounts of data from state comes with an overhead (especially in the ES5 implementation). If this ever becomes an issue (measure before you optimize!), do the current state analysis before entering the producer function or read from the `currentState` rather than the `draftState`. Also, realize that immer is opt-in everywhere, so it is perfectly fine to manually write super performance critical reducers, and use immer for all the normal ones. Also note that `original` can be used to get the original state of an object, which is cheaper to read.
14-
1. It is possible to return values from producers, except, it is not possible to return `undefined` that way, as it is indistinguishable from not updating the draft at all! If you want to replace the draft with `undefined`, just return `nothing` from the producer.
15-
1. Immer [does not support exotic objects](https://github.com/immerjs/immer/issues/504) such as window.location.
16-
1. You will need to enable your own classes to work properly with Immer. For docs on the topic, check out the section on [working with complex objects](https://immerjs.github.io/immer/docs/complex-objects).
17-
1. For arrays, only numeric properties and the `length` property can be mutated. Custom properties are not preserved on arrays.
18-
1. Note that data that comes from the closure, and not from the base state, will never be drafted, even when the data has become part of the new draft:
19-
1. The set of patches generated by Immer should be correct, that is, applying them to an equal base object should result in the same end state. However Immer does not guarantee the generated set of patches will be optimal, that is, the minimum set of patches possible.
10+
### Performance tipes
11+
12+
For performance tips, see [Performance Tips](https://immerjs.github.io/immer/docs/performance/#performance-tips).
13+
14+
### Don't reassign the recipe argument
15+
16+
Never reassign the `draft` argument (example: `draft = myCoolNewState`). Instead, either modify the `draft` or return a new state. See [Returning data from producers](https://immerjs.github.io/immer/docs/return).
17+
18+
### Immer only supports unidirectional trees
19+
20+
Immer assumes your state to be a unidirectional tree. That is, no object should appear twice in the tree, there should be no circular references. There should be exactly one path from the root to any node of the tree.
21+
22+
### Never explicitly return `undefined` from a producer
23+
24+
It is possible to return values from producers, except, it is not possible to return `undefined` that way, as it is indistinguishable from not updating the draft at all! If you want to replace the draft with `undefined`, just return `nothing` from the producer.
25+
26+
### Don't mutate exotic objects
27+
28+
Immer [does not support exotic objects](https://github.com/immerjs/immer/issues/504) such as window.location.
29+
30+
### Classes should be made draftable or not mutated
31+
32+
You will need to enable your own classes to work properly with Immer. For docs on the topic, check out the section on [working with complex objects](https://immerjs.github.io/immer/docs/complex-objects).
33+
34+
### Only valid indices and length can be mutated on Arrays
35+
36+
For arrays, only numeric properties and the `length` property can be mutated. Custom properties are not preserved on arrays.
37+
38+
### Data not originating from the state will never be drafted
39+
40+
Note that data that comes from the closure, and not from the base state, will never be drafted, even when the data has become part of the new draft.
2041

2142
```javascript
2243
function onReceiveTodo(todo) {
@@ -33,3 +54,19 @@ function onReceiveTodo(todo) {
3354
})
3455
}
3556
```
57+
58+
### Immer patches are not necessarily optimal
59+
60+
The set of patches generated by Immer should be correct, that is, applying them to an equal base object should result in the same end state. However Immer does not guarantee the generated set of patches will be optimal, that is, the minimum set of patches possible.
61+
62+
### Always use the result of nested producers
63+
64+
Nested `produce` calls are supported, but note that `produce` will _always_ produce a new state, so even when passing a draft to a nested produce, the changes made by the inner produce won't be visibile in the draft that was passed it, but only in the output that is produced. In other words, when using nested produce, you get a draft of a draft and the result of the inner produce should be merged back into the original draft (or returned). For example `produce(state, draft => { produce(draft.user, userDraft => { userDraft.name += "!" })})` won't work as the output if the inner produce isn't used. The correct way to use nested producers is:
65+
66+
```javascript
67+
produce(state, draft => {
68+
draft.user = produce(draft.user, userDraft => {
69+
userDraft.name += "!"
70+
})
71+
})
72+
```

‎ignoreObseleteSnapshots.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = function(results) {
2+
// don't count obselete snapshot as a failure, but just check if there are no failing tests
3+
// console.dir(results)
4+
results.success = results.testResults.every(r => r.numFailingTests === 0)
5+
return results
6+
}

‎jest.config.build.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ module.exports = {
1414
preset: "ts-jest/presets/js-with-ts",
1515
testEnvironment: "node",
1616
testMatch: ["**/__tests__/**/*.[jt]s?(x)"],
17-
snapshotResolver: "<rootDir>/jest.config.build.snapshots.js"
17+
snapshotResolver: "<rootDir>/jest.config.build.snapshots.js",
18+
testResultsProcessor: "<rootDir>/ignoreObseleteSnapshots.js"
1819
}

‎src/utils/errors.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const errors = {
2929
18(plugin: string) {
3030
return `The plugin for '${plugin}' has not been loaded into Immer. To enable the plugin, import and call \`enable${plugin}()\` when initializing your application.`
3131
},
32-
19: "plugin not loaded",
3332
20: "Cannot use proxies if Proxy, Proxy.revocable or Reflect are not available",
3433
21(thing: string) {
3534
return `produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '${thing}'`
@@ -54,7 +53,7 @@ export function die(error: keyof typeof errors, ...args: any[]): never {
5453
}
5554
throw new Error(
5655
`[Immer] minified error nr: ${error}${
57-
args.length ? " " + args.join(",") : ""
56+
args.length ? " " + args.map(s => `'${s}'`).join(",") : ""
5857
}. Find the full error at: https://bit.ly/3cXEKWf`
5958
)
6059
}

‎src/utils/plugins.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function getPlugin<K extends keyof Plugins>(
5252
): Exclude<Plugins[K], undefined> {
5353
const plugin = plugins[pluginKey]
5454
if (!plugin) {
55-
die(__DEV__ ? 18 : 19, pluginKey)
55+
die(18, pluginKey)
5656
}
5757
// @ts-ignore
5858
return plugin
@@ -62,7 +62,7 @@ export function loadPlugin<K extends keyof Plugins>(
6262
pluginKey: K,
6363
implementation: Plugins[K]
6464
): void {
65-
plugins[pluginKey] = implementation
65+
if (!plugins[pluginKey]) plugins[pluginKey] = implementation
6666
}
6767

6868
/** ES5 Plugin */

0 commit comments

Comments
 (0)
Please sign in to comment.