diff --git a/docs/README.md b/docs/README.md index d5598792..44d039a5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ mutative - [Options](interfaces/Options.md) -### Type aliases +### Type Aliases - [Draft](README.md#draft) - [Immutable](README.md#immutable) @@ -24,11 +24,12 @@ mutative - [current](README.md#current) - [isDraft](README.md#isdraft) - [isDraftable](README.md#isdraftable) +- [makeCreator](README.md#makecreator) - [original](README.md#original) -- [safeReturn](README.md#safereturn) +- [rawReturn](README.md#rawreturn) - [unsafe](README.md#unsafe) -## Type aliases +## Type Aliases ### Draft @@ -42,7 +43,7 @@ mutative #### Defined in -[interface.ts:167](https://github.com/unadlib/mutative/blob/557d56b/src/interface.ts#L167) +[interface.ts:162](https://github.com/unadlib/mutative/blob/5b264a3/src/interface.ts#L162) ___ @@ -58,7 +59,7 @@ ___ #### Defined in -[interface.ts:149](https://github.com/unadlib/mutative/blob/557d56b/src/interface.ts#L149) +[interface.ts:144](https://github.com/unadlib/mutative/blob/5b264a3/src/interface.ts#L144) ___ @@ -74,7 +75,7 @@ ___ #### Defined in -[interface.ts:63](https://github.com/unadlib/mutative/blob/557d56b/src/interface.ts#L63) +[interface.ts:58](https://github.com/unadlib/mutative/blob/5b264a3/src/interface.ts#L58) ___ @@ -90,7 +91,7 @@ ___ #### Defined in -[interface.ts:77](https://github.com/unadlib/mutative/blob/557d56b/src/interface.ts#L77) +[interface.ts:72](https://github.com/unadlib/mutative/blob/5b264a3/src/interface.ts#L72) ## Functions @@ -130,7 +131,7 @@ expect(state).toEqual(apply(baseState, patches)); | Name | Type | | :------ | :------ | | `state` | `T` | -| `patches` | [`Patches`](README.md#patches)<`any`\> | +| `patches` | [`Patches`](README.md#patches) | | `applyOptions?` | `Pick`<[`Options`](interfaces/Options.md)<`boolean`, `F`\>, ``"mark"`` \| ``"strict"`` \| ``"enableAutoFreeze"``\> | #### Returns @@ -139,7 +140,7 @@ expect(state).toEqual(apply(baseState, patches)); #### Defined in -[apply.ts:26](https://github.com/unadlib/mutative/blob/557d56b/src/apply.ts#L26) +[apply.ts:26](https://github.com/unadlib/mutative/blob/5b264a3/src/apply.ts#L26) ___ @@ -167,7 +168,7 @@ Cast a value to an Draft type value. #### Defined in -[utils/cast.ts:7](https://github.com/unadlib/mutative/blob/557d56b/src/utils/cast.ts#L7) +[utils/cast.ts:6](https://github.com/unadlib/mutative/blob/5b264a3/src/utils/cast.ts#L6) ___ @@ -195,7 +196,7 @@ Cast a value to an Immutable type value. #### Defined in -[utils/cast.ts:14](https://github.com/unadlib/mutative/blob/557d56b/src/utils/cast.ts#L14) +[utils/cast.ts:13](https://github.com/unadlib/mutative/blob/5b264a3/src/utils/cast.ts#L13) ___ @@ -247,10 +248,31 @@ expect(state.arr).toBe(baseState.arr); #### Defined in -[create.ts:41](https://github.com/unadlib/mutative/blob/557d56b/src/create.ts#L41) +[makeCreator.ts:25](https://github.com/unadlib/mutative/blob/5b264a3/src/makeCreator.ts#L25) ▸ **create**<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`<`T`, `O`, `F`, `R`\> +`create(baseState, callback, options)` to create the next state + +## Example + +```ts +import { create } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const state = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, +); + +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(state).not.toBe(baseState); +expect(state.foo).not.toBe(baseState.foo); +expect(state.arr).toBe(baseState.arr); +``` + #### Type parameters | Name | Type | @@ -274,10 +296,31 @@ expect(state.arr).toBe(baseState.arr); #### Defined in -[create.ts:51](https://github.com/unadlib/mutative/blob/557d56b/src/create.ts#L51) +[makeCreator.ts:35](https://github.com/unadlib/mutative/blob/5b264a3/src/makeCreator.ts#L35) ▸ **create**<`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options?`): (`base`: `T`, ...`args`: `P`) => `CreateResult`<`T`, `O`, `F`, `R`\> +`create(baseState, callback, options)` to create the next state + +## Example + +```ts +import { create } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const state = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, +); + +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(state).not.toBe(baseState); +expect(state.foo).not.toBe(baseState.foo); +expect(state.arr).toBe(baseState.arr); +``` + #### Type parameters | Name | Type | @@ -299,7 +342,7 @@ expect(state.arr).toBe(baseState.arr); `fn` -▸ (`base`, ...`args`): `CreateResult`<`T`, `O`, `F`, `R`\> +▸ (`base`, `...args`): `CreateResult`<`T`, `O`, `F`, `R`\> ##### Parameters @@ -314,10 +357,31 @@ expect(state.arr).toBe(baseState.arr); #### Defined in -[create.ts:61](https://github.com/unadlib/mutative/blob/557d56b/src/create.ts#L61) +[makeCreator.ts:45](https://github.com/unadlib/mutative/blob/5b264a3/src/makeCreator.ts#L45) ▸ **create**<`T`, `O`, `F`\>(`base`, `options?`): [`T`, () => `Result`<`T`, `O`, `F`\>] +`create(baseState, callback, options)` to create the next state + +## Example + +```ts +import { create } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const state = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, +); + +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(state).not.toBe(baseState); +expect(state.foo).not.toBe(baseState.foo); +expect(state.arr).toBe(baseState.arr); +``` + #### Type parameters | Name | Type | @@ -339,7 +403,7 @@ expect(state.arr).toBe(baseState.arr); #### Defined in -[create.ts:71](https://github.com/unadlib/mutative/blob/557d56b/src/create.ts#L71) +[makeCreator.ts:55](https://github.com/unadlib/mutative/blob/5b264a3/src/makeCreator.ts#L55) ___ @@ -382,7 +446,7 @@ const state = create( #### Defined in -[current.ts:81](https://github.com/unadlib/mutative/blob/557d56b/src/current.ts#L81) +[current.ts:103](https://github.com/unadlib/mutative/blob/5b264a3/src/current.ts#L103) ___ @@ -404,7 +468,7 @@ Check if the value is a draft #### Defined in -[utils/draft.ts:11](https://github.com/unadlib/mutative/blob/557d56b/src/utils/draft.ts#L11) +[utils/draft.ts:11](https://github.com/unadlib/mutative/blob/5b264a3/src/utils/draft.ts#L11) ___ @@ -428,7 +492,160 @@ Check if a value is draftable #### Defined in -[utils/draft.ts:28](https://github.com/unadlib/mutative/blob/557d56b/src/utils/draft.ts#L28) +[utils/draft.ts:28](https://github.com/unadlib/mutative/blob/5b264a3/src/utils/draft.ts#L28) + +___ + +### makeCreator + +▸ **makeCreator**<`_F`, `_O`\>(`options?`): (`base`: `T`, `mutate`: (`draft`: [`Draft`](README.md#draft)<`T`\>) => `R`, `options?`: [`Options`](interfaces/Options.md)<`O`, `F`\>) => `CreateResult`<`T`, `O`, `F`, `R`\>(`base`: `T`, `mutate`: (`draft`: `T`) => `R`, `options?`: [`Options`](interfaces/Options.md)<`O`, `F`\>) => `CreateResult`<`T`, `O`, `F`, `R`\>(`mutate`: (`draft`: [`Draft`](README.md#draft)<`T`\>, ...`args`: `P`) => `R`, `options?`: [`Options`](interfaces/Options.md)<`O`, `F`\>) => (`base`: `T`, ...`args`: `P`) => `CreateResult`<`T`, `O`, `F`, `R`\>(`base`: `T`, `options?`: [`Options`](interfaces/Options.md)<`O`, `F`\>) => [`T`, () => `Result`<`T`, `O`, `F`\>] + +`makeCreator(options)` to make a creator function. + +## Example + +```ts +import { makeCreator } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const create = makeCreator({ enableAutoFreeze: true }); +const state = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, +); + +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(state).not.toBe(baseState); +expect(state.foo).not.toBe(baseState.foo); +expect(state.arr).toBe(baseState.arr); +expect(Object.isFrozen(state)).toBeTruthy(); +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `_F` | extends `boolean` = ``false`` | +| `_O` | extends `PatchesOptions` = ``false`` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `options?` | [`Options`](interfaces/Options.md)<`_O`, `_F`\> | + +#### Returns + +`fn` + +▸ <`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`<`T`, `O`, `F`, `R`\> + +##### Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | extends `unknown` | +| `F` | extends `boolean` = `_F` | +| `O` | extends `PatchesOptions` = `_O` | +| `R` | extends `unknown` = `void` | + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `base` | `T` | +| `mutate` | (`draft`: [`Draft`](README.md#draft)<`T`\>) => `R` | +| `options?` | [`Options`](interfaces/Options.md)<`O`, `F`\> | + +##### Returns + +`CreateResult`<`T`, `O`, `F`, `R`\> + +▸ <`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`<`T`, `O`, `F`, `R`\> + +##### Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | extends `unknown` | +| `F` | extends `boolean` = `_F` | +| `O` | extends `PatchesOptions` = `_O` | +| `R` | extends `void` \| `Promise`<`void`\> = `void` | + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `base` | `T` | +| `mutate` | (`draft`: `T`) => `R` | +| `options?` | [`Options`](interfaces/Options.md)<`O`, `F`\> | + +##### Returns + +`CreateResult`<`T`, `O`, `F`, `R`\> + +▸ <`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options?`): (`base`: `T`, ...`args`: `P`) => `CreateResult`<`T`, `O`, `F`, `R`\> + +##### Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | extends `unknown` | +| `P` | extends `any`[] = [] | +| `F` | extends `boolean` = `_F` | +| `O` | extends `PatchesOptions` = `_O` | +| `R` | extends `void` \| `Promise`<`void`\> = `void` | + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `mutate` | (`draft`: [`Draft`](README.md#draft)<`T`\>, ...`args`: `P`) => `R` | +| `options?` | [`Options`](interfaces/Options.md)<`O`, `F`\> | + +##### Returns + +`fn` + +▸ (`base`, `...args`): `CreateResult`<`T`, `O`, `F`, `R`\> + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `base` | `T` | +| `...args` | `P` | + +##### Returns + +`CreateResult`<`T`, `O`, `F`, `R`\> + +▸ <`T`, `O`, `F`\>(`base`, `options?`): [`T`, () => `Result`<`T`, `O`, `F`\>] + +##### Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | extends `unknown` | +| `O` | extends `PatchesOptions` = `_O` | +| `F` | extends `boolean` = `_F` | + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `base` | `T` | +| `options?` | [`Options`](interfaces/Options.md)<`O`, `F`\> | + +##### Returns + +[`T`, () => `Result`<`T`, `O`, `F`\>] + +#### Defined in + +[makeCreator.ts:19](https://github.com/unadlib/mutative/blob/5b264a3/src/makeCreator.ts#L19) ___ @@ -471,15 +688,30 @@ const state = create( #### Defined in -[original.ts:21](https://github.com/unadlib/mutative/blob/557d56b/src/original.ts#L21) +[original.ts:21](https://github.com/unadlib/mutative/blob/5b264a3/src/original.ts#L21) ___ -### safeReturn +### rawReturn + +▸ **rawReturn**<`T`\>(`value`): `T` -▸ **safeReturn**<`T`\>(`value`): `T` +Use rawReturn() to wrap the return value to skip the draft check and thus improve performance. -It is used as a safe return value to ensure that this value replaces the finalized value. +## Example + +```ts +import { create, rawReturn } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const state = create( + baseState, + (draft) => { + return rawReturn(baseState); + }, +); +expect(state).toBe(baseState); +``` #### Type parameters @@ -499,7 +731,7 @@ It is used as a safe return value to ensure that this value replaces the finaliz #### Defined in -[safeReturn.ts:6](https://github.com/unadlib/mutative/blob/557d56b/src/safeReturn.ts#L6) +[rawReturn.ts:21](https://github.com/unadlib/mutative/blob/5b264a3/src/rawReturn.ts#L21) ___ @@ -554,4 +786,4 @@ expect(state.foobar.bar).toBe(2); #### Defined in -[unsafe.ts:53](https://github.com/unadlib/mutative/blob/557d56b/src/unsafe.ts#L53) +[unsafe.ts:53](https://github.com/unadlib/mutative/blob/5b264a3/src/unsafe.ts#L53) diff --git a/docs/interfaces/Options.md b/docs/interfaces/Options.md index e5569a1c..25613154 100644 --- a/docs/interfaces/Options.md +++ b/docs/interfaces/Options.md @@ -28,7 +28,7 @@ Enable autoFreeze, and return frozen state. #### Defined in -[interface.ts:122](https://github.com/unadlib/mutative/blob/557d56b/src/interface.ts#L122) +[interface.ts:117](https://github.com/unadlib/mutative/blob/5b264a3/src/interface.ts#L117) ___ @@ -40,7 +40,7 @@ Enable patch, and return the patches and inversePatches. #### Defined in -[interface.ts:118](https://github.com/unadlib/mutative/blob/557d56b/src/interface.ts#L118) +[interface.ts:113](https://github.com/unadlib/mutative/blob/5b264a3/src/interface.ts#L113) ___ @@ -53,7 +53,7 @@ And it can also return a shallow copy function(AutoFreeze and Patches should bot #### Defined in -[interface.ts:127](https://github.com/unadlib/mutative/blob/557d56b/src/interface.ts#L127) +[interface.ts:122](https://github.com/unadlib/mutative/blob/5b264a3/src/interface.ts#L122) ___ @@ -65,4 +65,4 @@ In strict mode, Forbid accessing non-draftable values and forbid returning a non #### Defined in -[interface.ts:114](https://github.com/unadlib/mutative/blob/557d56b/src/interface.ts#L114) +[interface.ts:109](https://github.com/unadlib/mutative/blob/5b264a3/src/interface.ts#L109) diff --git a/package.json b/package.json index 7261b406..e0b51bce 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "performance:array-object": "cd test/performance && NODE_ENV='production' ts-node array-object.ts", "performance": "yarn build && yarn perf && yarn performance:basic && yarn performance:set-map && yarn performance:big-object && yarn performance:sample && yarn performance:array-object", "build": "tsdx build --format esm,cjs,umd && cp dist/mutative.esm.js dist/mutative.esm.mjs", - "build:doc": "rimraf docs && typedoc --out docs src/index.ts --readme none", + "build:doc": "rimraf docs && typedoc --plugin typedoc-plugin-markdown --out docs src/index.ts --readme none", "commit": "yarn git-cz", "size": "size-limit", "analyze": "size-limit --why", diff --git a/src/create.ts b/src/create.ts index 43f0122c..c9db6fe7 100644 --- a/src/create.ts +++ b/src/create.ts @@ -1,20 +1,4 @@ -import { - CreateResult, - Draft, - Options, - PatchesOptions, - Result, -} from './interface'; -import { draftify } from './draftify'; -import { - getProxyDraft, - isDraft, - isDraftable, - isEqual, - revokeProxy, -} from './utils'; -import { current, handleReturnValue } from './current'; -import { RAW_RETURN_SYMBOL } from './constant'; +import { makeCreator } from './makeCreator'; /** * `create(baseState, callback, options)` to create the next state @@ -38,151 +22,6 @@ import { RAW_RETURN_SYMBOL } from './constant'; * expect(state.arr).toBe(baseState.arr); * ``` */ -function create< - T extends any, - F extends boolean = false, - O extends PatchesOptions = false, - R extends void | Promise | T | Promise = void ->( - base: T, - mutate: (draft: Draft) => R, - options?: Options -): CreateResult; -function create< - T extends any, - F extends boolean = false, - O extends PatchesOptions = false, - R extends void | Promise = void ->( - base: T, - mutate: (draft: T) => R, - options?: Options -): CreateResult; -function create< - T extends any, - P extends any[] = [], - F extends boolean = false, - O extends PatchesOptions = false, - R extends void | Promise = void ->( - mutate: (draft: Draft, ...args: P) => R, - options?: Options -): (base: T, ...args: P) => CreateResult; -function create< - T extends any, - O extends PatchesOptions = false, - F extends boolean = false ->(base: T, options?: Options): [T, () => Result]; -function create(arg0: any, arg1: any, arg2?: any): any { - if (typeof arg0 === 'function' && typeof arg1 !== 'function') { - return function (this: any, base: any, ...args: any[]) { - return create( - base, - (draft: any) => arg0.call(this, draft, ...args), - arg1 - ); - }; - } - const base = arg0; - const mutate = arg1 as (...args: any[]) => any; - let options = arg2 as Options; - if (typeof arg1 !== 'function') { - options = arg1; - } - if ( - options !== undefined && - Object.prototype.toString.call(options) !== '[object Object]' - ) { - throw new Error( - `Invalid options: ${options}, 'options' should be an object.` - ); - } - const state = isDraft(base) ? current(base) : base; - const mark = options?.mark; - const enablePatches = options?.enablePatches ?? false; - const strict = options?.strict ?? false; - const enableAutoFreeze = options?.enableAutoFreeze ?? false; - const _options: Options = { - enableAutoFreeze, - mark, - strict, - enablePatches, - }; - if ( - !isDraftable(state, _options) && - typeof state === 'object' && - state !== null - ) { - throw new Error( - `Invalid base state: create() only supports plain objects, arrays, Set, Map or using mark() to mark the state as immutable.` - ); - } - const [draft, finalize] = draftify(state, _options); - if (typeof arg1 !== 'function') { - if (!isDraftable(state, _options)) { - throw new Error( - `Invalid base state: create() only supports plain objects, arrays, Set, Map or using mark() to mark the state as immutable.` - ); - } - return [draft, finalize]; - } - let result: any; - try { - result = mutate(draft); - } catch (error) { - revokeProxy(getProxyDraft(draft)); - throw error; - } - const returnValue = (value: any) => { - const proxyDraft = getProxyDraft(draft)!; - if (!isDraft(value)) { - if ( - value !== undefined && - !isEqual(value, draft) && - proxyDraft?.operated - ) { - throw new Error( - `Either the value is returned as a new non-draft value, or only the draft is modified without returning any value.` - ); - } - const rawReturnValue = value?.[RAW_RETURN_SYMBOL] as [any] | undefined; - if (rawReturnValue) { - const _value = rawReturnValue[0]; - if (_options.strict && typeof value === 'object' && value !== null) { - handleReturnValue({ - rootDraft: proxyDraft, - value, - useRawReturn: true, - }); - } - return finalize([_value]); - } - if (value !== undefined) { - if (typeof value === 'object' && value !== null) { - handleReturnValue({ rootDraft: proxyDraft, value }); - } - return finalize([value]); - } - } - if (value === draft || value === undefined) { - return finalize([]); - } - const returnedProxyDraft = getProxyDraft(value)!; - if (_options === returnedProxyDraft.options) { - if (returnedProxyDraft.operated) { - throw new Error(`Cannot return a modified child draft.`); - } - return finalize([current(value)]); - } - return finalize([value]); - }; - if (result instanceof Promise) { - return result.then(returnValue, (error) => { - revokeProxy(getProxyDraft(draft)!); - throw error; - }); - } - return returnValue(result); -} +const create = makeCreator(); export { create }; diff --git a/src/index.ts b/src/index.ts index 620f9bee..86856508 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +export { makeCreator } from './makeCreator'; export { create } from './create'; export { apply } from './apply'; export { original } from './original'; diff --git a/src/makeCreator.ts b/src/makeCreator.ts new file mode 100644 index 00000000..f158fbcc --- /dev/null +++ b/src/makeCreator.ts @@ -0,0 +1,211 @@ +import { + CreateResult, + Draft, + Options, + PatchesOptions, + Result, +} from './interface'; +import { draftify } from './draftify'; +import { + getProxyDraft, + isDraft, + isDraftable, + isEqual, + revokeProxy, +} from './utils'; +import { current, handleReturnValue } from './current'; +import { RAW_RETURN_SYMBOL } from './constant'; + +type MakeCreator = < + _F extends boolean = false, + _O extends PatchesOptions = false +>( + options?: Options<_O, _F> +) => { + < + T extends any, + F extends boolean = _F, + O extends PatchesOptions = _O, + R extends void | Promise | T | Promise = void + >( + base: T, + mutate: (draft: Draft) => R, + options?: Options + ): CreateResult; + < + T extends any, + F extends boolean = _F, + O extends PatchesOptions = _O, + R extends void | Promise = void + >( + base: T, + mutate: (draft: T) => R, + options?: Options + ): CreateResult; + < + T extends any, + P extends any[] = [], + F extends boolean = _F, + O extends PatchesOptions = _O, + R extends void | Promise = void + >( + mutate: (draft: Draft, ...args: P) => R, + options?: Options + ): (base: T, ...args: P) => CreateResult; + ( + base: T, + options?: Options + ): [T, () => Result]; +}; + +/** + * `makeCreator(options)` to make a creator function. + * + * ## Example + * + * ```ts + * import { makeCreator } from '../index'; + * + * const baseState = { foo: { bar: 'str' }, arr: [] }; + * const create = makeCreator({ enableAutoFreeze: true }); + * const state = create( + * baseState, + * (draft) => { + * draft.foo.bar = 'str2'; + * }, + * ); + * + * expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); + * expect(state).not.toBe(baseState); + * expect(state.foo).not.toBe(baseState.foo); + * expect(state.arr).toBe(baseState.arr); + * expect(Object.isFrozen(state)).toBeTruthy(); + * ``` + */ +export const makeCreator: MakeCreator = (arg) => { + if ( + __DEV__ && + arg !== undefined && + Object.prototype.toString.call(arg) !== '[object Object]' + ) { + throw new Error( + `Invalid options: ${String(arg)}, 'options' should be an object.` + ); + } + return function create(arg0: any, arg1: any, arg2?: any): any { + if (typeof arg0 === 'function' && typeof arg1 !== 'function') { + return function (this: any, base: any, ...args: any[]) { + return create( + base, + (draft: any) => arg0.call(this, draft, ...args), + arg1 + ); + }; + } + const base = arg0; + const mutate = arg1 as (...args: any[]) => any; + let options = arg2 as Options; + if (typeof arg1 !== 'function') { + options = arg1; + } + if ( + __DEV__ && + options !== undefined && + Object.prototype.toString.call(options) !== '[object Object]' + ) { + throw new Error( + `Invalid options: ${options}, 'options' should be an object.` + ); + } + options = { + ...arg, + ...options, + }; + const state = isDraft(base) ? current(base) : base; + const mark = options.mark; + const enablePatches = options.enablePatches ?? false; + const strict = options.strict ?? false; + const enableAutoFreeze = options.enableAutoFreeze ?? false; + const _options: Options = { + enableAutoFreeze, + mark, + strict, + enablePatches, + }; + if ( + !isDraftable(state, _options) && + typeof state === 'object' && + state !== null + ) { + throw new Error( + `Invalid base state: create() only supports plain objects, arrays, Set, Map or using mark() to mark the state as immutable.` + ); + } + const [draft, finalize] = draftify(state, _options); + if (typeof arg1 !== 'function') { + if (!isDraftable(state, _options)) { + throw new Error( + `Invalid base state: create() only supports plain objects, arrays, Set, Map or using mark() to mark the state as immutable.` + ); + } + return [draft, finalize]; + } + let result: any; + try { + result = mutate(draft); + } catch (error) { + revokeProxy(getProxyDraft(draft)); + throw error; + } + const returnValue = (value: any) => { + const proxyDraft = getProxyDraft(draft)!; + if (!isDraft(value)) { + if ( + value !== undefined && + !isEqual(value, draft) && + proxyDraft?.operated + ) { + throw new Error( + `Either the value is returned as a new non-draft value, or only the draft is modified without returning any value.` + ); + } + const rawReturnValue = value?.[RAW_RETURN_SYMBOL] as [any] | undefined; + if (rawReturnValue) { + const _value = rawReturnValue[0]; + if (_options.strict && typeof value === 'object' && value !== null) { + handleReturnValue({ + rootDraft: proxyDraft, + value, + useRawReturn: true, + }); + } + return finalize([_value]); + } + if (value !== undefined) { + if (typeof value === 'object' && value !== null) { + handleReturnValue({ rootDraft: proxyDraft, value }); + } + return finalize([value]); + } + } + if (value === draft || value === undefined) { + return finalize([]); + } + const returnedProxyDraft = getProxyDraft(value)!; + if (_options === returnedProxyDraft.options) { + if (returnedProxyDraft.operated) { + throw new Error(`Cannot return a modified child draft.`); + } + return finalize([current(value)]); + } + return finalize([value]); + }; + if (result instanceof Promise) { + return result.then(returnValue, (error) => { + revokeProxy(getProxyDraft(draft)!); + throw error; + }); + } + return returnValue(result); + }; +}; diff --git a/src/utils/forEach.ts b/src/utils/forEach.ts index 3b752c45..8d84c3bc 100644 --- a/src/utils/forEach.ts +++ b/src/utils/forEach.ts @@ -3,26 +3,22 @@ import { getType } from './draft'; export function forEach( target: T, - iter: (key: string | number | symbol, value: any, source: T) => void, - enumerableOnly = false + iter: (key: string | number | symbol, value: any, source: T) => void ) { - if (getType(target) === DraftType.Object) { - const getKeys = enumerableOnly ? Object.keys : Reflect.ownKeys; - getKeys(target).forEach((key) => { - if (!enumerableOnly || typeof key !== 'symbol') - iter(key, (target as any)[key], target); + const type = getType(target); + if (type === DraftType.Object) { + Reflect.ownKeys(target).forEach((key) => { + iter(key, (target as any)[key], target); }); - return; - } - if (getType(target) === DraftType.Array) { + } else if (type === DraftType.Array) { let index = 0; for (const entry of target as any[]) { iter(index, entry, target); index += 1; } - return; + } else { + (target as Map | Set).forEach((entry: any, index: any) => + iter(index, entry, target) + ); } - (target as Map | Set).forEach((entry: any, index: any) => - iter(index, entry, target) - ); } diff --git a/test/jsdoc.test.ts b/test/jsdoc.test.ts index 56a87392..6c370890 100644 --- a/test/jsdoc.test.ts +++ b/test/jsdoc.test.ts @@ -19,4 +19,7 @@ describe('jsdoc', () => { test('rawReturn()', () => { jsdocTests('../src/rawReturn.ts', __dirname); }); + test('makeCreator()', () => { + jsdocTests('../src/makeCreator.ts', __dirname); + }); }); diff --git a/test/makeCreator.0.snap.ts b/test/makeCreator.0.snap.ts new file mode 100644 index 00000000..52dfab45 --- /dev/null +++ b/test/makeCreator.0.snap.ts @@ -0,0 +1,16 @@ +import { makeCreator } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const create = makeCreator({ enableAutoFreeze: true }); +const state = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, +); + +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(state).not.toBe(baseState); +expect(state.foo).not.toBe(baseState.foo); +expect(state.arr).toBe(baseState.arr); +expect(Object.isFrozen(state)).toBeTruthy(); diff --git a/test/makeCreator.test.ts b/test/makeCreator.test.ts new file mode 100644 index 00000000..494462d6 --- /dev/null +++ b/test/makeCreator.test.ts @@ -0,0 +1,36 @@ +import { makeCreator } from '../src'; + +test('check makeCreator options', () => { + [ + [], + null, + 0, + 1, + '', + 'str', + true, + false, + new Map(), + new Set(), + Symbol(''), + function () { + // + }, + ].forEach((arg) => { + expect(() => { + // @ts-expect-error + makeCreator(arg); + }).toThrowError(`'options' should be an object.`); + }); +}); + +test('check enableAutoFreeze', () => { + const create = makeCreator({ + enableAutoFreeze: true, + }); + const baseState = { foo: { bar: 'str' }, arr: [] }; + const state = create(baseState, (draft) => { + // + }); + expect(Object.isFrozen(state)).toBeTruthy(); +});