diff --git a/__tests__/base.js b/__tests__/base.js index 43a57d6f..9786b2ab 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -1,6 +1,6 @@ "use strict" import {Immer, nothing, original, isDraft, immerable} from "../src/index" -import {each, shallowCopy, isEnumerable, DRAFT_STATE} from "../src/common" +import {each, shallowCopy, isEnumerable, DRAFT_STATE} from "../src/internal" import deepFreeze from "deep-freeze" import cloneDeep from "lodash.clonedeep" import * as lodash from "lodash" @@ -582,6 +582,13 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) { expect(entries[0][0]).toBe(key) expect(entries[0][0].a).toBe(2) }) + + it("does support instanceof Map", () => { + const map = new Map() + produce(map, d => { + expect(d instanceof Map).toBeTruthy() + }) + }) }) describe("set drafts", () => { @@ -849,6 +856,13 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) { "Cannot use a proxy that has been revoked" ) }) + + it("does support instanceof Set", () => { + const set = new Set() + produce(set, d => { + expect(d instanceof Set).toBeTruthy() + }) + }) }) it("supports `immerable` symbol on constructor", () => { diff --git a/__tests__/empty.ts b/__tests__/empty.ts index 9b0fad1b..70f4d47e 100644 --- a/__tests__/empty.ts +++ b/__tests__/empty.ts @@ -1,5 +1,5 @@ import {produce, produceWithPatches, setUseProxies} from "../src" -import {DRAFT_STATE} from "../src/common" +import {DRAFT_STATE} from "../src/internal" test("empty stub test", () => { expect(true).toBe(true) @@ -10,6 +10,7 @@ describe("map set - es5", () => { setUseProxies(false) const baseState = new Map([["x", 1]]) + debugger const nextState = produce(baseState, s => { s.set("x", 2) }) diff --git a/__tests__/polyfills.js b/__tests__/polyfills.js index 53a1415c..b56d5147 100644 --- a/__tests__/polyfills.js +++ b/__tests__/polyfills.js @@ -7,7 +7,7 @@ Object.assign = undefined Reflect.ownKeys = undefined jest.resetModules() -const common = require("../src/common") +const common = require("../src/internal") // Reset the globals to avoid unintended effects. Symbol = SymbolConstructor diff --git a/src/common.ts b/src/common.ts index 370384c1..69e3f0b9 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,4 +1,7 @@ import { + DRAFT_STATE, + DRAFTABLE, + hasSet, Objectish, Drafted, AnyObject, @@ -7,46 +10,9 @@ import { AnySet, ImmerState, ProxyType, - Archtype -} from "./types" - -/** Use a class type for `nothing` so its type is unique */ -export class Nothing { - // This lets us do `Exclude` - // @ts-ignore - private _!: unique symbol -} - -const hasSymbol = typeof Symbol !== "undefined" -export const hasMap = typeof Map !== "undefined" -export const hasSet = typeof Set !== "undefined" - -/** - * The sentinel value returned by producers to replace the draft with undefined. - */ -export const NOTHING: Nothing = hasSymbol - ? Symbol("immer-nothing") - : ({["immer-nothing"]: true} as any) - -/** - * To let Immer treat your class instances as plain immutable objects - * (albeit with a custom prototype), you must define either an instance property - * or a static property on each of your custom classes. - * - * Otherwise, your class instance will never be drafted, which means it won't be - * safe to mutate in a produce callback. - */ -export const DRAFTABLE: unique symbol = hasSymbol - ? Symbol("immer-draftable") - : ("__$immer_draftable" as any) - -export const DRAFT_STATE: unique symbol = hasSymbol - ? Symbol("immer-state") - : ("__$immer_state" as any) - -export const iteratorSymbol: typeof Symbol.iterator = hasSymbol - ? Symbol.iterator - : ("@@iterator" as any) + Archtype, + hasMap +} from "./internal" /** Returns true if the given value is an Immer draft */ export function isDraft(value: any): boolean { diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 00000000..ec21a739 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,40 @@ +// Should be no imports here! + +// SOme things that should be evaluated before all else... +const hasSymbol = typeof Symbol !== "undefined" +export const hasMap = typeof Map !== "undefined" +export const hasSet = typeof Set !== "undefined" + +/** + * The sentinel value returned by producers to replace the draft with undefined. + */ +export const NOTHING: Nothing = hasSymbol + ? Symbol("immer-nothing") + : ({["immer-nothing"]: true} as any) + +/** + * To let Immer treat your class instances as plain immutable objects + * (albeit with a custom prototype), you must define either an instance property + * or a static property on each of your custom classes. + * + * Otherwise, your class instance will never be drafted, which means it won't be + * safe to mutate in a produce callback. + */ +export const DRAFTABLE: unique symbol = hasSymbol + ? Symbol("immer-draftable") + : ("__$immer_draftable" as any) + +export const DRAFT_STATE: unique symbol = hasSymbol + ? Symbol("immer-state") + : ("__$immer_state" as any) + +export const iteratorSymbol: typeof Symbol.iterator = hasSymbol + ? Symbol.iterator + : ("@@iterator" as any) + +/** Use a class type for `nothing` so its type is unique */ +export class Nothing { + // This lets us do `Exclude` + // @ts-ignore + private _!: unique symbol +} diff --git a/src/es5.ts b/src/es5.ts index f7ac428b..7cd58e20 100644 --- a/src/es5.ts +++ b/src/es5.ts @@ -7,23 +7,20 @@ import { isDraftable, isEnumerable, shallowCopy, - DRAFT_STATE, latest, - createHiddenProperty -} from "./common" - -import {ImmerScope} from "./scope" -import { + createHiddenProperty, + ImmerScope, ImmerState, Drafted, AnyObject, Objectish, ImmerBaseState, AnyArray, - ProxyType -} from "./types" -import {MapState} from "./map" -import {SetState} from "./set" + ProxyType, + MapState, + SetState, + DRAFT_STATE +} from "./internal" interface ES5BaseState extends ImmerBaseState { finalizing: boolean diff --git a/src/extends.ts b/src/extends.ts new file mode 100644 index 00000000..115ad2ef --- /dev/null +++ b/src/extends.ts @@ -0,0 +1,23 @@ +var extendStatics = function(d: any, b: any): any { + extendStatics = + Object.setPrototypeOf || + ({__proto__: []} instanceof Array && + function(d, b) { + d.__proto__ = b + }) || + function(d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p] + } + return extendStatics(d, b) +} + +// Ugly hack to resolve #502 and inherit built in Map / Set +export function __extends(d: any, b: any): any { + extendStatics(d, b) + function __(this: any): any { + this.constructor = d + } + d.prototype = + // @ts-ignore + ((__.prototype = b.prototype), new __()) +} diff --git a/src/finalize.ts b/src/finalize.ts index 4ecaf10e..e57b6235 100644 --- a/src/finalize.ts +++ b/src/finalize.ts @@ -1,21 +1,25 @@ -import {Immer} from "./immer" -import {ImmerState, Drafted, ProxyType} from "./types" -import {ImmerScope} from "./scope" import { - isSet, - has, - is, - get, - each, + Immer, + ImmerScope, DRAFT_STATE, + isDraftable, NOTHING, + Drafted, + PatchPath, + ProxyType, + each, + has, freeze, + generatePatches, shallowCopy, - set -} from "./common" -import {isDraft, isDraftable} from "./index" -import {SetState} from "./set" -import {generatePatches, PatchPath} from "./patches" + ImmerState, + isSet, + isDraft, + SetState, + set, + is, + get +} from "./internal" export function processResult(immer: Immer, result: any, scope: ImmerScope) { const baseDraft = scope.drafts![0] diff --git a/src/immer.ts b/src/immer.ts index d777d67a..e2eb5cbf 100644 --- a/src/immer.ts +++ b/src/immer.ts @@ -1,31 +1,32 @@ -import {createES5Proxy, willFinalizeES5, markChangedES5} from "./es5" -import {createProxy, markChanged} from "./proxy" - -import {applyPatches} from "./patches" import { + createES5Proxy, + willFinalizeES5, + markChangedES5, + IProduceWithPatches, + IProduce, + ImmerState, each, - isDraft, - isSet, - isMap, + Drafted, isDraftable, - DRAFT_STATE, + ImmerScope, + processResult, NOTHING, - die -} from "./common" -import {ImmerScope} from "./scope" -import { - ImmerState, - IProduce, - IProduceWithPatches, + maybeFreeze, + die, + Patch, Objectish, - PatchListener, + DRAFT_STATE, Draft, - Patch, - Drafted -} from "./types" -import {proxyMap} from "./map" -import {proxySet} from "./set" -import {processResult, maybeFreeze} from "./finalize" + PatchListener, + isDraft, + applyPatches, + isMap, + proxyMap, + isSet, + proxySet, + createProxy, + markChanged +} from "./internal" /* istanbul ignore next */ function verifyMinified() {} diff --git a/src/index.ts b/src/index.ts index 7733b500..88c318ac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,16 @@ -import {Immer} from "./immer" -import {IProduce, IProduceWithPatches} from "./types" +import {IProduce, IProduceWithPatches, Immer} from "./internal" -export {Draft, Immutable, Patch, PatchListener} from "./types" +export { + Draft, + Immutable, + Patch, + PatchListener, + original, + isDraft, + isDraftable, + NOTHING as nothing, + DRAFTABLE as immerable +} from "./internal" const immer = new Immer() @@ -73,12 +82,4 @@ export const createDraft = immer.createDraft.bind(immer) */ export const finishDraft = immer.finishDraft.bind(immer) -export { - original, - isDraft, - isDraftable, - NOTHING as nothing, - DRAFTABLE as immerable -} from "./common" - export {Immer} diff --git a/src/internal.ts b/src/internal.ts new file mode 100644 index 00000000..ceb7e1c3 --- /dev/null +++ b/src/internal.ts @@ -0,0 +1,12 @@ +export * from "./env" +export * from "./extends" +export * from "./types" +export * from "./common" +export * from "./scope" +export * from "./finalize" +export * from "./proxy" +export * from "./es5" +export * from "./map" +export * from "./set" +export * from "./patches" +export * from "./immer" diff --git a/src/map.ts b/src/map.ts index e46a8f7b..494364b0 100644 --- a/src/map.ts +++ b/src/map.ts @@ -1,8 +1,17 @@ -import {isDraftable, DRAFT_STATE, latest, iteratorSymbol} from "./common" - -import {ImmerScope} from "./scope" -import {AnyMap, Drafted, ImmerState, ImmerBaseState, ProxyType} from "./types" -import {assertUnrevoked} from "./es5" +import { + __extends, + ImmerBaseState, + ProxyType, + AnyMap, + Drafted, + ImmerState, + DRAFT_STATE, + ImmerScope, + latest, + assertUnrevoked, + isDraftable, + iteratorSymbol +} from "./internal" export interface MapState extends ImmerBaseState { type: ProxyType.Map @@ -13,15 +22,14 @@ export interface MapState extends ImmerBaseState { draft: Drafted } -// Make sure DraftMap declarion doesn't die if Map is not avialable... -/* istanbul ignore next */ -const MapBase: MapConstructor = - typeof Map !== "undefined" ? Map : (function FakeMap() {} as any) - -export class DraftMap extends MapBase implements Map { - [DRAFT_STATE]: MapState - constructor(target: AnyMap, parent?: ImmerState) { - super() +const DraftMap = (function(_super) { + if (!_super) { + /* istanbul ignore next */ + throw new Error("Map is not polyfilled") + } + __extends(DraftMap, _super) + // Create class manually, cause #502 + function DraftMap(this: any, target: AnyMap, parent?: ImmerState): any { this[DRAFT_STATE] = { type: ProxyType.Map, parent, @@ -31,21 +39,28 @@ export class DraftMap extends MapBase implements Map { copy: undefined, assigned: undefined, base: target, - draft: this, + draft: this as any, isManual: false, revoked: false } + return this } - - get size(): number { - return latest(this[DRAFT_STATE]).size - } - - has(key: K): boolean { + const p = DraftMap.prototype + + // TODO: smaller build size if we create a util for Object.defineProperty + Object.defineProperty(p, "size", { + get: function() { + return latest(this[DRAFT_STATE]).size + }, + enumerable: true, + configurable: true + }) + + p.has = function(key: any): boolean { return latest(this[DRAFT_STATE]).has(key) } - set(key: K, value: V): this { + p.set = function(key: any, value: any) { const state = this[DRAFT_STATE] assertUnrevoked(state) if (latest(state).get(key) !== value) { @@ -58,7 +73,7 @@ export class DraftMap extends MapBase implements Map { return this } - delete(key: K): boolean { + p.delete = function(key: any): boolean { if (!this.has(key)) { return false } @@ -72,7 +87,7 @@ export class DraftMap extends MapBase implements Map { return true } - clear() { + p.clear = function() { const state = this[DRAFT_STATE] assertUnrevoked(state) prepareCopy(state) @@ -84,14 +99,17 @@ export class DraftMap extends MapBase implements Map { return state.copy!.clear() } - forEach(cb: (value: V, key: K, self: this) => void, thisArg?: any) { + p.forEach = function( + cb: (value: any, key: any, self: any) => void, + thisArg?: any + ) { const state = this[DRAFT_STATE] - latest(state).forEach((_value: V, key: K, _map: this) => { + latest(state).forEach((_value: any, key: any, _map: any) => { cb.call(thisArg, this.get(key), key, this) }) } - get(key: K): V { + p.get = function(key: any): any { const state = this[DRAFT_STATE] assertUnrevoked(state) const value = latest(state).get(key) @@ -108,11 +126,11 @@ export class DraftMap extends MapBase implements Map { return draft } - keys(): IterableIterator { + p.keys = function(): IterableIterator { return latest(this[DRAFT_STATE]).keys() } - values(): IterableIterator { + p.values = function(): IterableIterator { const iterator = this.keys() return { [iteratorSymbol]: () => this.values(), @@ -129,7 +147,7 @@ export class DraftMap extends MapBase implements Map { } as any } - entries(): IterableIterator<[K, V]> { + p.entries = function(): IterableIterator<[any, any]> { const iterator = this.keys() return { [iteratorSymbol]: () => this.entries(), @@ -146,12 +164,18 @@ export class DraftMap extends MapBase implements Map { } as any } - [iteratorSymbol]() { + p[iteratorSymbol] = function() { return this.entries() } -} -export function proxyMap(target: AnyMap, parent?: ImmerState) { + return DraftMap +})(Map) + +export function proxyMap( + target: T, + parent?: ImmerState +): T & {[DRAFT_STATE]: MapState} { + // @ts-ignore return new DraftMap(target, parent) } diff --git a/src/patches.ts b/src/patches.ts index 28892c64..74135fd5 100644 --- a/src/patches.ts +++ b/src/patches.ts @@ -1,9 +1,21 @@ -import {get, each, isMap, has, die, getArchtype} from "./common" -import {Patch, ImmerState, ProxyType, Archtype} from "./types" -import {SetState} from "./set" -import {ES5ArrayState, ES5ObjectState} from "./es5" -import {ProxyArrayState, ProxyObjectState} from "./proxy" -import {MapState} from "./map" +import { + get, + each, + has, + die, + getArchtype, + ImmerState, + Patch, + ProxyType, + SetState, + ES5ArrayState, + ProxyArrayState, + MapState, + ES5ObjectState, + ProxyObjectState, + Archtype, + isMap +} from "./internal" export type PatchPath = (string | number)[] diff --git a/src/proxy.ts b/src/proxy.ts index 009dde04..0a203cba 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -5,19 +5,17 @@ import { is, isDraftable, shallowCopy, - DRAFT_STATE, - latest -} from "./common" -import {ImmerScope} from "./scope" -import { - AnyObject, - Drafted, + latest, + ImmerBaseState, ImmerState, + Drafted, + ProxyType, + AnyObject, AnyArray, Objectish, - ImmerBaseState, - ProxyType -} from "./types" + ImmerScope, + DRAFT_STATE +} from "./internal" interface ProxyBaseState extends ImmerBaseState { assigned: { diff --git a/src/scope.ts b/src/scope.ts index 9c66a91d..c881c413 100644 --- a/src/scope.ts +++ b/src/scope.ts @@ -1,6 +1,11 @@ -import {DRAFT_STATE} from "./common" -import {Patch, PatchListener, Drafted, ProxyType} from "./types" -import {Immer} from "./immer" +import { + Patch, + PatchListener, + Drafted, + ProxyType, + Immer, + DRAFT_STATE +} from "./internal" /** Each scope represents a `produce` call. */ export class ImmerScope { diff --git a/src/set.ts b/src/set.ts index a952b4af..866de604 100644 --- a/src/set.ts +++ b/src/set.ts @@ -1,8 +1,17 @@ -import {DRAFT_STATE, latest, isDraftable, iteratorSymbol} from "./common" - -import {ImmerScope} from "./scope" -import {AnySet, Drafted, ImmerState, ImmerBaseState, ProxyType} from "./types" -import {assertUnrevoked} from "./es5" +import { + __extends, + ImmerBaseState, + ProxyType, + AnySet, + Drafted, + ImmerState, + DRAFT_STATE, + ImmerScope, + latest, + assertUnrevoked, + iteratorSymbol, + isDraftable +} from "./internal" export interface SetState extends ImmerBaseState { type: ProxyType.Set @@ -13,15 +22,14 @@ export interface SetState extends ImmerBaseState { draft: Drafted } -// Make sure DraftSet declarion doesn't die if Map is not avialable... -/* istanbul ignore next */ -const SetBase: SetConstructor = - typeof Set !== "undefined" ? Set : (function FakeSet() {} as any) - -export class DraftSet extends SetBase implements Set { - [DRAFT_STATE]: SetState - constructor(target: AnySet, parent?: ImmerState) { - super() +const DraftSet = (function(_super) { + if (!_super) { + /* istanbul ignore next */ + throw new Error("Set is not polyfilled") + } + __extends(DraftSet, _super) + // Create class manually, cause #502 + function DraftSet(this: any, target: AnySet, parent?: ImmerState) { this[DRAFT_STATE] = { type: ProxyType.Set, parent, @@ -35,13 +43,19 @@ export class DraftSet extends SetBase implements Set { revoked: false, isManual: false } + return this } + const p = DraftSet.prototype - get size(): number { - return latest(this[DRAFT_STATE]).size - } + Object.defineProperty(p, "size", { + get: function() { + return latest(this[DRAFT_STATE]).size + }, + enumerable: true, + configurable: true + }) - has(value: V): boolean { + p.has = function(value: any): boolean { const state = this[DRAFT_STATE] assertUnrevoked(state) // bit of trickery here, to be able to recognize both the value, and the draft of its value @@ -54,7 +68,7 @@ export class DraftSet extends SetBase implements Set { return false } - add(value: V): this { + p.add = function(value: any): any { const state = this[DRAFT_STATE] assertUnrevoked(state) if (state.copy) { @@ -67,7 +81,7 @@ export class DraftSet extends SetBase implements Set { return this } - delete(value: V): boolean { + p.delete = function(value: any): any { if (!this.has(value)) { return false } @@ -84,7 +98,7 @@ export class DraftSet extends SetBase implements Set { ) } - clear() { + p.clear = function() { const state = this[DRAFT_STATE] assertUnrevoked(state) prepareCopy(state) @@ -92,29 +106,29 @@ export class DraftSet extends SetBase implements Set { return state.copy!.clear() } - values(): IterableIterator { + p.values = function(): IterableIterator { const state = this[DRAFT_STATE] assertUnrevoked(state) prepareCopy(state) return state.copy!.values() } - entries(): IterableIterator<[V, V]> { + p.entries = function entries(): IterableIterator<[any, any]> { const state = this[DRAFT_STATE] assertUnrevoked(state) prepareCopy(state) return state.copy!.entries() } - keys(): IterableIterator { + p.keys = function(): IterableIterator { return this.values() } - [iteratorSymbol]() { + p[iteratorSymbol] = function() { return this.values() } - forEach(cb: (value: V, key: V, self: this) => void, thisArg?: any) { + p.forEach = function forEach(cb: any, thisArg?: any) { const iterator = this.values() let result = iterator.next() while (!result.done) { @@ -122,9 +136,15 @@ export class DraftSet extends SetBase implements Set { result = iterator.next() } } -} -export function proxySet(target: AnySet, parent?: ImmerState) { + return DraftSet +})(Set) + +export function proxySet( + target: T, + parent?: ImmerState +): T & {[DRAFT_STATE]: SetState} { + // @ts-ignore return new DraftSet(target, parent) } diff --git a/src/types.ts b/src/types.ts index e0562c00..7df5a2ba 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,14 @@ -import {Nothing, DRAFT_STATE} from "./common" -import {SetState} from "./set" -import {MapState} from "./map" -import {ProxyObjectState, ProxyArrayState} from "./proxy" -import {ES5ObjectState, ES5ArrayState} from "./es5" -import {ImmerScope} from "./scope" +import { + SetState, + ImmerScope, + ProxyObjectState, + ProxyArrayState, + ES5ObjectState, + ES5ArrayState, + MapState, + DRAFT_STATE, + Nothing +} from "./internal" export type Objectish = AnyObject | AnyArray | AnyMap | AnySet export type ObjectishNoSet = AnyObject | AnyArray | AnyMap diff --git a/tsconfig.json b/tsconfig.json index 4c36b761..8bcffcf8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "lib": ["es2015"], - "target": "ES6", + "target": "ES5", "strict": true, "declaration": true, "importHelpers": false,