diff --git a/src/apply.ts b/src/apply.ts index b358c3f..8a33d4e 100644 --- a/src/apply.ts +++ b/src/apply.ts @@ -1,4 +1,11 @@ -import { Draft, Options, Patches, DraftType, Operation } from './interface'; +import { Operation, DraftType } from './interface'; +import type { + Draft, + Patches, + ApplyMutableOptions, + ApplyOptions, + ApplyResult, +} from './interface'; import { deepClone, get, getType, isDraft, unescapePath } from './utils'; import { create } from './create'; @@ -23,14 +30,11 @@ import { create } from './create'; * expect(state).toEqual(apply(baseState, patches)); * ``` */ -export function apply( - state: T, - patches: Patches, - applyOptions?: Pick< - Options, - Exclude, 'enablePatches'> - > -) { +export function apply< + T extends object, + F extends boolean = false, + A extends ApplyOptions = ApplyOptions, +>(state: T, patches: Patches, applyOptions?: A): ApplyResult { let i: number; for (i = patches.length - 1; i >= 0; i -= 1) { const { value, op, path } = patches[i]; @@ -45,7 +49,7 @@ export function apply( if (i > -1) { patches = patches.slice(i + 1); } - const mutate = (draft: Draft) => { + const mutate = (draft: Draft | T) => { patches.forEach((patch) => { const { path: _path, op } = patch; const path = unescapePath(_path); @@ -119,15 +123,28 @@ export function apply( } }); }; + if ((applyOptions as ApplyMutableOptions)?.mutable) { + if (__DEV__) { + if ( + Object.keys(applyOptions!).filter((key) => key !== 'mutable').length + ) { + console.warn( + 'The "mutable" option is not allowed to be used with other options.' + ); + } + } + mutate(state); + return undefined as ApplyResult; + } if (isDraft(state)) { if (applyOptions !== undefined) { throw new Error(`Cannot apply patches with options to a draft.`); } mutate(state as Draft); - return state; + return state as ApplyResult; } return create(state, mutate, { ...applyOptions, enablePatches: false, - }); + }) as any; } diff --git a/src/interface.ts b/src/interface.ts index 9568ef8..d38d969 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -102,6 +102,13 @@ export type Mark = ( ? BaseMark : MarkWithCopy; +export interface ApplyMutableOptions { + /** + * If it's `true`, the state will be mutated directly. + */ + mutable?: boolean; +} + export interface Options { /** * In strict mode, Forbid accessing non-draftable values and forbid returning a non-draft value. @@ -190,3 +197,16 @@ export type Draft = T extends Primitive | AtomicObject : T extends object ? DraftedObject : T; + +export type ApplyOptions = + | Pick< + Options, + Exclude, 'enablePatches'> + > + | ApplyMutableOptions; + +export type ApplyResult< + T extends object, + F extends boolean = false, + A extends ApplyOptions = ApplyOptions +> = A extends { mutable: true } ? void : T; diff --git a/test/apply.test.ts b/test/apply.test.ts index ee48b54..f5bba7e 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -1658,3 +1658,46 @@ test('array - update primitive', () => { d.a[2] += 1; }); }); + +test('base - mutate', () => { + const baseState = { + a: { + c: 1, + }, + }; + const [state, patches, inversePatches] = create( + baseState, + (draft) => { + draft.a.c = 2; + }, + { + enablePatches: true, + } + ); + expect(state).toEqual({ a: { c: 2 } }); + expect({ patches, inversePatches }).toEqual({ + patches: [ + { + op: 'replace', + path: ['a', 'c'], + value: 2, + }, + ], + inversePatches: [ + { + op: 'replace', + path: ['a', 'c'], + value: 1, + }, + ], + }); + const nextState = apply(baseState, patches); + expect(nextState).toEqual({ a: { c: 2 } }); + expect(baseState).toEqual({ a: { c: 1 } }); + + const result = apply(baseState, patches, { + mutable: true, + }); + expect(baseState).toEqual({ a: { c: 2 } }); + expect(result).toBeUndefined(); +}); diff --git a/test/dev.test.ts b/test/dev.test.ts index 1e45f51..c208687 100644 --- a/test/dev.test.ts +++ b/test/dev.test.ts @@ -1,7 +1,7 @@ /* eslint-disable consistent-return */ /* eslint-disable no-param-reassign */ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { create } from '../src'; +import { apply, create } from '../src'; test('custom shallow copy without checking in prod mode', () => { global.__DEV__ = false; @@ -88,3 +88,53 @@ test('custom shallow copy with checking in dev mode', () => { `"You can't use mark and patches or auto freeze together."` ); }); + +test('check warn when apply patches with other options', () => { + { + global.__DEV__ = true; + const baseState = { foo: { bar: 'test' } }; + const warn = console.warn; + jest.spyOn(console, 'warn').mockImplementation(() => {}); + apply( + baseState, + [ + { + op: 'replace', + path: ['foo', 'bar'], + value: 'test2', + }, + ], + { + mutable: true, + enableAutoFreeze: true, + } + ); + expect(console.warn).toHaveBeenCalledWith( + 'The "mutable" option is not allowed to be used with other options.' + ); + } + { + global.__DEV__ = true; + const baseState = { foo: { bar: 'test' } }; + const warn = console.warn; + jest.spyOn(console, 'warn').mockImplementation(() => {}); + apply( + baseState, + [ + { + op: 'replace', + path: ['foo', 'bar'], + value: 'test2', + }, + ], + { + mutable: true, + enableAutoFreeze: true, + mark: () => {}, + } + ); + expect(console.warn).toHaveBeenCalledWith( + 'The "mutable" option is not allowed to be used with other options.' + ); + } +});