├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── nodejs.yml │ ├── npm-publish.yml │ ├── pull-request.yml │ └── tests.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── benchmark-array.jpg ├── benchmark-class.jpg ├── benchmark-object.jpg ├── benchmark.jpg ├── docs ├── README.md ├── functions │ ├── apply.md │ ├── castDraft.md │ ├── castImmutable.md │ ├── castMutable.md │ ├── create.md │ ├── current.md │ ├── isDraft.md │ ├── isDraftable.md │ ├── makeCreator.md │ ├── markSimpleObject.md │ ├── original.md │ ├── rawReturn.md │ └── unsafe.md ├── interfaces │ └── Options.md └── type-aliases │ ├── Draft.md │ ├── Immutable.md │ ├── Patch.md │ ├── Patches.md │ └── PatchesOptions.md ├── global.d.ts ├── index.ts ├── package.json ├── rollup.config.js ├── src ├── apply.ts ├── constant.ts ├── create.ts ├── current.ts ├── draft.ts ├── draftify.ts ├── index.ts ├── interface.ts ├── internal.ts ├── makeCreator.ts ├── map.ts ├── original.ts ├── patch.ts ├── rawReturn.ts ├── set.ts ├── unsafe.ts └── utils │ ├── cast.ts │ ├── copy.ts │ ├── deepFreeze.ts │ ├── draft.ts │ ├── finalize.ts │ ├── forEach.ts │ ├── index.ts │ ├── mark.ts │ ├── marker.ts │ └── proto.ts ├── test ├── __immer_performance_tests__ │ ├── add-data.ts │ ├── data.json │ ├── incremental.ts │ ├── large-obj.ts │ ├── measure.ts │ └── todo.ts ├── __snapshots__ │ ├── apply.test.ts.snap │ ├── current.test.ts.snap │ ├── immer-non-support.test.ts.snap │ └── index.test.ts.snap ├── apply.0.snap.ts ├── apply.test.ts ├── array.test.ts ├── benchmark │ ├── all.ts │ ├── array-batch-getter.ts │ ├── array-batch.ts │ ├── array-single-push.ts │ ├── array.ts │ ├── class.ts │ ├── index.ts │ ├── map-batch.ts │ ├── map.ts │ ├── object-batch-getter.ts │ ├── object-batch.ts │ ├── object.ts │ ├── results │ │ ├── all.jpg │ │ ├── array-batch-getter.jpg │ │ ├── array-batch.jpg │ │ ├── array-single-push.jpg │ │ ├── array.jpg │ │ ├── class.jpg │ │ ├── map-batch.jpg │ │ ├── map.jpg │ │ ├── object-batch-getter.jpg │ │ ├── object-batch.jpg │ │ ├── object.jpg │ │ ├── result.json │ │ ├── set-batch.jpg │ │ └── set.jpg │ ├── set-batch.ts │ └── set.ts ├── create.0.snap.ts ├── create.test.ts ├── current.0.snap.ts ├── current.test.ts ├── curry.test.ts ├── dev.test.ts ├── edge.test.ts ├── immer-non-support.test.ts ├── immer │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── base.ts.snap │ │ │ ├── curry.ts.snap │ │ │ ├── frozen.ts.snap │ │ │ ├── manual.ts.snap │ │ │ ├── patch.ts.snap │ │ │ └── readme.ts.snap │ │ ├── base.ts │ │ ├── current.ts │ │ ├── curry.ts │ │ ├── draft.ts │ │ ├── empty.ts │ │ ├── frozen.ts │ │ ├── immutable.ts │ │ ├── isDraftable.ts │ │ ├── manual.ts │ │ ├── map-set.ts │ │ ├── not-strict-copy.ts │ │ ├── null.ts │ │ ├── original.ts │ │ ├── patch.ts │ │ ├── plugins.ts │ │ ├── produce.ts │ │ ├── readme.ts │ │ ├── redux.ts │ │ ├── regressions.ts │ │ ├── test-data.json │ │ └── tsconfig.json │ └── src │ │ └── immer.ts ├── index.test.ts ├── jsdoc.test.ts ├── json-patch.test.ts ├── json-patch │ ├── coreSpec.test.ts │ └── duplexSpec.test.ts ├── makeCreator.0.snap.ts ├── makeCreator.test.ts ├── original.0.snap.ts ├── original.test.ts ├── performance │ ├── array-object-first-time.ts │ ├── array-object.ts │ ├── benchmark-array.ts │ ├── benchmark-class.ts │ ├── benchmark-object.ts │ ├── benchmark-reducer.ts │ ├── benchmark.ts │ ├── benchmarks │ │ ├── forEach.ts │ │ ├── rawReturn.ts │ │ └── returnWithDraft.ts │ ├── big-object.ts │ ├── index.ts │ ├── new-set-api.jpg │ ├── new-set-api.ts │ ├── read-draft │ │ ├── index.ts │ │ └── mockPhysics.ts │ ├── sample.ts │ └── set-map.ts ├── rawReturn.0.snap.ts ├── rawReturn.test.ts ├── unsafe.0.snap.ts └── unsafe.test.ts ├── tsconfig.json ├── website ├── .gitignore ├── README.md ├── babel.config.js ├── blog │ ├── authors.yml │ └── releases │ │ └── 1.0 │ │ ├── img │ │ ├── benchmark-array.jpg │ │ ├── benchmark-object.jpg │ │ ├── benchmark.jpg │ │ └── social-card.jpg │ │ └── index.md ├── docs │ ├── advanced-guides │ │ ├── _category_.json │ │ ├── auto-freeze.md │ │ ├── currying.md │ │ ├── mark.md │ │ ├── migration.md │ │ ├── pathes.md │ │ ├── strict-mode.md │ │ └── typescript.md │ ├── api-reference │ │ ├── _category_.json │ │ ├── apply.md │ │ ├── create.md │ │ ├── current.md │ │ ├── isdraft.md │ │ ├── isdraftable.md │ │ ├── makecreator.md │ │ ├── marksimplebject.md │ │ ├── original.md │ │ ├── rawreturn.md │ │ └── unsafe.md │ ├── extra-topics │ │ ├── _category_.json │ │ ├── comparison-with-immer.md │ │ ├── contributing.md │ │ ├── faq.md │ │ ├── img │ │ │ └── benchmark.jpg │ │ └── mutative-ecosystem.md │ ├── getting-started │ │ ├── _category_.json │ │ ├── concepts.md │ │ ├── img │ │ │ ├── all.jpg │ │ │ ├── benchmark-array.jpg │ │ │ ├── benchmark-object.jpg │ │ │ ├── benchmark.jpg │ │ │ └── mutative-workflow.png │ │ ├── installation.md │ │ ├── mutative-with-react.md │ │ ├── performance.md │ │ └── usages.md │ └── intro.md ├── docusaurus.config.js ├── i18n │ ├── en │ │ ├── code.json │ │ ├── docusaurus-plugin-content-blog │ │ │ └── options.json │ │ ├── docusaurus-plugin-content-docs │ │ │ └── current.json │ │ └── docusaurus-theme-classic │ │ │ ├── footer.json │ │ │ └── navbar.json │ └── zh-CN │ │ ├── code.json │ │ ├── docusaurus-plugin-content-blog │ │ ├── authors.yml │ │ ├── options.json │ │ └── releases │ │ │ └── 1.0 │ │ │ ├── index.md │ │ │ └── social-card.png │ │ ├── docusaurus-plugin-content-docs │ │ ├── current.json │ │ └── current │ │ │ ├── intro.md │ │ │ ├── tutorial-basics │ │ │ ├── _category_.json │ │ │ ├── congratulations.md │ │ │ ├── create-a-blog-post.md │ │ │ ├── create-a-document.md │ │ │ ├── create-a-page.md │ │ │ ├── deploy-your-site.md │ │ │ └── markdown-features.mdx │ │ │ └── tutorial-extras │ │ │ ├── _category_.json │ │ │ ├── img │ │ │ ├── docsVersionDropdown.png │ │ │ └── localeDropdown.png │ │ │ ├── manage-docs-versions.md │ │ │ └── translate-your-site.md │ │ ├── docusaurus-plugin-content-pages │ │ └── markdown-page.md │ │ └── docusaurus-theme-classic │ │ ├── footer.json │ │ └── navbar.json ├── package.json ├── sidebars.js ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.js │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.js │ │ ├── index.module.css │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ ├── CNAME │ └── img │ │ ├── mutative.png │ │ ├── favicon.ico │ │ ├── logo.svg │ │ └── mutative-social-card.jpg └── yarn.lock └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | # install EditorConfig for VS Code extension 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = space 11 | insert_final_newline = true 12 | max_line_length = 80 13 | trim_trailing_whitespace = true 14 | 15 | [*.md] 16 | max_line_length = 0 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | test/immer/** 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "airbnb", 10 | "prettier", 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "globals": { 16 | "Atomics": "readonly", 17 | "SharedArrayBuffer": "readonly" 18 | }, 19 | "parser": "@typescript-eslint/parser", 20 | "parserOptions": { 21 | "ecmaVersion": 2019, 22 | "sourceType": "module" 23 | }, 24 | "plugins": ["prettier", "@typescript-eslint"], 25 | "rules": { 26 | "no-case-declarations": "off", 27 | "no-useless-return": "off", 28 | "no-self-compare": "off", 29 | "no-multi-assign": "off", 30 | "guard-for-in": "off", 31 | "no-cond-assign":"off", 32 | "no-else-return": "off", 33 | "default-case": "off", 34 | "no-restricted-globals": "off", 35 | "@typescript-eslint/ban-types": "off", 36 | "no-use-before-define": "off", 37 | "no-console": "off", 38 | "no-nested-ternary":"off", 39 | "func-names": "off", 40 | "@typescript-eslint/no-explicit-any": "off", 41 | "@typescript-eslint/no-unnecessary-type-constraint": "off", 42 | "no-return-assign": "off", 43 | "consistent-return":"off", 44 | "no-self-assign":"off", 45 | "no-lone-blocks": "off", 46 | "@typescript-eslint/no-explicit-an": "off", 47 | "prefer-const":"off", 48 | "no-plusplus": "off", 49 | "no-shadow": "off", 50 | "prefer-destructuring": "off", 51 | "@typescript-eslint/ban-ts-comment": "off", 52 | "no-param-reassign": "off", 53 | "no-useless-constructor": 0, 54 | "class-methods-use-this": 0, 55 | "no-underscore-dangle": 0, 56 | "@typescript-eslint/no-non-null-assertion": 0, 57 | "@typescript-eslint/interface-name-prefix": 0, 58 | "@typescript-eslint/explicit-function-return-type": 0, 59 | "max-classes-per-file": 0, 60 | "no-await-in-loop": "off", 61 | "no-restricted-syntax": "off", 62 | "import/no-extraneous-dependencies": [ 63 | "error", 64 | { 65 | "devDependencies": true 66 | } 67 | ], 68 | "import/prefer-default-export": "off", 69 | "prettier/prettier": ["error"], 70 | "import/extensions": [ 71 | "error", 72 | "ignorePackages", 73 | { 74 | "js": "never", 75 | "ts": "never" 76 | } 77 | ] 78 | }, 79 | "settings": { 80 | "import/resolver": { 81 | "node": { 82 | "extensions": [".js", ".ts"] 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [18.x, 20.x, 22.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: yarn install, build, and test 21 | run: | 22 | yarn install 23 | yarn build 24 | yarn test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: '20.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: yarn 20 | - run: yarn build 21 | - run: yarn test 22 | - run: npm publish --provenance 23 | env: 24 | NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} 25 | CI: true 26 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | on: ["pull_request"] 2 | 3 | name: Test Coverage 4 | permissions: 5 | contents: read 6 | pull-requests: write 7 | jobs: 8 | 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - uses: actions/checkout@v1 15 | 16 | - name: Use Node.js 22.x 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 22.x 20 | 21 | - name: yarn install, yarn test:coverage 22 | run: | 23 | yarn install 24 | yarn test --coverage 25 | 26 | - name: Coverage 27 | uses: romeovs/lcov-reporter-action@v0.4.0 28 | with: 29 | lcov-file: ./coverage/lcov.info 30 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Test with Coveralls 7 | 8 | jobs: 9 | 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - uses: actions/checkout@v1 16 | 17 | - name: Use Node.js 22.x 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 22.x 21 | 22 | - name: yarn install, yarn test:coverage 23 | run: | 24 | yarn install 25 | echo repo_token: ${{ secrets.COVERALLS_REPO_TOKEN }} > .coveralls.yml 26 | yarn test:coverage 27 | 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@master 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | benchmark.csv 107 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | 5 | # Ignore all HTML files: 6 | **/*.html 7 | 8 | **/.git 9 | **/.svn 10 | **/.hg 11 | **/node_modules 12 | **/*/*.snap.ts 13 | **/*/*.snap.tsx 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Michael Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Thanks for helping make Mutative safe for everyone. 2 | 3 | # Reporting Security Issues 4 | 5 | If you believe you have found a security vulnerability in Mutative, we encourage you to let us know right away, please contact us at unadlib@gmail.com. We will investigate all legitimate reports and do our best to quickly fix the problem. 6 | -------------------------------------------------------------------------------- /benchmark-array.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/benchmark-array.jpg -------------------------------------------------------------------------------- /benchmark-class.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/benchmark-class.jpg -------------------------------------------------------------------------------- /benchmark-object.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/benchmark-object.jpg -------------------------------------------------------------------------------- /benchmark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/benchmark.jpg -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | **mutative** • **Docs** 2 | 3 | *** 4 | 5 | # mutative 6 | 7 | ## Interfaces 8 | 9 | - [Options](interfaces/Options.md) 10 | 11 | ## Type Aliases 12 | 13 | - [Draft](type-aliases/Draft.md) 14 | - [Immutable](type-aliases/Immutable.md) 15 | - [Patch](type-aliases/Patch.md) 16 | - [Patches](type-aliases/Patches.md) 17 | - [PatchesOptions](type-aliases/PatchesOptions.md) 18 | 19 | ## Functions 20 | 21 | - [apply](functions/apply.md) 22 | - [castDraft](functions/castDraft.md) 23 | - [castImmutable](functions/castImmutable.md) 24 | - [castMutable](functions/castMutable.md) 25 | - [create](functions/create.md) 26 | - [current](functions/current.md) 27 | - [isDraft](functions/isDraft.md) 28 | - [isDraftable](functions/isDraftable.md) 29 | - [makeCreator](functions/makeCreator.md) 30 | - [markSimpleObject](functions/markSimpleObject.md) 31 | - [original](functions/original.md) 32 | - [rawReturn](functions/rawReturn.md) 33 | - [unsafe](functions/unsafe.md) 34 | -------------------------------------------------------------------------------- /docs/functions/apply.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / apply 6 | 7 | # Function: apply() 8 | 9 | > **apply**\<`T`, `F`\>(`state`, `patches`, `applyOptions`?): `T` \| `F` *extends* `true` ? [`Immutable`](../type-aliases/Immutable.md)\<`T`\> : `T` 10 | 11 | `apply(state, patches)` to apply patches to state 12 | 13 | ## Example 14 | 15 | ```ts 16 | import { create, apply } from '../index'; 17 | 18 | const baseState = { foo: { bar: 'str' }, arr: [] }; 19 | const [state, patches] = create( 20 | baseState, 21 | (draft) => { 22 | draft.foo.bar = 'str2'; 23 | }, 24 | { enablePatches: true } 25 | ); 26 | expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); 27 | expect(patches).toEqual([{ op: 'replace', path: ['foo', 'bar'], value: 'str2' }]); 28 | expect(state).toEqual(apply(baseState, patches)); 29 | ``` 30 | 31 | ## Type Parameters 32 | 33 | • **T** *extends* `object` 34 | 35 | • **F** *extends* `boolean` = `false` 36 | 37 | ## Parameters 38 | 39 | • **state**: `T` 40 | 41 | • **patches**: [`Patches`](../type-aliases/Patches.md) 42 | 43 | • **applyOptions?**: `Pick`\<`Options`\<`boolean`, `F`\>, `"mark"` \| `"strict"` \| `"enableAutoFreeze"`\> 44 | 45 | ## Returns 46 | 47 | `T` \| `F` *extends* `true` ? [`Immutable`](../type-aliases/Immutable.md)\<`T`\> : `T` 48 | 49 | ## Defined in 50 | 51 | [apply.ts:26](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/apply.ts#L26) 52 | -------------------------------------------------------------------------------- /docs/functions/castDraft.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / castDraft 6 | 7 | # Function: castDraft() 8 | 9 | > **castDraft**\<`T`\>(`value`): [`Draft`](../type-aliases/Draft.md)\<`T`\> 10 | 11 | Cast a value to an Draft type value. 12 | 13 | ## Type Parameters 14 | 15 | • **T** 16 | 17 | ## Parameters 18 | 19 | • **value**: `T` 20 | 21 | ## Returns 22 | 23 | [`Draft`](../type-aliases/Draft.md)\<`T`\> 24 | 25 | ## Defined in 26 | 27 | [utils/cast.ts:6](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/cast.ts#L6) 28 | -------------------------------------------------------------------------------- /docs/functions/castImmutable.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / castImmutable 6 | 7 | # Function: castImmutable() 8 | 9 | > **castImmutable**\<`T`\>(`value`): [`Immutable`](../type-aliases/Immutable.md)\<`T`\> 10 | 11 | Cast a value to an Immutable type value. 12 | 13 | ## Type Parameters 14 | 15 | • **T** 16 | 17 | ## Parameters 18 | 19 | • **value**: `T` 20 | 21 | ## Returns 22 | 23 | [`Immutable`](../type-aliases/Immutable.md)\<`T`\> 24 | 25 | ## Defined in 26 | 27 | [utils/cast.ts:13](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/cast.ts#L13) 28 | -------------------------------------------------------------------------------- /docs/functions/castMutable.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / castMutable 6 | 7 | # Function: castMutable() 8 | 9 | > **castMutable**\<`T`\>(`draft`): `T` 10 | 11 | Cast a value to an Mutable type value. 12 | 13 | ## Type Parameters 14 | 15 | • **T** 16 | 17 | ## Parameters 18 | 19 | • **draft**: [`Draft`](../type-aliases/Draft.md)\<`T`\> 20 | 21 | ## Returns 22 | 23 | `T` 24 | 25 | ## Defined in 26 | 27 | [utils/cast.ts:20](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/cast.ts#L20) 28 | -------------------------------------------------------------------------------- /docs/functions/current.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / current 6 | 7 | # Function: current() 8 | 9 | ## current(target) 10 | 11 | > **current**\<`T`\>(`target`): `T` 12 | 13 | `current(draft)` to get current state in the draft mutation function. 14 | 15 | ## Example 16 | 17 | ```ts 18 | import { create, current } from '../index'; 19 | 20 | const baseState = { foo: { bar: 'str' }, arr: [] }; 21 | const state = create( 22 | baseState, 23 | (draft) => { 24 | draft.foo.bar = 'str2'; 25 | expect(current(draft.foo)).toEqual({ bar: 'str2' }); 26 | }, 27 | ); 28 | ``` 29 | 30 | ### Type Parameters 31 | 32 | • **T** *extends* `object` 33 | 34 | ### Parameters 35 | 36 | • **target**: [`Draft`](../type-aliases/Draft.md)\<`T`\> 37 | 38 | ### Returns 39 | 40 | `T` 41 | 42 | ### Defined in 43 | 44 | [current.ts:120](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/current.ts#L120) 45 | 46 | ## current(target) 47 | 48 | > **current**\<`T`\>(`target`): `T` 49 | 50 | ### Type Parameters 51 | 52 | • **T** *extends* `object` 53 | 54 | ### Parameters 55 | 56 | • **target**: `T` 57 | 58 | ### Returns 59 | 60 | `T` 61 | 62 | ### Deprecated 63 | 64 | You should call current only on `Draft` types. 65 | 66 | ### Defined in 67 | 68 | [current.ts:122](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/current.ts#L122) 69 | -------------------------------------------------------------------------------- /docs/functions/isDraft.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / isDraft 6 | 7 | # Function: isDraft() 8 | 9 | > **isDraft**(`target`): `boolean` 10 | 11 | Check if the value is a draft 12 | 13 | ## Parameters 14 | 15 | • **target**: `any` 16 | 17 | ## Returns 18 | 19 | `boolean` 20 | 21 | ## Defined in 22 | 23 | [utils/draft.ts:12](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/draft.ts#L12) 24 | -------------------------------------------------------------------------------- /docs/functions/isDraftable.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / isDraftable 6 | 7 | # Function: isDraftable() 8 | 9 | > **isDraftable**(`value`, `options`?): `boolean` 10 | 11 | Check if a value is draftable 12 | 13 | ## Parameters 14 | 15 | • **value**: `any` 16 | 17 | • **options?** 18 | 19 | • **options.mark?**: `Mark`\<`any`, `any`\> 20 | 21 | ## Returns 22 | 23 | `boolean` 24 | 25 | ## Defined in 26 | 27 | [utils/draft.ts:29](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/draft.ts#L29) 28 | -------------------------------------------------------------------------------- /docs/functions/makeCreator.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / makeCreator 6 | 7 | # Function: makeCreator() 8 | 9 | > **makeCreator**\<`_F`, `_O`\>(`options`?): \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options`?) => `CreateResult`\<`T`, `O`, `F`, `R`\>\<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options`?) => `CreateResult`\<`T`, `O`, `F`, `R`\>\<`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options`?) => (`base`, ...`args`) => `CreateResult`\<`T`, `O`, `F`, `R`\>\<`T`, `O`, `F`\>(`base`, `options`?) => [[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>] 10 | 11 | `makeCreator(options)` to make a creator function. 12 | 13 | ## Example 14 | 15 | ```ts 16 | import { makeCreator } from '../index'; 17 | 18 | const baseState = { foo: { bar: 'str' }, arr: [] }; 19 | const create = makeCreator({ enableAutoFreeze: true }); 20 | const state = create( 21 | baseState, 22 | (draft) => { 23 | draft.foo.bar = 'str2'; 24 | }, 25 | ); 26 | 27 | expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); 28 | expect(state).not.toBe(baseState); 29 | expect(state.foo).not.toBe(baseState.foo); 30 | expect(state.arr).toBe(baseState.arr); 31 | expect(Object.isFrozen(state)).toBeTruthy(); 32 | ``` 33 | 34 | ## Type Parameters 35 | 36 | • **_F** *extends* `boolean` = `false` 37 | 38 | • **_O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` 39 | 40 | ## Parameters 41 | 42 | • **options?**: [`Options`](../interfaces/Options.md)\<`_O`, `_F`\> 43 | 44 | ## Returns 45 | 46 | `Function` 47 | 48 | ### Type Parameters 49 | 50 | • **T** *extends* `unknown` 51 | 52 | • **F** *extends* `boolean` = `_F` 53 | 54 | • **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` 55 | 56 | • **R** *extends* `unknown` = `void` 57 | 58 | ### Parameters 59 | 60 | • **base**: `T` 61 | 62 | • **mutate** 63 | 64 | • **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> 65 | 66 | ### Returns 67 | 68 | `CreateResult`\<`T`, `O`, `F`, `R`\> 69 | 70 | ### Type Parameters 71 | 72 | • **T** *extends* `unknown` 73 | 74 | • **F** *extends* `boolean` = `_F` 75 | 76 | • **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` 77 | 78 | • **R** *extends* `void` \| `Promise`\<`void`\> = `void` 79 | 80 | ### Parameters 81 | 82 | • **base**: `T` 83 | 84 | • **mutate** 85 | 86 | • **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> 87 | 88 | ### Returns 89 | 90 | `CreateResult`\<`T`, `O`, `F`, `R`\> 91 | 92 | ### Type Parameters 93 | 94 | • **T** *extends* `unknown` 95 | 96 | • **P** *extends* `any`[] = [] 97 | 98 | • **F** *extends* `boolean` = `_F` 99 | 100 | • **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` 101 | 102 | • **R** *extends* `void` \| `Promise`\<`void`\> = `void` 103 | 104 | ### Parameters 105 | 106 | • **mutate** 107 | 108 | • **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> 109 | 110 | ### Returns 111 | 112 | `Function` 113 | 114 | #### Parameters 115 | 116 | • **base**: `T` 117 | 118 | • ...**args**: `P` 119 | 120 | #### Returns 121 | 122 | `CreateResult`\<`T`, `O`, `F`, `R`\> 123 | 124 | ### Type Parameters 125 | 126 | • **T** *extends* `unknown` 127 | 128 | • **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` 129 | 130 | • **F** *extends* `boolean` = `_F` 131 | 132 | ### Parameters 133 | 134 | • **base**: `T` 135 | 136 | • **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> 137 | 138 | ### Returns 139 | 140 | [[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>] 141 | 142 | ## Defined in 143 | 144 | [makeCreator.ts:87](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/makeCreator.ts#L87) 145 | -------------------------------------------------------------------------------- /docs/functions/markSimpleObject.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / markSimpleObject 6 | 7 | # Function: markSimpleObject() 8 | 9 | > **markSimpleObject**(`value`): `undefined` \| `"immutable"` 10 | 11 | ## Parameters 12 | 13 | • **value**: `unknown` 14 | 15 | ## Returns 16 | 17 | `undefined` \| `"immutable"` 18 | 19 | ## Defined in 20 | 21 | [utils/marker.ts:26](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/marker.ts#L26) 22 | -------------------------------------------------------------------------------- /docs/functions/original.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / original 6 | 7 | # Function: original() 8 | 9 | > **original**\<`T`\>(`target`): `T` 10 | 11 | `original(draft)` to get original state in the draft mutation function. 12 | 13 | ## Example 14 | 15 | ```ts 16 | import { create, original } from '../index'; 17 | 18 | const baseState = { foo: { bar: 'str' }, arr: [] }; 19 | const state = create( 20 | baseState, 21 | (draft) => { 22 | draft.foo.bar = 'str2'; 23 | expect(original(draft.foo)).toEqual({ bar: 'str' }); 24 | } 25 | ); 26 | ``` 27 | 28 | ## Type Parameters 29 | 30 | • **T** 31 | 32 | ## Parameters 33 | 34 | • **target**: `T` 35 | 36 | ## Returns 37 | 38 | `T` 39 | 40 | ## Defined in 41 | 42 | [original.ts:21](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/original.ts#L21) 43 | -------------------------------------------------------------------------------- /docs/functions/rawReturn.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / rawReturn 6 | 7 | # Function: rawReturn() 8 | 9 | > **rawReturn**\<`T`\>(`value`): `T` 10 | 11 | Use rawReturn() to wrap the return value to skip the draft check and thus improve performance. 12 | 13 | ## Example 14 | 15 | ```ts 16 | import { create, rawReturn } from '../index'; 17 | 18 | const baseState = { foo: { bar: 'str' }, arr: [] }; 19 | const state = create( 20 | baseState, 21 | (draft) => { 22 | return rawReturn(baseState); 23 | }, 24 | ); 25 | expect(state).toBe(baseState); 26 | ``` 27 | 28 | ## Type Parameters 29 | 30 | • **T** *extends* `undefined` \| `object` 31 | 32 | ## Parameters 33 | 34 | • **value**: `T` 35 | 36 | ## Returns 37 | 38 | `T` 39 | 40 | ## Defined in 41 | 42 | [rawReturn.ts:21](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/rawReturn.ts#L21) 43 | -------------------------------------------------------------------------------- /docs/functions/unsafe.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / unsafe 6 | 7 | # Function: unsafe() 8 | 9 | > **unsafe**\<`T`\>(`callback`): `T` 10 | 11 | `unsafe(callback)` to access mutable data directly in strict mode. 12 | 13 | ## Example 14 | 15 | ```ts 16 | import { create, unsafe } from '../index'; 17 | 18 | class Foobar { 19 | bar = 1; 20 | } 21 | 22 | const baseState = { foobar: new Foobar() }; 23 | const state = create( 24 | baseState, 25 | (draft) => { 26 | unsafe(() => { 27 | draft.foobar.bar = 2; 28 | }); 29 | }, 30 | { 31 | strict: true, 32 | } 33 | ); 34 | 35 | expect(state).toBe(baseState); 36 | expect(state.foobar).toBe(baseState.foobar); 37 | expect(state.foobar.bar).toBe(2); 38 | ``` 39 | 40 | ## Type Parameters 41 | 42 | • **T** 43 | 44 | ## Parameters 45 | 46 | • **callback** 47 | 48 | ## Returns 49 | 50 | `T` 51 | 52 | ## Defined in 53 | 54 | [unsafe.ts:53](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/unsafe.ts#L53) 55 | -------------------------------------------------------------------------------- /docs/interfaces/Options.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / Options 6 | 7 | # Interface: Options\ 8 | 9 | ## Type Parameters 10 | 11 | • **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) 12 | 13 | • **F** *extends* `boolean` 14 | 15 | ## Properties 16 | 17 | ### enableAutoFreeze? 18 | 19 | > `optional` **enableAutoFreeze**: `F` 20 | 21 | Enable autoFreeze, and return frozen state. 22 | 23 | #### Defined in 24 | 25 | [interface.ts:137](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L137) 26 | 27 | *** 28 | 29 | ### enablePatches? 30 | 31 | > `optional` **enablePatches**: `O` 32 | 33 | Enable patch, and return the patches and inversePatches. 34 | 35 | #### Defined in 36 | 37 | [interface.ts:133](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L133) 38 | 39 | *** 40 | 41 | ### mark? 42 | 43 | > `optional` **mark**: `Mark`\<`O`, `F`\> \| `Mark`\<`O`, `F`\>[] 44 | 45 | Set a mark to determine if the object is mutable or if an instance is an immutable. 46 | And it can also return a shallow copy function(AutoFreeze and Patches should both be disabled). 47 | 48 | #### Defined in 49 | 50 | [interface.ts:142](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L142) 51 | 52 | *** 53 | 54 | ### strict? 55 | 56 | > `optional` **strict**: `boolean` 57 | 58 | In strict mode, Forbid accessing non-draftable values and forbid returning a non-draft value. 59 | 60 | #### Defined in 61 | 62 | [interface.ts:129](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L129) 63 | -------------------------------------------------------------------------------- /docs/type-aliases/Draft.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / Draft 6 | 7 | # Type Alias: Draft\ 8 | 9 | > **Draft**\<`T`\>: `T` *extends* `Primitive` \| `AtomicObject` ? `T` : `T` *extends* `IfAvailable`\<`ReadonlyMap`\\> ? `DraftedMap`\<`K`, `V`\> : `T` *extends* `IfAvailable`\<`ReadonlySet`\\> ? `DraftedSet`\<`V`\> : `T` *extends* `WeakReferences` ? `T` : `T` *extends* `object` ? `DraftedObject`\<`T`\> : `T` 10 | 11 | ## Type Parameters 12 | 13 | • **T** 14 | 15 | ## Defined in 16 | 17 | [interface.ts:182](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L182) 18 | -------------------------------------------------------------------------------- /docs/type-aliases/Immutable.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / Immutable 6 | 7 | # Type Alias: Immutable\ 8 | 9 | > **Immutable**\<`T`\>: `T` *extends* `Primitive` \| `AtomicObject` ? `T` : `T` *extends* `IfAvailable`\<`ReadonlyMap`\\> ? `ImmutableMap`\<`K`, `V`\> : `T` *extends* `IfAvailable`\<`ReadonlySet`\\> ? `ImmutableSet`\<`V`\> : `T` *extends* `WeakReferences` ? `T` : `T` *extends* `object` ? `ImmutableObject`\<`T`\> : `T` 10 | 11 | ## Type Parameters 12 | 13 | • **T** 14 | 15 | ## Defined in 16 | 17 | [interface.ts:164](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L164) 18 | -------------------------------------------------------------------------------- /docs/type-aliases/Patch.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / Patch 6 | 7 | # Type Alias: Patch\ 8 | 9 | > **Patch**\<`P`\>: `P` *extends* `object` ? `IPatch` & `object` : `P` *extends* `true` \| `object` ? `IPatch` & `object` : `IPatch` & `object` 10 | 11 | ## Type Parameters 12 | 13 | • **P** *extends* [`PatchesOptions`](PatchesOptions.md) = `any` 14 | 15 | ## Defined in 16 | 17 | [interface.ts:58](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L58) 18 | -------------------------------------------------------------------------------- /docs/type-aliases/Patches.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / Patches 6 | 7 | # Type Alias: Patches\ 8 | 9 | > **Patches**\<`P`\>: [`Patch`](Patch.md)\<`P`\>[] 10 | 11 | ## Type Parameters 12 | 13 | • **P** *extends* [`PatchesOptions`](PatchesOptions.md) = `any` 14 | 15 | ## Defined in 16 | 17 | [interface.ts:72](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L72) 18 | -------------------------------------------------------------------------------- /docs/type-aliases/PatchesOptions.md: -------------------------------------------------------------------------------- 1 | [**mutative**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [mutative](../README.md) / PatchesOptions 6 | 7 | # Type Alias: PatchesOptions 8 | 9 | > **PatchesOptions**: `boolean` \| `object` 10 | 11 | ## Defined in 12 | 13 | [interface.ts:18](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L18) 14 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-var 2 | declare var __DEV__: boolean; 3 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import replace from '@rollup/plugin-replace'; 5 | import terser from '@rollup/plugin-terser'; 6 | import typescript from '@rollup/plugin-typescript'; 7 | import pkg from './package.json'; 8 | 9 | export default [ 10 | { 11 | input: 'src/index.ts', 12 | output: [ 13 | { 14 | format: 'cjs', 15 | file: 'dist/mutative.cjs.production.min.js', 16 | sourcemap: true, 17 | plugins: [terser()], 18 | }, 19 | { 20 | format: 'umd', 21 | name: pkg.name 22 | .split('-') 23 | .map(([s, ...rest]) => [s.toUpperCase(), ...rest].join('')) 24 | .join(''), 25 | file: pkg.unpkg, 26 | sourcemap: true, 27 | plugins: [terser()], 28 | }, 29 | ], 30 | plugins: [ 31 | resolve(), 32 | commonjs(), 33 | typescript({ 34 | declaration: true, 35 | declarationDir: 'dist', 36 | }), 37 | replace({ 38 | __DEV__: 'false', 39 | preventAssignment: true, 40 | }), 41 | ], 42 | }, 43 | { 44 | input: 'src/index.ts', 45 | output: [ 46 | { 47 | format: 'cjs', 48 | file: 'dist/mutative.cjs.development.js', 49 | sourcemap: true, 50 | }, 51 | { 52 | format: 'es', 53 | file: 'dist/mutative.esm.js', 54 | sourcemap: true, 55 | }, 56 | { 57 | format: 'es', 58 | file: 'dist/mutative.esm.mjs', 59 | sourcemap: true, 60 | }, 61 | { 62 | format: 'umd', 63 | name: pkg.name 64 | .split('-') 65 | .map(([s, ...rest]) => [s.toUpperCase(), ...rest].join('')) 66 | .join(''), 67 | file: pkg.unpkg.replace('.production.min.js', '.development.js'), 68 | sourcemap: true, 69 | }, 70 | ], 71 | plugins: [ 72 | resolve(), 73 | commonjs(), 74 | typescript({ 75 | declaration: true, 76 | declarationDir: 'dist', 77 | }), 78 | replace({ 79 | __DEV__: 'true', 80 | preventAssignment: true, 81 | }), 82 | { 83 | name: 'create-cjs-index', 84 | buildEnd: () => { 85 | fs.writeFileSync( 86 | 'dist/index.js', 87 | ` 88 | 'use strict' 89 | 90 | if (process.env.NODE_ENV === 'production') { 91 | module.exports = require('./mutative.cjs.production.min.js') 92 | } else { 93 | module.exports = require('./mutative.cjs.development.js') 94 | } 95 | ` 96 | ); 97 | }, 98 | }, 99 | ], 100 | }, 101 | ]; 102 | -------------------------------------------------------------------------------- /src/constant.ts: -------------------------------------------------------------------------------- 1 | // Don't use `Symbol()` just for 3rd party access the draft 2 | export const PROXY_DRAFT = Symbol.for('__MUTATIVE_PROXY_DRAFT__'); 3 | export const RAW_RETURN_SYMBOL = Symbol('__MUTATIVE_RAW_RETURN_SYMBOL__'); 4 | 5 | export const iteratorSymbol: typeof Symbol.iterator = Symbol.iterator; 6 | 7 | export const dataTypes = { 8 | mutable: 'mutable', 9 | immutable: 'immutable', 10 | } as const; 11 | -------------------------------------------------------------------------------- /src/create.ts: -------------------------------------------------------------------------------- 1 | import { makeCreator } from './makeCreator'; 2 | 3 | /** 4 | * `create(baseState, callback, options)` to create the next state 5 | * 6 | * ## Example 7 | * 8 | * ```ts 9 | * import { create } from '../index'; 10 | * 11 | * const baseState = { foo: { bar: 'str' }, arr: [] }; 12 | * const state = create( 13 | * baseState, 14 | * (draft) => { 15 | * draft.foo.bar = 'str2'; 16 | * }, 17 | * ); 18 | * 19 | * expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); 20 | * expect(state).not.toBe(baseState); 21 | * expect(state.foo).not.toBe(baseState.foo); 22 | * expect(state.arr).toBe(baseState.arr); 23 | * ``` 24 | */ 25 | const create = makeCreator(); 26 | 27 | export { create }; 28 | -------------------------------------------------------------------------------- /src/draftify.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Finalities, 3 | Options, 4 | Patches, 5 | PatchesOptions, 6 | Result, 7 | } from './interface'; 8 | import { createDraft, finalizeDraft } from './draft'; 9 | import { isDraftable } from './utils'; 10 | import { dataTypes } from './constant'; 11 | 12 | export function draftify< 13 | T extends object, 14 | O extends PatchesOptions = false, 15 | F extends boolean = false 16 | >( 17 | baseState: T, 18 | options: Options 19 | ): [T, (returnedValue: [T] | []) => Result] { 20 | const finalities: Finalities = { 21 | draft: [], 22 | revoke: [], 23 | handledSet: new WeakSet(), 24 | }; 25 | let patches: Patches | undefined; 26 | let inversePatches: Patches | undefined; 27 | if (options.enablePatches) { 28 | patches = []; 29 | inversePatches = []; 30 | } 31 | const isMutable = 32 | options.mark?.(baseState, dataTypes) === dataTypes.mutable || 33 | !isDraftable(baseState, options); 34 | const draft = isMutable 35 | ? baseState 36 | : createDraft({ 37 | original: baseState, 38 | parentDraft: null, 39 | finalities, 40 | options, 41 | }); 42 | return [ 43 | draft, 44 | (returnedValue: [T] | [] = []) => { 45 | const [finalizedState, finalizedPatches, finalizedInversePatches] = 46 | finalizeDraft( 47 | draft, 48 | returnedValue, 49 | patches, 50 | inversePatches, 51 | options.enableAutoFreeze 52 | ); 53 | return ( 54 | options.enablePatches 55 | ? [finalizedState, finalizedPatches, finalizedInversePatches] 56 | : finalizedState 57 | ) as Result; 58 | }, 59 | ]; 60 | } 61 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { makeCreator } from './makeCreator'; 2 | export { create } from './create'; 3 | export { apply } from './apply'; 4 | export { original } from './original'; 5 | export { current } from './current'; 6 | export { unsafe } from './unsafe'; 7 | export { rawReturn } from './rawReturn'; 8 | export { isDraft } from './utils/draft'; 9 | export { isDraftable } from './utils/draft'; 10 | export { markSimpleObject } from './utils/marker'; 11 | 12 | export { castDraft, castImmutable, castMutable } from './utils/cast'; 13 | export type { 14 | Immutable, 15 | Draft, 16 | Patches, 17 | Patch, 18 | ExternalOptions as Options, 19 | PatchesOptions, 20 | } from './interface'; 21 | -------------------------------------------------------------------------------- /src/internal.ts: -------------------------------------------------------------------------------- 1 | import { createDraft } from './draft'; 2 | 3 | export const internal = {} as { 4 | createDraft: typeof createDraft; 5 | }; 6 | -------------------------------------------------------------------------------- /src/map.ts: -------------------------------------------------------------------------------- 1 | import { dataTypes, iteratorSymbol } from './constant'; 2 | import { internal } from './internal'; 3 | import { generatePatches } from './patch'; 4 | import { checkReadable } from './unsafe'; 5 | import { 6 | ensureShallowCopy, 7 | getProxyDraft, 8 | isDraftable, 9 | isEqual, 10 | latest, 11 | markChanged, 12 | markFinalization, 13 | } from './utils'; 14 | 15 | export const mapHandler = { 16 | get size() { 17 | const current: Map = latest(getProxyDraft(this)!); 18 | return current.size; 19 | }, 20 | has(key: any): boolean { 21 | return latest(getProxyDraft(this)!).has(key); 22 | }, 23 | set(key: any, value: any) { 24 | const target = getProxyDraft(this)!; 25 | const source = latest(target); 26 | if (!source.has(key) || !isEqual(source.get(key), value)) { 27 | ensureShallowCopy(target); 28 | markChanged(target); 29 | target.assignedMap!.set(key, true); 30 | target.copy.set(key, value); 31 | markFinalization(target, key, value, generatePatches); 32 | } 33 | return this; 34 | }, 35 | delete(key: any): boolean { 36 | if (!this.has(key)) { 37 | return false; 38 | } 39 | const target = getProxyDraft(this)!; 40 | ensureShallowCopy(target); 41 | markChanged(target); 42 | if (target.original.has(key)) { 43 | target.assignedMap!.set(key, false); 44 | } else { 45 | target.assignedMap!.delete(key); 46 | } 47 | target.copy.delete(key); 48 | return true; 49 | }, 50 | clear() { 51 | const target = getProxyDraft(this)!; 52 | if (!this.size) return; 53 | ensureShallowCopy(target); 54 | markChanged(target); 55 | target.assignedMap = new Map(); 56 | for (const [key] of target.original) { 57 | target.assignedMap.set(key, false); 58 | } 59 | target.copy!.clear(); 60 | }, 61 | forEach(callback: (value: any, key: any, self: any) => void, thisArg?: any) { 62 | const target = getProxyDraft(this)!; 63 | latest(target).forEach((_value: any, _key: any) => { 64 | callback.call(thisArg, this.get(_key), _key, this); 65 | }); 66 | }, 67 | get(key: any): any { 68 | const target = getProxyDraft(this)!; 69 | const value = latest(target).get(key); 70 | const mutable = 71 | target.options.mark?.(value, dataTypes) === dataTypes.mutable; 72 | if (target.options.strict) { 73 | checkReadable(value, target.options, mutable); 74 | } 75 | if (mutable) { 76 | return value; 77 | } 78 | if (target.finalized || !isDraftable(value, target.options)) { 79 | return value; 80 | } 81 | // drafted or reassigned 82 | if (value !== target.original.get(key)) { 83 | return value; 84 | } 85 | const draft = internal.createDraft({ 86 | original: value, 87 | parentDraft: target, 88 | key, 89 | finalities: target.finalities, 90 | options: target.options, 91 | }); 92 | ensureShallowCopy(target); 93 | target.copy.set(key, draft); 94 | return draft; 95 | }, 96 | keys(): IterableIterator { 97 | return latest(getProxyDraft(this)!).keys(); 98 | }, 99 | values(): IterableIterator { 100 | const iterator = this.keys(); 101 | return { 102 | [iteratorSymbol]: () => this.values(), 103 | next: () => { 104 | const result = iterator.next(); 105 | if (result.done) return result; 106 | const value = this.get(result.value); 107 | return { 108 | done: false, 109 | value, 110 | }; 111 | }, 112 | } as any; 113 | }, 114 | entries(): IterableIterator<[any, any]> { 115 | const iterator = this.keys(); 116 | return { 117 | [iteratorSymbol]: () => this.entries(), 118 | next: () => { 119 | const result = iterator.next(); 120 | if (result.done) return result; 121 | const value = this.get(result.value); 122 | return { 123 | done: false, 124 | value: [result.value, value], 125 | }; 126 | }, 127 | } as any; 128 | }, 129 | [iteratorSymbol]() { 130 | return this.entries(); 131 | }, 132 | }; 133 | 134 | export const mapHandlerKeys = Reflect.ownKeys(mapHandler); 135 | -------------------------------------------------------------------------------- /src/original.ts: -------------------------------------------------------------------------------- 1 | import { getProxyDraft } from './utils'; 2 | 3 | /** 4 | * `original(draft)` to get original state in the draft mutation function. 5 | * 6 | * ## Example 7 | * 8 | * ```ts 9 | * import { create, original } from '../index'; 10 | * 11 | * const baseState = { foo: { bar: 'str' }, arr: [] }; 12 | * const state = create( 13 | * baseState, 14 | * (draft) => { 15 | * draft.foo.bar = 'str2'; 16 | * expect(original(draft.foo)).toEqual({ bar: 'str' }); 17 | * } 18 | * ); 19 | * ``` 20 | */ 21 | export function original(target: T): T { 22 | const proxyDraft = getProxyDraft(target); 23 | if (!proxyDraft) { 24 | throw new Error( 25 | `original() is only used for a draft, parameter: ${target}` 26 | ); 27 | } 28 | return proxyDraft.original; 29 | } 30 | -------------------------------------------------------------------------------- /src/rawReturn.ts: -------------------------------------------------------------------------------- 1 | import { RAW_RETURN_SYMBOL } from './constant'; 2 | 3 | /** 4 | * Use rawReturn() to wrap the return value to skip the draft check and thus improve performance. 5 | * 6 | * ## Example 7 | * 8 | * ```ts 9 | * import { create, rawReturn } from '../index'; 10 | * 11 | * const baseState = { foo: { bar: 'str' }, arr: [] }; 12 | * const state = create( 13 | * baseState, 14 | * (draft) => { 15 | * return rawReturn(baseState); 16 | * }, 17 | * ); 18 | * expect(state).toBe(baseState); 19 | * ``` 20 | */ 21 | export function rawReturn(value: T): T { 22 | if (arguments.length === 0) { 23 | throw new Error('rawReturn() must be called with a value.'); 24 | } 25 | if (arguments.length > 1) { 26 | throw new Error('rawReturn() must be called with one argument.'); 27 | } 28 | if ( 29 | __DEV__ && 30 | value !== undefined && 31 | (typeof value !== 'object' || value === null) 32 | ) { 33 | console.warn( 34 | 'rawReturn() must be called with an object(including plain object, arrays, Set, Map, etc.) or `undefined`, other types do not need to be returned via rawReturn().' 35 | ); 36 | } 37 | return { 38 | [RAW_RETURN_SYMBOL]: [value], 39 | } as never; 40 | } 41 | -------------------------------------------------------------------------------- /src/unsafe.ts: -------------------------------------------------------------------------------- 1 | import { Options } from './interface'; 2 | import { isDraftable } from './utils'; 3 | 4 | let readable = false; 5 | 6 | export const checkReadable = ( 7 | value: any, 8 | options: Options, 9 | ignoreCheckDraftable = false 10 | ) => { 11 | if ( 12 | typeof value === 'object' && 13 | value !== null && 14 | (!isDraftable(value, options) || ignoreCheckDraftable) && 15 | !readable 16 | ) { 17 | throw new Error( 18 | `Strict mode: Mutable data cannot be accessed directly, please use 'unsafe(callback)' wrap.` 19 | ); 20 | } 21 | }; 22 | 23 | /** 24 | * `unsafe(callback)` to access mutable data directly in strict mode. 25 | * 26 | * ## Example 27 | * 28 | * ```ts 29 | * import { create, unsafe } from '../index'; 30 | * 31 | * class Foobar { 32 | * bar = 1; 33 | * } 34 | * 35 | * const baseState = { foobar: new Foobar() }; 36 | * const state = create( 37 | * baseState, 38 | * (draft) => { 39 | * unsafe(() => { 40 | * draft.foobar.bar = 2; 41 | * }); 42 | * }, 43 | * { 44 | * strict: true, 45 | * } 46 | * ); 47 | * 48 | * expect(state).toBe(baseState); 49 | * expect(state.foobar).toBe(baseState.foobar); 50 | * expect(state.foobar.bar).toBe(2); 51 | * ``` 52 | */ 53 | export function unsafe(callback: () => T): T { 54 | readable = true; 55 | let result: T; 56 | try { 57 | result = callback(); 58 | } finally { 59 | readable = false; 60 | } 61 | return result; 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/cast.ts: -------------------------------------------------------------------------------- 1 | import { Draft, Immutable } from '../interface'; 2 | 3 | /** 4 | * Cast a value to an Draft type value. 5 | */ 6 | export function castDraft(value: T): Draft { 7 | return value as any; 8 | } 9 | 10 | /** 11 | * Cast a value to an Immutable type value. 12 | */ 13 | export function castImmutable(value: T): Immutable { 14 | return value as any; 15 | } 16 | 17 | /** 18 | * Cast a value to an Mutable type value. 19 | */ 20 | export function castMutable(draft: Draft): T { 21 | return draft as any; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/deepFreeze.ts: -------------------------------------------------------------------------------- 1 | import { DraftType } from '../interface'; 2 | import { getType, isDraft } from './draft'; 3 | 4 | function throwFrozenError() { 5 | throw new Error('Cannot modify frozen object'); 6 | } 7 | 8 | function isFreezable(value: any) { 9 | return ( 10 | __DEV__ || (value && typeof value === 'object' && !Object.isFrozen(value)) 11 | ); 12 | } 13 | 14 | export function deepFreeze( 15 | target: any, 16 | subKey?: any, 17 | updatedValues?: WeakMap, 18 | stack?: any[], 19 | keys?: any[] 20 | ) { 21 | if (__DEV__) { 22 | updatedValues = updatedValues ?? new WeakMap(); 23 | stack = stack ?? []; 24 | keys = keys ?? []; 25 | const value = updatedValues.has(target) 26 | ? updatedValues.get(target) 27 | : target; 28 | if (stack.length > 0) { 29 | const index = stack.indexOf(value); 30 | if (value && typeof value === 'object' && index !== -1) { 31 | if (stack[0] === value) { 32 | throw new Error(`Forbids circular reference`); 33 | } 34 | throw new Error( 35 | `Forbids circular reference: ~/${keys 36 | .slice(0, index) 37 | .map((key, index) => { 38 | if (typeof key === 'symbol') return `[${key.toString()}]`; 39 | const parent = stack![index]; 40 | if ( 41 | typeof key === 'object' && 42 | (parent instanceof Map || parent instanceof Set) 43 | ) 44 | return Array.from(parent.keys()).indexOf(key); 45 | return key; 46 | }) 47 | .join('/')}` 48 | ); 49 | } 50 | stack.push(value); 51 | keys.push(subKey); 52 | } else { 53 | stack.push(value); 54 | } 55 | } 56 | if (Object.isFrozen(target) || isDraft(target)) { 57 | if (__DEV__) { 58 | stack!.pop(); 59 | keys!.pop(); 60 | } 61 | return; 62 | } 63 | const type = getType(target); 64 | switch (type) { 65 | case DraftType.Map: 66 | for (const [key, value] of target) { 67 | if (isFreezable(key)) deepFreeze(key, key, updatedValues, stack, keys); 68 | if (isFreezable(value)) 69 | deepFreeze(value, key, updatedValues, stack, keys); 70 | } 71 | target.set = target.clear = target.delete = throwFrozenError; 72 | break; 73 | case DraftType.Set: 74 | for (const value of target) { 75 | if (isFreezable(value)) 76 | deepFreeze(value, value, updatedValues, stack, keys); 77 | } 78 | target.add = target.clear = target.delete = throwFrozenError; 79 | break; 80 | case DraftType.Array: 81 | Object.freeze(target); 82 | let index = 0; 83 | for (const value of target) { 84 | if (isFreezable(value)) 85 | deepFreeze(value, index, updatedValues, stack, keys); 86 | index += 1; 87 | } 88 | break; 89 | default: 90 | Object.freeze(target); 91 | // ignore non-enumerable or symbol properties 92 | Object.keys(target).forEach((name) => { 93 | const value = target[name]; 94 | if (isFreezable(value)) 95 | deepFreeze(value, name, updatedValues, stack, keys); 96 | }); 97 | } 98 | if (__DEV__) { 99 | stack!.pop(); 100 | keys!.pop(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/utils/forEach.ts: -------------------------------------------------------------------------------- 1 | import { DraftType } from '../interface'; 2 | import { getType } from './draft'; 3 | 4 | export function forEach( 5 | target: T, 6 | iter: (key: string | number | symbol, value: any, source: T) => void 7 | ) { 8 | const type = getType(target); 9 | if (type === DraftType.Object) { 10 | Reflect.ownKeys(target).forEach((key) => { 11 | iter(key, (target as any)[key], target); 12 | }); 13 | } else if (type === DraftType.Array) { 14 | let index = 0; 15 | for (const entry of target as any[]) { 16 | iter(index, entry, target); 17 | index += 1; 18 | } 19 | } else { 20 | (target as Map | Set).forEach((entry: any, index: any) => 21 | iter(index, entry, target) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './copy'; 2 | export * from './mark'; 3 | export * from './deepFreeze'; 4 | export * from './draft'; 5 | export * from './proto'; 6 | export * from './forEach'; 7 | export * from './finalize'; 8 | -------------------------------------------------------------------------------- /src/utils/mark.ts: -------------------------------------------------------------------------------- 1 | import { ProxyDraft } from '../interface'; 2 | 3 | export function markChanged(proxyDraft: ProxyDraft) { 4 | proxyDraft.assignedMap = proxyDraft.assignedMap ?? new Map(); 5 | if (!proxyDraft.operated) { 6 | proxyDraft.operated = true; 7 | if (proxyDraft.parent) { 8 | markChanged(proxyDraft.parent); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/marker.ts: -------------------------------------------------------------------------------- 1 | import { dataTypes } from '../constant'; 2 | 3 | const constructorString = Object.prototype.constructor.toString(); 4 | /** 5 | * Check if the value is a simple object(No prototype chain object or iframe same-origin object), 6 | * support case: https://github.com/unadlib/mutative/issues/17 7 | */ 8 | const isSimpleObject = (value: unknown) => { 9 | if (!value || typeof value !== 'object') return false; 10 | const prototype = Object.getPrototypeOf(value); 11 | if (prototype === null) { 12 | return true; 13 | } 14 | const constructor = 15 | Object.hasOwnProperty.call(prototype, 'constructor') && 16 | prototype.constructor; 17 | 18 | if (constructor === Object) return true; 19 | 20 | return ( 21 | typeof constructor === 'function' && 22 | Function.toString.call(constructor) === constructorString 23 | ); 24 | }; 25 | 26 | export const markSimpleObject = (value: unknown) => { 27 | if (isSimpleObject(value)) { 28 | return dataTypes.immutable; 29 | } 30 | return; 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/proto.ts: -------------------------------------------------------------------------------- 1 | export function has(target: object, key: PropertyKey) { 2 | return target instanceof Map 3 | ? target.has(key) 4 | : Object.prototype.hasOwnProperty.call(target, key); 5 | } 6 | 7 | export function getDescriptor(target: object, key: PropertyKey) { 8 | if (key in target) { 9 | let prototype = Reflect.getPrototypeOf(target); 10 | while (prototype) { 11 | const descriptor = Reflect.getOwnPropertyDescriptor(prototype, key); 12 | if (descriptor) return descriptor; 13 | prototype = Reflect.getPrototypeOf(prototype); 14 | } 15 | } 16 | return; 17 | } 18 | 19 | export function isBaseSetInstance(obj: any) { 20 | return Object.getPrototypeOf(obj) === Set.prototype; 21 | } 22 | 23 | export function isBaseMapInstance(obj: any) { 24 | return Object.getPrototypeOf(obj) === Map.prototype; 25 | } 26 | -------------------------------------------------------------------------------- /test/__immer_performance_tests__/add-data.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 'use strict'; 3 | 4 | import { produce, setAutoFreeze } from 'immer'; 5 | import cloneDeep from 'lodash.clonedeep'; 6 | import immutable from 'immutable'; 7 | import Seamless from 'seamless-immutable'; 8 | import deepFreeze from 'deep-freeze'; 9 | import { measure } from './measure'; 10 | import { create } from '../..'; 11 | 12 | const { fromJS } = immutable; 13 | 14 | console.log('\n# add-data - loading large set of data\n'); 15 | 16 | import dataSet from './data.json' assert { type: 'json' }; 17 | 18 | const baseState = { 19 | data: null, 20 | }; 21 | const frozenBazeState = deepFreeze(cloneDeep(baseState)); 22 | const immutableJsBaseState = fromJS(baseState); 23 | const seamlessBaseState = Seamless.from(baseState); 24 | 25 | const MAX = 10000; 26 | 27 | measure( 28 | 'just mutate', 29 | () => ({ draft: cloneDeep(baseState) }), 30 | ({ draft }) => { 31 | draft.data = dataSet; 32 | } 33 | ); 34 | 35 | measure( 36 | 'just mutate, freeze', 37 | () => ({ draft: cloneDeep(baseState) }), 38 | ({ draft }) => { 39 | draft.data = dataSet; 40 | deepFreeze(draft); 41 | } 42 | ); 43 | 44 | measure('handcrafted reducer (no freeze)', () => { 45 | const nextState = { 46 | ...baseState, 47 | data: dataSet, 48 | }; 49 | }); 50 | 51 | measure('handcrafted reducer (with freeze)', () => { 52 | const nextState = deepFreeze({ 53 | ...baseState, 54 | data: dataSet, 55 | }); 56 | }); 57 | 58 | measure('immutableJS', () => { 59 | let state = immutableJsBaseState.withMutations((state) => { 60 | state.setIn(['data'], fromJS(dataSet)); 61 | }); 62 | }); 63 | 64 | measure('immutableJS + toJS', () => { 65 | let state = immutableJsBaseState 66 | .withMutations((state) => { 67 | state.setIn(['data'], fromJS(dataSet)); 68 | }) 69 | .toJS(); 70 | }); 71 | 72 | measure('seamless-immutable', () => { 73 | seamlessBaseState.set('data', dataSet); 74 | }); 75 | 76 | measure('seamless-immutable + asMutable', () => { 77 | seamlessBaseState.set('data', dataSet).asMutable({ deep: true }); 78 | }); 79 | 80 | measure('immer - without autofreeze * ' + MAX, () => { 81 | setAutoFreeze(false); 82 | for (let i = 0; i < MAX; i++) 83 | produce(baseState, (draft) => { 84 | draft.data = dataSet; 85 | }); 86 | }); 87 | 88 | measure('immer - with autofreeze * ' + MAX, () => { 89 | setAutoFreeze(true); 90 | for (let i = 0; i < MAX; i++) 91 | produce(frozenBazeState, (draft) => { 92 | draft.data = dataSet; 93 | }); 94 | }); 95 | 96 | measure( 97 | 'mutative without autofreeze * ' + MAX, 98 | () => cloneDeep(baseState), 99 | (baseState) => { 100 | for (let i = 0; i < MAX; i++) 101 | create(baseState, (draft) => { 102 | draft.data = dataSet; 103 | }); 104 | } 105 | ); 106 | 107 | measure( 108 | 'mutative with autofreeze * ' + MAX, 109 | () => cloneDeep(baseState), 110 | (baseState) => { 111 | for (let i = 0; i < MAX; i++) 112 | create( 113 | baseState, 114 | (draft) => { 115 | draft.data = dataSet; 116 | }, 117 | { 118 | enableAutoFreeze: true, 119 | } 120 | ); 121 | } 122 | ); 123 | -------------------------------------------------------------------------------- /test/__immer_performance_tests__/incremental.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 'use strict'; 3 | import { produce, setAutoFreeze } from 'immer'; 4 | import cloneDeep from 'lodash.clonedeep'; 5 | import Immutable from 'immutable'; 6 | import { measure } from './measure'; 7 | import { create } from '../..'; 8 | 9 | console.log('\n# incremental - lot of small incremental changes\n'); 10 | 11 | function createTestObject() { 12 | return { 13 | a: 1, 14 | b: 'Some data here', 15 | }; 16 | } 17 | 18 | const MAX = 1000; 19 | const baseState = { 20 | ids: [], 21 | map: Object.create(null), 22 | }; 23 | 24 | let immutableJsBaseState; 25 | 26 | immutableJsBaseState = { 27 | ids: Immutable.List(), 28 | map: Immutable.Map(), 29 | }; 30 | 31 | measure( 32 | 'just mutate', 33 | () => cloneDeep(baseState), 34 | (draft) => { 35 | for (let i = 0; i < MAX; i++) { 36 | draft.ids.push(i); 37 | draft.map[i] = createTestObject(); 38 | } 39 | } 40 | ); 41 | 42 | measure( 43 | 'handcrafted reducer', 44 | () => cloneDeep(baseState), 45 | (state) => { 46 | for (let i = 0; i < MAX; i++) { 47 | state = { 48 | ids: [...state.ids, i], 49 | map: { 50 | ...state.map, 51 | [i]: createTestObject(), 52 | }, 53 | }; 54 | } 55 | } 56 | ); 57 | 58 | measure( 59 | 'immutableJS', 60 | () => immutableJsBaseState, 61 | (state) => { 62 | for (let i = 0; i < MAX; i++) { 63 | state = { 64 | ids: state.ids.push(i), 65 | map: state.map.set(i, createTestObject()), 66 | }; 67 | } 68 | } 69 | ); 70 | 71 | measure( 72 | 'immer', 73 | () => { 74 | setAutoFreeze(false); 75 | return baseState; 76 | }, 77 | (state) => { 78 | for (let i = 0; i < MAX; i++) { 79 | state = produce(state, (draft) => { 80 | draft.ids.push(i); 81 | draft.map[i] = createTestObject(); 82 | }); 83 | } 84 | } 85 | ); 86 | 87 | measure( 88 | 'immer - single produce', 89 | () => { 90 | setAutoFreeze(false); 91 | return baseState; 92 | }, 93 | (state) => { 94 | produce(state, (draft) => { 95 | for (let i = 0; i < MAX; i++) { 96 | draft.ids.push(i); 97 | draft.map[i] = createTestObject(); 98 | } 99 | }); 100 | } 101 | ); 102 | 103 | measure( 104 | 'mutative', 105 | () => baseState, 106 | (baseState) => { 107 | let state = baseState; 108 | for (let i = 0; i < MAX; i++) { 109 | state = create(state, (draft) => { 110 | draft.ids.push(i); 111 | draft.map[i] = createTestObject(); 112 | }); 113 | } 114 | } 115 | ); 116 | 117 | measure( 118 | 'mutative - single create', 119 | () => baseState, 120 | (state) => { 121 | create(state, (draft) => { 122 | for (let i = 0; i < MAX; i++) { 123 | draft.ids.push(i); 124 | draft.map[i] = createTestObject(); 125 | } 126 | }); 127 | } 128 | ); 129 | -------------------------------------------------------------------------------- /test/__immer_performance_tests__/large-obj.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { produce, setUseStrictShallowCopy } from 'immer'; 3 | import { measure } from './measure'; 4 | 5 | console.log('\n# large-obj - mutate large object\n'); 6 | 7 | const MAX = 50; 8 | 9 | const baseState = Object.fromEntries( 10 | Array(10000) 11 | .fill(0) 12 | .map((_, i) => [i, i]) 13 | ); 14 | 15 | measure('immer - with setUseStrictShallowCopy', () => { 16 | setUseStrictShallowCopy(true); 17 | 18 | for (let i = 0; i < MAX; i++) { 19 | produce(baseState, (draft) => { 20 | draft[5000]++; 21 | }); 22 | } 23 | }); 24 | 25 | measure('immer - without setUseStrictShallowCopy', () => { 26 | setUseStrictShallowCopy(false); 27 | 28 | for (let i = 0; i < MAX; i++) { 29 | produce(baseState, (draft) => { 30 | draft[5000]++; 31 | }); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /test/__immer_performance_tests__/measure.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 'use strict'; 3 | 4 | function measureTime(setup, fn) { 5 | if (!fn) { 6 | fn = setup; 7 | setup = () => {}; 8 | } 9 | const args = setup(); 10 | global.gc && global.gc(); 11 | const startTime = Date.now(); 12 | fn(args); 13 | const endTime = Date.now(); 14 | return endTime - startTime; 15 | } 16 | 17 | export function measure(name, setup, fn) { 18 | const times = [...Array(5)].map(() => measureTime(setup, fn)); 19 | const medianTime = times.sort()[Math.round(times.length / 2)]; 20 | console.log(`${name}: ${medianTime}ms`); 21 | } 22 | -------------------------------------------------------------------------------- /test/__snapshots__/current.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`current() for Custom Set/Map draft 1`] = ` 4 | { 5 | "a": Set { 6 | { 7 | "id": 42, 8 | }, 9 | { 10 | "id": 43, 11 | }, 12 | }, 13 | "b": Map { 14 | 1 => { 15 | "id": 42, 16 | }, 17 | 2 => { 18 | "id": 43, 19 | }, 20 | }, 21 | "x": { 22 | "y": { 23 | "z": { 24 | "k": 43, 25 | }, 26 | }, 27 | }, 28 | } 29 | `; 30 | 31 | exports[`current() for Custom Set/Map draft 2`] = ` 32 | { 33 | "a": Set { 34 | { 35 | "id": 42, 36 | }, 37 | { 38 | "id": 43, 39 | }, 40 | Set { 41 | {}, 42 | }, 43 | }, 44 | "b": Map { 45 | 1 => { 46 | "id": 42, 47 | }, 48 | 2 => { 49 | "id": 43, 50 | }, 51 | }, 52 | "x": { 53 | "y": { 54 | "z": { 55 | "k": 43, 56 | }, 57 | }, 58 | }, 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /test/__snapshots__/immer-non-support.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`error key setting in array 1`] = `"Only supports setting array indices and the 'length' property."`; 4 | 5 | exports[`error key setting in array 2`] = `"Only supports setting array indices and the 'length' property."`; 6 | 7 | exports[`error key setting in array 3`] = `"Only supports setting array indices and the 'length' property."`; 8 | 9 | exports[`error key setting in array 4`] = `"Only supports setting array indices and the 'length' property."`; 10 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`check array options error 1`] = `"Only supports setting array indices and the 'length' property."`; 4 | 5 | exports[`check array options error 2`] = `"Only supports setting array indices and the 'length' property."`; 6 | 7 | exports[`check array options error 3`] = `"Only supports setting array indices and the 'length' property."`; 8 | 9 | exports[`check array options error 4`] = `"Only supports setting array indices and the 'length' property."`; 10 | 11 | exports[`check array options error 5`] = `"Only supports setting array indices and the 'length' property."`; 12 | 13 | exports[`check array options error 6`] = `"Only supports setting array indices and the 'length' property."`; 14 | 15 | exports[`check array options error 7`] = `"Only supports setting array indices and the 'length' property."`; 16 | 17 | exports[`check array options error 8`] = `"Only supports setting array indices and the 'length' property."`; 18 | -------------------------------------------------------------------------------- /test/apply.0.snap.ts: -------------------------------------------------------------------------------- 1 | import { create, apply } from '../index'; 2 | 3 | const baseState = { foo: { bar: 'str' }, arr: [] }; 4 | const [state, patches] = create( 5 | baseState, 6 | (draft) => { 7 | draft.foo.bar = 'str2'; 8 | }, 9 | { enablePatches: true } 10 | ); 11 | expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); 12 | expect(patches).toEqual([{ op: 'replace', path: ['foo', 'bar'], value: 'str2' }]); 13 | expect(state).toEqual(apply(baseState, patches)); 14 | -------------------------------------------------------------------------------- /test/array.test.ts: -------------------------------------------------------------------------------- 1 | import { create, isDraft } from '../src'; 2 | 3 | test('shift', () => { 4 | const obj = { 5 | a: Array.from({ length: 20 }, (_, i) => ({ i })), 6 | o: { b: { c: 1 } }, 7 | }; 8 | const state = create(obj, (draft) => { 9 | const a = draft.a.shift()!; 10 | a.i++; 11 | draft.a.push(a); 12 | expect(isDraft(a)).toBeTruthy(); 13 | }); 14 | // !!! check draft proxy array leakage 15 | expect(obj.a[0] === state.a.slice(-1)[0]).toBe(false); 16 | }); 17 | 18 | test('splice', () => { 19 | const obj = { 20 | a: Array.from({ length: 20 }, (_, i) => ({ i })), 21 | o: { b: { c: 1 } }, 22 | }; 23 | const state = create(obj, (draft) => { 24 | const [a] = draft.a.splice(0, 1)!; 25 | a.i++; 26 | draft.a.push(a); 27 | expect(isDraft(a)).toBeTruthy(); 28 | }); 29 | // !!! check draft proxy array leakage 30 | expect(obj.a[0] === state.a.slice(-1)[0]).toBe(false); 31 | }); 32 | 33 | test('shift with mark', () => { 34 | class Test { 35 | constructor(public i: number) {} 36 | } 37 | const obj = { 38 | a: Array.from({ length: 20 }, (_, i) => new Test(i)), 39 | o: { b: { c: 1 } }, 40 | }; 41 | const state = create( 42 | obj, 43 | (draft) => { 44 | const a = draft.a.shift()!; 45 | a.i++; 46 | draft.a.push(a); 47 | expect(isDraft(a)).toBeTruthy(); 48 | }, 49 | { 50 | mark: (value) => (value instanceof Test ? 'immutable' : undefined), 51 | } 52 | ); 53 | // !!! check draft proxy array leakage 54 | expect(obj.a[0] === state.a.slice(-1)[0]).toBe(false); 55 | }); 56 | 57 | test('splice with mark', () => { 58 | class Test { 59 | constructor(public i: number) {} 60 | } 61 | const obj = { 62 | a: Array.from({ length: 20 }, (_, i) => new Test(i)), 63 | o: { b: { c: 1 } }, 64 | }; 65 | const state = create( 66 | obj, 67 | (draft) => { 68 | const [a] = draft.a.splice(0, 1)!; 69 | a.i++; 70 | draft.a.push(a); 71 | expect(isDraft(a)).toBeTruthy(); 72 | }, 73 | { 74 | mark: (value) => (value instanceof Test ? 'immutable' : undefined), 75 | } 76 | ); 77 | // !!! check draft proxy array leakage 78 | expect(obj.a[0] === state.a.slice(-1)[0]).toBe(false); 79 | }); 80 | 81 | 82 | // test('shift with custom copy', () => { 83 | // const obj = { 84 | // a: Array.from({ length: 20 }, (_, i) => new Date(i)), 85 | // o: { b: { c: 1 } }, 86 | // }; 87 | // const state = create( 88 | // obj, 89 | // (draft) => { 90 | // const a = draft.a.shift()!; 91 | // a.setMilliseconds(42); 92 | // draft.a.push(a); 93 | // expect(isDraft(a)).toBeTruthy(); 94 | // }, 95 | // { 96 | // mark: (target) => { 97 | // if (target instanceof Date) return () => new Date(target.getTime()); 98 | // }, 99 | // } 100 | // ); 101 | // // !!! check draft proxy array leakage 102 | // expect(obj.a[0] === state.a.slice(-1)[0]).toBe(false); 103 | // }); 104 | 105 | // test('splice with custom copy', () => { 106 | // const obj = { 107 | // a: Array.from({ length: 20 }, (_, i) => new Date(i)), 108 | // o: { b: { c: 1 } }, 109 | // }; 110 | // const state = create( 111 | // obj, 112 | // (draft) => { 113 | // const [a] = draft.a.splice(0, 1)!; 114 | // a.setMilliseconds(42); 115 | // draft.a.push(a); 116 | // expect(isDraft(a)).toBeTruthy(); 117 | // }, 118 | // { 119 | // mark: (target) => { 120 | // if (target instanceof Date) return () => new Date(target.getTime()); 121 | // }, 122 | // } 123 | // ); 124 | // // !!! check draft proxy array leakage 125 | // expect(obj.a[0] === state.a.slice(-1)[0]).toBe(false); 126 | // }); 127 | -------------------------------------------------------------------------------- /test/benchmark/all.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import https from 'https'; 4 | import QuickChart from 'quickchart-js'; 5 | 6 | const resultPath = path.resolve(__dirname, `./results/result.json`); 7 | const raw = fs.readFileSync(resultPath); 8 | const result: Record = JSON.parse( 9 | String(raw) 10 | ); 11 | 12 | const data = Object.entries(result).sort( 13 | ([, { avg: avgA }], [, { avg: avgB }]) => avgA - avgB 14 | ); 15 | 16 | const name = path.basename(__filename).replace('.ts', ''); 17 | const chart = new QuickChart(); 18 | chart.setConfig({ 19 | type: 'horizontalBar', 20 | data: { 21 | labels: data.map(([name]) => name), 22 | datasets: [ 23 | { 24 | label: 'All benchmark items', 25 | backgroundColor: 'rgba(54, 162, 235, 0.5)', 26 | borderColor: 'rgb(54, 162, 235)', 27 | borderWidth: 1, 28 | data: data.map(([, { avg }]) => avg.toFixed(1)), 29 | }, 30 | ], 31 | }, 32 | options: { 33 | title: { 34 | display: true, 35 | text: 'Mutative vs Immer - All benchmark results by average multiplier', 36 | }, 37 | plugins: { 38 | datalabels: { 39 | anchor: 'right', 40 | align: 'right', 41 | color: '#666', 42 | font: { 43 | weight: 'normal', 44 | }, 45 | }, 46 | }, 47 | scales: { 48 | xAxes: [ 49 | { 50 | display: true, 51 | scaleLabel: { 52 | display: true, 53 | labelString: 'Multiplier', 54 | }, 55 | }, 56 | ], 57 | yAxes: [ 58 | { 59 | type: 'category', 60 | position: 'left', 61 | display: true, 62 | scaleLabel: { 63 | display: true, 64 | labelString: 'Benchmark Type', 65 | }, 66 | ticks: { 67 | reverse: true, 68 | }, 69 | }, 70 | ], 71 | }, 72 | }, 73 | }); 74 | 75 | const file = fs.createWriteStream( 76 | path.resolve(__dirname, `./results/${name}.jpg`) 77 | ); 78 | https.get(chart.getUrl(), (response) => { 79 | response.pipe(file); 80 | file.on('finish', () => { 81 | file.close(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/benchmark/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-loop-func */ 2 | import fs from 'fs'; 3 | import { spawn } from 'child_process'; 4 | import path from 'path'; 5 | 6 | const currentDir = __dirname; 7 | 8 | async function runTsFilesSequentially() { 9 | const files = fs.readdirSync(currentDir); 10 | const tsFiles = files.filter( 11 | (file) => 12 | path.extname(file) === '.ts' && 13 | path.basename(file) !== 'index.ts' && 14 | path.basename(file) !== 'all.ts' 15 | ); 16 | const all = files.find((file) => path.basename(file) === 'all.ts')!; 17 | for (const file of [...tsFiles, all]) { 18 | console.log(`Running: ${file}`); 19 | const env = { ...process.env, NODE_ENV: 'production' }; 20 | await new Promise((resolve, reject) => { 21 | const child = spawn(`ts-node`, [path.join(currentDir, file)], { 22 | stdio: 'inherit', 23 | env, 24 | }); 25 | 26 | child.on('close', (code) => { 27 | if (code === 0) { 28 | resolve(); 29 | } else { 30 | reject(new Error(`error code: ${code}`)); 31 | } 32 | }); 33 | }); 34 | } 35 | } 36 | 37 | runTsFilesSequentially(); 38 | -------------------------------------------------------------------------------- /test/benchmark/results/all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/all.jpg -------------------------------------------------------------------------------- /test/benchmark/results/array-batch-getter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/array-batch-getter.jpg -------------------------------------------------------------------------------- /test/benchmark/results/array-batch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/array-batch.jpg -------------------------------------------------------------------------------- /test/benchmark/results/array-single-push.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/array-single-push.jpg -------------------------------------------------------------------------------- /test/benchmark/results/array.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/array.jpg -------------------------------------------------------------------------------- /test/benchmark/results/class.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/class.jpg -------------------------------------------------------------------------------- /test/benchmark/results/map-batch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/map-batch.jpg -------------------------------------------------------------------------------- /test/benchmark/results/map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/map.jpg -------------------------------------------------------------------------------- /test/benchmark/results/object-batch-getter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/object-batch-getter.jpg -------------------------------------------------------------------------------- /test/benchmark/results/object-batch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/object-batch.jpg -------------------------------------------------------------------------------- /test/benchmark/results/object.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/object.jpg -------------------------------------------------------------------------------- /test/benchmark/results/result.json: -------------------------------------------------------------------------------- 1 | { 2 | "array-batch-getter": { 3 | "name": "array-batch-getter", 4 | "avg": 6.321058088498315 5 | }, 6 | "array-batch": { 7 | "name": "array-batch", 8 | "avg": 2.9293966353410776 9 | }, 10 | "array-single-push": { 11 | "name": "array-single-push", 12 | "avg": 25.196361547206763 13 | }, 14 | "array": { 15 | "name": "array", 16 | "avg": 82.92665595932255 17 | }, 18 | "class": { 19 | "name": "class", 20 | "avg": 2.9577714835340125 21 | }, 22 | "map-batch": { 23 | "name": "map-batch", 24 | "avg": 3.4827841500897168 25 | }, 26 | "map": { 27 | "name": "map", 28 | "avg": 5.7553703684454725 29 | }, 30 | "object-batch-getter": { 31 | "name": "object-batch-getter", 32 | "avg": 2.976148878415991 33 | }, 34 | "object-batch": { 35 | "name": "object-batch", 36 | "avg": 2.5238631589023566 37 | }, 38 | "object": { 39 | "name": "object", 40 | "avg": 2.601897556539274 41 | }, 42 | "set-batch": { 43 | "name": "set-batch", 44 | "avg": 2.5015325732958087 45 | }, 46 | "set": { 47 | "name": "set", 48 | "avg": 3.9251446240188272 49 | } 50 | } -------------------------------------------------------------------------------- /test/benchmark/results/set-batch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/set-batch.jpg -------------------------------------------------------------------------------- /test/benchmark/results/set.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/benchmark/results/set.jpg -------------------------------------------------------------------------------- /test/create.0.snap.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../index'; 2 | 3 | const baseState = { foo: { bar: 'str' }, arr: [] }; 4 | const state = create( 5 | baseState, 6 | (draft) => { 7 | draft.foo.bar = 'str2'; 8 | }, 9 | ); 10 | 11 | expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); 12 | expect(state).not.toBe(baseState); 13 | expect(state.foo).not.toBe(baseState.foo); 14 | expect(state.arr).toBe(baseState.arr); 15 | -------------------------------------------------------------------------------- /test/current.0.snap.ts: -------------------------------------------------------------------------------- 1 | import { create, current } from '../index'; 2 | 3 | const baseState = { foo: { bar: 'str' }, arr: [] }; 4 | const state = create( 5 | baseState, 6 | (draft) => { 7 | draft.foo.bar = 'str2'; 8 | expect(current(draft.foo)).toEqual({ bar: 'str2' }); 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /test/dev.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | /* eslint-disable no-param-reassign */ 3 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 4 | import { apply, create } from '../src'; 5 | 6 | test('custom shallow copy without checking in prod mode', () => { 7 | global.__DEV__ = false; 8 | const baseState = { foo: { bar: 'test' } }; 9 | 10 | expect(() => { 11 | create( 12 | baseState, 13 | (draft) => { 14 | draft.foo.bar = 'test2'; 15 | }, 16 | { 17 | enableAutoFreeze: true, 18 | // @ts-expect-error 19 | mark: (target) => { 20 | if (target === baseState.foo) { 21 | return () => ({ ...target }); 22 | } 23 | }, 24 | } 25 | ); 26 | }).not.toThrowError(); 27 | 28 | expect(() => { 29 | create( 30 | baseState, 31 | (draft) => { 32 | draft.foo.bar = 'test2'; 33 | }, 34 | { 35 | enablePatches: true, 36 | // @ts-expect-error 37 | mark: (target) => { 38 | if (target === baseState.foo) { 39 | return () => ({ ...target }); 40 | } 41 | }, 42 | } 43 | ); 44 | }).not.toThrowError(); 45 | }); 46 | 47 | test('custom shallow copy with checking in dev mode', () => { 48 | global.__DEV__ = true; 49 | const baseState = { foo: { bar: 'test' } }; 50 | 51 | expect(() => { 52 | create( 53 | baseState, 54 | (draft) => { 55 | draft.foo.bar = 'test2'; 56 | }, 57 | { 58 | enableAutoFreeze: true, 59 | // @ts-expect-error 60 | mark: (target) => { 61 | if (target === baseState.foo) { 62 | return () => ({ ...target }); 63 | } 64 | }, 65 | } 66 | ); 67 | }).toThrowErrorMatchingInlineSnapshot( 68 | `"You can't use mark and patches or auto freeze together."` 69 | ); 70 | 71 | expect(() => { 72 | create( 73 | baseState, 74 | (draft) => { 75 | draft.foo.bar = 'test2'; 76 | }, 77 | { 78 | enablePatches: true, 79 | // @ts-expect-error 80 | mark: (target) => { 81 | if (target === baseState.foo) { 82 | return () => ({ ...target }); 83 | } 84 | }, 85 | } 86 | ); 87 | }).toThrowErrorMatchingInlineSnapshot( 88 | `"You can't use mark and patches or auto freeze together."` 89 | ); 90 | }); 91 | 92 | test('check warn when apply patches with other options', () => { 93 | { 94 | global.__DEV__ = true; 95 | const baseState = { foo: { bar: 'test' } }; 96 | const warn = console.warn; 97 | jest.spyOn(console, 'warn').mockImplementation(() => {}); 98 | apply( 99 | baseState, 100 | [ 101 | { 102 | op: 'replace', 103 | path: ['foo', 'bar'], 104 | value: 'test2', 105 | }, 106 | ], 107 | { 108 | mutable: true, 109 | enableAutoFreeze: true, 110 | } 111 | ); 112 | expect(console.warn).toHaveBeenCalledWith( 113 | 'The "mutable" option is not allowed to be used with other options.' 114 | ); 115 | } 116 | { 117 | global.__DEV__ = true; 118 | const baseState = { foo: { bar: 'test' } }; 119 | const warn = console.warn; 120 | jest.spyOn(console, 'warn').mockImplementation(() => {}); 121 | apply( 122 | baseState, 123 | [ 124 | { 125 | op: 'replace', 126 | path: ['foo', 'bar'], 127 | value: 'test2', 128 | }, 129 | ], 130 | { 131 | mutable: true, 132 | enableAutoFreeze: true, 133 | mark: () => {}, 134 | } 135 | ); 136 | expect(console.warn).toHaveBeenCalledWith( 137 | 'The "mutable" option is not allowed to be used with other options.' 138 | ); 139 | } 140 | }); 141 | -------------------------------------------------------------------------------- /test/edge.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-self-assign */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | /* eslint-disable no-param-reassign */ 4 | import { create } from '../src'; 5 | 6 | test('works with interweaved Immer instances with strict mode and disable Freeze', () => { 7 | const base = {}; 8 | const result = create( 9 | base, 10 | (s1) => { 11 | const f = create( 12 | { s1 }, 13 | (s2) => { 14 | s2.s1 = s2.s1; 15 | }, 16 | { 17 | enableAutoFreeze: false, 18 | } 19 | ); 20 | return f; 21 | }, 22 | { 23 | enableAutoFreeze: false, 24 | strict: true, 25 | } 26 | ); 27 | // @ts-expect-error 28 | expect(result.s1).toBe(base); 29 | }); 30 | -------------------------------------------------------------------------------- /test/immer/__tests__/__snapshots__/curry.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`curry - proxy should check arguments 1`] = `"Invalid base state: create() only supports plain objects, arrays, Set, Map or using mark() to mark the state as immutable."`; 4 | 5 | exports[`curry - proxy should check arguments 2`] = `"patchListener is not a function"`; 6 | -------------------------------------------------------------------------------- /test/immer/__tests__/__snapshots__/frozen.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`auto freeze - proxy will freeze maps 1`] = `"Cannot modify frozen object"`; 4 | 5 | exports[`auto freeze - proxy will freeze maps 2`] = `"Cannot modify frozen object"`; 6 | 7 | exports[`auto freeze - proxy will freeze maps 3`] = `"Cannot modify frozen object"`; 8 | 9 | exports[`auto freeze - proxy will freeze sets 1`] = `"Cannot modify frozen object"`; 10 | 11 | exports[`auto freeze - proxy will freeze sets 2`] = `"Cannot modify frozen object"`; 12 | 13 | exports[`auto freeze - proxy will freeze sets 3`] = `"Cannot modify frozen object"`; 14 | -------------------------------------------------------------------------------- /test/immer/__tests__/__snapshots__/manual.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`manual - proxy cannot finishDraft twice 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; 4 | 5 | exports[`manual - proxy cannot modify after finish 1`] = `"Cannot perform 'set' on a proxy that has been revoked"`; 6 | 7 | exports[`manual - proxy should check arguments 1`] = `"Invalid base state: create() only supports plain objects, arrays, Set, Map or using mark() to mark the state as immutable."`; 8 | 9 | exports[`manual - proxy should check arguments 2`] = `"Invalid base state: create() only supports plain objects, arrays, Set, Map or using mark() to mark the state as immutable."`; 10 | 11 | exports[`manual - proxy should not finish drafts from produce 1`] = `"finishDraft is not defined"`; 12 | 13 | exports[`manual - proxy should not finish twice 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; 14 | 15 | exports[`manual - proxy should support patches drafts 1`] = ` 16 | [ 17 | [ 18 | [ 19 | { 20 | "op": "replace", 21 | "path": [ 22 | "a", 23 | ], 24 | "value": 2, 25 | }, 26 | { 27 | "op": "add", 28 | "path": [ 29 | "b", 30 | ], 31 | "value": 3, 32 | }, 33 | ], 34 | [ 35 | { 36 | "op": "replace", 37 | "path": [ 38 | "a", 39 | ], 40 | "value": 1, 41 | }, 42 | { 43 | "op": "remove", 44 | "path": [ 45 | "b", 46 | ], 47 | }, 48 | ], 49 | ], 50 | ] 51 | `; 52 | -------------------------------------------------------------------------------- /test/immer/__tests__/__snapshots__/patch.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`applyPatches throws when \`op\` is not "add", "replace", nor "remove" 1`] = `"Unsupported patch operation: copy."`; 4 | 5 | exports[`applyPatches throws when \`path\` cannot be resolved 1`] = `"Cannot apply patch at 'a/b'."`; 6 | 7 | exports[`applyPatches throws when \`path\` cannot be resolved 2`] = `"Cannot apply patch at 'a/b/c'."`; 8 | -------------------------------------------------------------------------------- /test/immer/__tests__/__snapshots__/readme.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Producers can update Maps 4`] = `"Cannot modify frozen object"`; 4 | -------------------------------------------------------------------------------- /test/immer/__tests__/curry.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { 3 | produce, 4 | produceWithPatches, 5 | enablePatches, 6 | } from '../src/immer'; 7 | 8 | enablePatches(); 9 | 10 | runTests('proxy', true); 11 | 12 | function runTests(name) { 13 | describe('curry - ' + name, () => { 14 | it('should check arguments', () => { 15 | expect(() => produce()).toThrowErrorMatchingSnapshot(); 16 | // !!! This is different from immer 17 | expect(() => produce({})).not.toThrowError(); 18 | // !!! This is different from immer 19 | expect(() => produce({}, {})).not.toThrowError(); 20 | expect(() => produce({}, () => {}, [])).toThrowErrorMatchingSnapshot(); 21 | }); 22 | 23 | it('should support currying', () => { 24 | const state = [{}, {}, {}]; 25 | const mapper = produce((item, index) => { 26 | item.index = index; 27 | }); 28 | 29 | expect(state.map(mapper)).not.toBe(state); 30 | expect(state.map(mapper)).toEqual([ 31 | { index: 0 }, 32 | { index: 1 }, 33 | { index: 2 }, 34 | ]); 35 | expect(state).toEqual([{}, {}, {}]); 36 | }); 37 | 38 | it('should support returning new states from curring', () => { 39 | const reducer = produce((item, index) => { 40 | if (!item) { 41 | return { hello: 'world' }; 42 | } 43 | item.index = index; 44 | }); 45 | 46 | expect(reducer(undefined, 3)).toEqual({ hello: 'world' }); 47 | expect(reducer({}, 3)).toEqual({ index: 3 }); 48 | }); 49 | 50 | it('should support passing an initial state as second argument', () => { 51 | // !!! This is different from immer 52 | const reducer = (state = { hello: 'world' }, index) => 53 | produce(state, (item) => { 54 | item.index = index; 55 | }); 56 | 57 | expect(reducer(undefined, 3)).toEqual({ hello: 'world', index: 3 }); 58 | expect(reducer({}, 3)).toEqual({ index: 3 }); 59 | expect(reducer()).toEqual({ hello: 'world', index: undefined }); 60 | }); 61 | 62 | it('can has fun with change detection', () => { 63 | const spread = produce(Object.assign); 64 | 65 | const base = { 66 | x: 1, 67 | y: 1, 68 | }; 69 | 70 | expect({ ...base }).not.toBe(base); 71 | expect(spread(base, {})).toBe(base); 72 | expect(spread(base, { y: 1 })).toBe(base); 73 | expect(spread(base, { ...base })).toBe(base); 74 | expect(spread(base, { ...base, y: 2 })).not.toBe(base); 75 | expect(spread(base, { ...base, y: 2 })).toEqual({ x: 1, y: 2 }); 76 | expect(spread(base, { z: 3 })).toEqual({ x: 1, y: 1, z: 3 }); 77 | expect(spread(base, { y: 1 })).toBe(base); 78 | }); 79 | }); 80 | 81 | it('support currying for produceWithPatches', () => { 82 | const increment = produceWithPatches((draft, delta) => { 83 | draft.x += delta; 84 | }); 85 | 86 | expect(increment({ x: 5 }, 2)).toEqual([ 87 | { x: 7 }, 88 | [ 89 | { 90 | op: 'replace', 91 | path: ['x'], 92 | value: 7, 93 | }, 94 | ], 95 | [ 96 | { 97 | op: 'replace', 98 | path: ['x'], 99 | value: 5, 100 | }, 101 | ], 102 | ]); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /test/immer/__tests__/immutable.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { assert, _, produce, Immutable, castImmutable } from '../src/immer'; 3 | 4 | test('types are ok', () => { 5 | // array in tuple 6 | { 7 | let val = _ as Immutable<[string[], 1]>; 8 | assert(val, _ as readonly [ReadonlyArray, 1]); 9 | } 10 | 11 | // tuple in array 12 | { 13 | let val = _ as Immutable<[string, 1][]>; 14 | assert(val, _ as ReadonlyArray); 15 | } 16 | 17 | // tuple in tuple 18 | { 19 | let val = _ as Immutable<[[string, 1], 1]>; 20 | assert(val, _ as readonly [readonly [string, 1], 1]); 21 | } 22 | 23 | // array in array 24 | { 25 | let val = _ as Immutable; 26 | assert(val, _ as ReadonlyArray>); 27 | } 28 | 29 | // tuple in object 30 | { 31 | let val = _ as Immutable<{ a: [string, 1] }>; 32 | assert(val, _ as { readonly a: readonly [string, 1] }); 33 | } 34 | 35 | // object in tuple 36 | { 37 | let val = _ as Immutable<[{ a: string }, 1]>; 38 | assert(val, _ as readonly [{ readonly a: string }, 1]); 39 | } 40 | 41 | // array in object 42 | { 43 | let val = _ as Immutable<{ a: string[] }>; 44 | assert(val, _ as { readonly a: ReadonlyArray }); 45 | } 46 | 47 | // object in array 48 | { 49 | let val = _ as Immutable>; 50 | assert(val, _ as ReadonlyArray<{ readonly a: string }>); 51 | } 52 | 53 | // object in object 54 | { 55 | let val = _ as Immutable<{ a: { b: string } }>; 56 | assert(val, _ as { readonly a: { readonly b: string } }); 57 | } 58 | 59 | // Map 60 | { 61 | let val = _ as Immutable>; 62 | assert(val, _ as ReadonlyMap); 63 | } 64 | 65 | // Already immutable Map 66 | { 67 | let val = _ as Immutable>; 68 | assert(val, _ as ReadonlyMap); 69 | } 70 | 71 | // object in Map 72 | { 73 | let val = _ as Immutable>; 74 | assert( 75 | val, 76 | _ as ReadonlyMap<{ readonly a: string }, { readonly b: string }> 77 | ); 78 | } 79 | 80 | // Set 81 | { 82 | let val = _ as Immutable>; 83 | assert(val, _ as ReadonlySet); 84 | } 85 | 86 | // Already immutable Set 87 | { 88 | let val = _ as Immutable>; 89 | assert(val, _ as ReadonlySet); 90 | } 91 | 92 | // object in Set 93 | { 94 | let val = _ as Immutable>; 95 | assert(val, _ as ReadonlySet<{ readonly a: string }>); 96 | } 97 | 98 | expect(true).toBe(true); 99 | }); 100 | 101 | test('#381 produce immutable state', () => { 102 | const someState = { 103 | todos: [ 104 | { 105 | done: false, 106 | }, 107 | ], 108 | }; 109 | 110 | const immutable = castImmutable(produce(someState, (_draft) => {})); 111 | assert( 112 | immutable, 113 | _ as { readonly todos: ReadonlyArray<{ readonly done: boolean }> } 114 | ); 115 | }); 116 | 117 | test('castImmutable preserves a value', () => { 118 | const x = {}; 119 | expect(castImmutable(x)).toBe(x); 120 | }); 121 | -------------------------------------------------------------------------------- /test/immer/__tests__/isDraftable.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { isDraftable } from '../src/immer'; 3 | 4 | test("non-plain object with undefined constructor doesn't error", () => { 5 | const obj = Object.create(Object.create(null)); 6 | expect(isDraftable(obj)).toBe(false); 7 | }); 8 | -------------------------------------------------------------------------------- /test/immer/__tests__/not-strict-copy.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { produce, setUseStrictShallowCopy } from '../src/immer'; 3 | 4 | describe('setUseStrictShallowCopy(true)', () => { 5 | test('keep descriptors', () => { 6 | setUseStrictShallowCopy(true); 7 | 8 | const base: Record = {}; 9 | Object.defineProperty(base, 'foo', { 10 | value: 'foo', 11 | writable: false, 12 | configurable: false, 13 | }); 14 | const copy = produce(base, (draft: any) => { 15 | draft.bar = 'bar'; 16 | }); 17 | // immer does not support 18 | // expect(copy.foo).toBe('bar'); 19 | expect(Object.getOwnPropertyDescriptor(copy, 'foo')).toStrictEqual( 20 | Object.getOwnPropertyDescriptor(base, 'foo') 21 | ); 22 | }); 23 | }); 24 | 25 | describe('setUseStrictShallowCopy(false)', () => { 26 | test('ignore descriptors', () => { 27 | setUseStrictShallowCopy(false); 28 | 29 | const base: Record = {}; 30 | Object.defineProperty(base, 'foo', { 31 | value: 'foo', 32 | writable: false, 33 | configurable: false, 34 | }); 35 | const copy = produce(base, (draft: any) => { 36 | draft.bar = 'bar'; 37 | }); 38 | expect(Object.getOwnPropertyDescriptor(copy, 'foo')).toBeUndefined(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/immer/__tests__/null.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { produce } from '../src/immer'; 3 | 4 | describe('null functionality', () => { 5 | const baseState = null; 6 | 7 | it('should return the original without modifications', () => { 8 | const nextState = produce(baseState, () => {}); 9 | expect(nextState).toBe(baseState); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/immer/__tests__/original.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { produce, original } from '../src/immer'; 3 | 4 | const isProd = process.env.NODE_ENV === 'production'; 5 | 6 | describe('original', () => { 7 | const baseState = { 8 | a: [], 9 | b: {}, 10 | }; 11 | 12 | it('should return the original from the draft', () => { 13 | produce(baseState, (draftState) => { 14 | expect(original(draftState)).toBe(baseState); 15 | expect(original(draftState.a)).toBe(baseState.a); 16 | expect(original(draftState.b)).toBe(baseState.b); 17 | }); 18 | }); 19 | 20 | it('should return the original from the proxy', () => { 21 | produce(baseState, (draftState) => { 22 | expect(original(draftState)).toBe(baseState); 23 | expect(original(draftState.a)).toBe(baseState.a); 24 | expect(original(draftState.b)).toBe(baseState.b); 25 | }); 26 | }); 27 | 28 | it('should throw undefined for new values on the draft', () => { 29 | produce(baseState, (draftState) => { 30 | draftState.c = {}; 31 | draftState.d = 3; 32 | expect(() => original(draftState.c)).toThrowError( 33 | isProd 34 | ? `[Immer] minified error nr: 15. Full error at: https://bit.ly/3cXEKWf` 35 | : `original() is only used for a draft, parameter: [object Object]` 36 | ); 37 | expect(() => original(draftState.d)).toThrowError( 38 | isProd 39 | ? `[Immer] minified error nr: 15. Full error at: https://bit.ly/3cXEKWf` 40 | : `original() is only used for a draft, parameter: 3` 41 | ); 42 | }); 43 | }); 44 | 45 | it('should return undefined for an object that is not proxied', () => { 46 | expect(() => original({})).toThrowError( 47 | isProd 48 | ? `[Immer] minified error nr: 15. Full error at: https://bit.ly/3cXEKWf` 49 | : `original() is only used for a draft, parameter: [object Object]` 50 | ); 51 | expect(() => original(3)).toThrowError( 52 | isProd 53 | ? `[Immer] minified error nr: 15. Full error at: https://bit.ly/3cXEKWf` 54 | : `original() is only used for a draft, parameter: 3` 55 | ); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/immer/__tests__/plugins.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { produce, produceWithPatches, applyPatches } from '../src/immer'; 3 | 4 | test('error when using Maps', () => { 5 | // !!! This is different from immer 6 | expect(() => { 7 | produce(new Map(), function () {}); 8 | }).not.toThrowError(); 9 | }); 10 | 11 | test('error when using patches - 1', () => { 12 | // !!! This is different from immer 13 | expect(() => { 14 | produce( 15 | {}, 16 | function () {}, 17 | function () {} 18 | ); 19 | }).not.toThrowError(); 20 | }); 21 | 22 | test('error when using patches - 2', () => { 23 | // !!! This is different from immer 24 | expect(() => { 25 | produceWithPatches({}, function () {}); 26 | }).not.toThrowError(); 27 | }); 28 | 29 | test('error when using patches - 3', () => { 30 | // !!! This is different from immer 31 | expect(() => { 32 | applyPatches({}, []); 33 | }).not.toThrowError(); 34 | }); 35 | -------------------------------------------------------------------------------- /test/immer/__tests__/test-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "5a5e59f1bc9132a90101f521", 3 | "index": 0, 4 | "guid": "8dc96fdb-0526-4bc4-a0ef-fb08e6c06e77", 5 | "isActive": true, 6 | "balance": "$3,359.66", 7 | "picture": "http://placehold.it/32x32", 8 | "age": 40, 9 | "eyeColor": "blue", 10 | "name": "Samantha Richardson", 11 | "gender": "female", 12 | "company": "COWTOWN", 13 | "email": "samantharichardson@cowtown.com", 14 | "phone": "+1 (847) 556-2336", 15 | "address": "419 Bergen Street, Emison, Nebraska, 554", 16 | "about": "Enim magna ut do esse voluptate et commodo anim laborum proident aute. Nulla non sit in incididunt minim velit. Laboris et tempor enim cillum ex adipisicing excepteur. Dolore consectetur eiusmod eu ad eu. Veniam qui sunt est reprehenderit et ut occaecat eiusmod pariatur aliquip officia ipsum.\r\n", 17 | "registered": "2017-07-09T02:30:48 -02:00", 18 | "latitude": -38.375287, 19 | "longitude": -27.090184, 20 | "tags": ["ex", "cillum", "cillum", "Lorem", "fugiat", "dolore", "nulla", "anim", "aliqua", "sint"], 21 | "friends": [ 22 | { 23 | "id": 0, 24 | "name": "Adrian Dunlap" 25 | }, 26 | { 27 | "id": 1, 28 | "name": "Salinas Copeland" 29 | }, 30 | { 31 | "id": 2, 32 | "name": "Doreen Emerson" 33 | }, 34 | { 35 | "id": 3, 36 | "name": "Willis Long" 37 | }, 38 | { 39 | "id": 4, 40 | "name": "Josefina Garza" 41 | } 42 | ], 43 | "greeting": "Hello, Samantha Richardson! You have 8 unread messages.", 44 | "favoriteFruit": "banana" 45 | } 46 | -------------------------------------------------------------------------------- /test/immer/__tests__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["*", "../dist"], 3 | "compilerOptions": { 4 | "lib": ["es2015"], 5 | "strict": true, 6 | "noUnusedLocals": false, 7 | "target": "ES5" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/jsdoc.test.ts: -------------------------------------------------------------------------------- 1 | import { jsdocTests } from 'jsdoc-tests'; 2 | 3 | describe('jsdoc', () => { 4 | test('create()', () => { 5 | jsdocTests('../src/create.ts', __dirname); 6 | }); 7 | test('apply()', () => { 8 | jsdocTests('../src/apply.ts', __dirname); 9 | }); 10 | test('current()', () => { 11 | jsdocTests('../src/current.ts', __dirname); 12 | }); 13 | test('original()', () => { 14 | jsdocTests('../src/original.ts', __dirname); 15 | }); 16 | test('unsafe()', () => { 17 | jsdocTests('../src/unsafe.ts', __dirname); 18 | }); 19 | test('rawReturn()', () => { 20 | jsdocTests('../src/rawReturn.ts', __dirname); 21 | }); 22 | test('makeCreator()', () => { 23 | jsdocTests('../src/makeCreator.ts', __dirname); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/makeCreator.0.snap.ts: -------------------------------------------------------------------------------- 1 | import { makeCreator } from '../index'; 2 | 3 | const baseState = { foo: { bar: 'str' }, arr: [] }; 4 | const create = makeCreator({ enableAutoFreeze: true }); 5 | const state = create( 6 | baseState, 7 | (draft) => { 8 | draft.foo.bar = 'str2'; 9 | }, 10 | ); 11 | 12 | expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); 13 | expect(state).not.toBe(baseState); 14 | expect(state.foo).not.toBe(baseState.foo); 15 | expect(state.arr).toBe(baseState.arr); 16 | expect(Object.isFrozen(state)).toBeTruthy(); 17 | -------------------------------------------------------------------------------- /test/makeCreator.test.ts: -------------------------------------------------------------------------------- 1 | import { makeCreator } from '../src'; 2 | 3 | test('check makeCreator options', () => { 4 | [ 5 | [], 6 | null, 7 | 0, 8 | 1, 9 | '', 10 | 'str', 11 | true, 12 | false, 13 | new Map(), 14 | new Set(), 15 | Symbol(''), 16 | function () { 17 | // 18 | }, 19 | ].forEach((arg) => { 20 | expect(() => { 21 | // @ts-expect-error 22 | makeCreator(arg); 23 | }).toThrowError(`'options' should be an object.`); 24 | }); 25 | }); 26 | 27 | test('check enableAutoFreeze', () => { 28 | const create = makeCreator({ 29 | enableAutoFreeze: true, 30 | }); 31 | const baseState = { foo: { bar: 'str' }, arr: [] }; 32 | const state = create(baseState, (draft) => { 33 | // 34 | }); 35 | expect(Object.isFrozen(state)).toBeTruthy(); 36 | }); 37 | 38 | test('check enableAutoFreeze - override', () => { 39 | const create = makeCreator({ 40 | enableAutoFreeze: true, 41 | }); 42 | const baseState = { foo: { bar: 'str' }, arr: [] }; 43 | const state = create( 44 | baseState, 45 | (draft) => { 46 | // 47 | }, 48 | { 49 | enableAutoFreeze: false, 50 | } 51 | ); 52 | expect(Object.isFrozen(state)).not.toBeTruthy(); 53 | }); 54 | 55 | test('check enablePatches', () => { 56 | const create = makeCreator({ 57 | enablePatches: true, 58 | }); 59 | const baseState = { foo: { bar: 'str' }, arr: [] }; 60 | const state = create(baseState, (draft) => { 61 | // 62 | }); 63 | expect(Array.isArray(state)).toBeTruthy(); 64 | }); 65 | 66 | test('check enablePatches - override', () => { 67 | const create = makeCreator({ 68 | enablePatches: true, 69 | }); 70 | const baseState = { foo: { bar: 'str' }, arr: [] }; 71 | const state = create( 72 | baseState, 73 | (draft) => { 74 | // 75 | }, 76 | { 77 | enablePatches: false, 78 | } 79 | ); 80 | expect(Array.isArray(state)).not.toBeTruthy(); 81 | }); 82 | 83 | test('check strict', () => { 84 | const create = makeCreator({ 85 | strict: true, 86 | }); 87 | class C { 88 | x = 0; 89 | } 90 | const baseState = { foo: { bar: 'str' }, arr: [], c: new C() }; 91 | expect(() => { 92 | const state = create(baseState, (draft) => { 93 | draft.c.x = 1; 94 | }); 95 | }).toThrowError(); 96 | }); 97 | 98 | test('check strict - override', () => { 99 | const create = makeCreator({ 100 | strict: true, 101 | }); 102 | class C { 103 | x = 0; 104 | } 105 | const baseState = { foo: { bar: 'str' }, arr: [], c: new C() }; 106 | expect(() => { 107 | const state = create( 108 | baseState, 109 | (draft) => { 110 | draft.c.x = 1; 111 | }, 112 | { strict: false } 113 | ); 114 | }).not.toThrowError(); 115 | }); 116 | 117 | test('check mark', () => { 118 | class C { 119 | x = 0; 120 | } 121 | const create = makeCreator({ 122 | mark: (target, { immutable }) => { 123 | if (target instanceof C) { 124 | return immutable; 125 | } 126 | }, 127 | }); 128 | const baseState = { foo: { bar: 'str' }, arr: [], c: new C() }; 129 | const state = create(baseState, (draft) => { 130 | draft.c.x = 1; 131 | }); 132 | expect(state.c.x).toBe(1); 133 | expect(state.c).not.toBe(baseState.c); 134 | }); 135 | 136 | test('check mark - override', () => { 137 | class C { 138 | x = 0; 139 | } 140 | const create = makeCreator({ 141 | mark: (target, { immutable }) => { 142 | if (target instanceof C) { 143 | return immutable; 144 | } 145 | }, 146 | }); 147 | const baseState = { foo: { bar: 'str' }, arr: [], c: new C() }; 148 | const state = create( 149 | baseState, 150 | (draft) => { 151 | draft.c.x = 1; 152 | }, 153 | { 154 | mark: undefined, 155 | } 156 | ); 157 | expect(state.c.x).toBe(1); 158 | expect(state.c).toBe(baseState.c); 159 | }); 160 | -------------------------------------------------------------------------------- /test/original.0.snap.ts: -------------------------------------------------------------------------------- 1 | import { create, original } from '../index'; 2 | 3 | const baseState = { foo: { bar: 'str' }, arr: [] }; 4 | const state = create( 5 | baseState, 6 | (draft) => { 7 | draft.foo.bar = 'str2'; 8 | expect(original(draft.foo)).toEqual({ bar: 'str' }); 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /test/original.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import { create, original, isDraft } from '../src'; 3 | 4 | describe('original', () => { 5 | test('should return the original value', () => { 6 | interface Item { 7 | foo: string; 8 | bar?: { foobar: string }; 9 | } 10 | const value = create( 11 | { 12 | arr: [{ foo: 'bar' } as Item], 13 | set: new Set([{ foo: 'bar' }]), 14 | map: new Map([['foo', { foo: 'bar' }]]), 15 | obj: { foo: 'bar' } as Item, 16 | }, 17 | (draft) => { 18 | draft.arr[0].foo = 'baz'; 19 | expect(isDraft(draft.arr[0])).toBe(true); 20 | expect(original(draft.arr[0])).toEqual({ foo: 'bar' }); 21 | expect(() => original(draft.arr[0].bar!)).toThrowError(); 22 | // !new props 23 | draft.arr[0].bar = { foobar: 'str' }; 24 | draft.arr[0].bar.foobar = 'baz'; 25 | expect(isDraft(draft.arr[0].bar)).toBe(false); 26 | expect(() => original(draft.arr[0].bar)).toThrowError(); 27 | 28 | Array.from(draft.set.values())[0].foo = 'baz'; 29 | expect(isDraft(Array.from(draft.set.values())[0])).toBe(true); 30 | expect(original(Array.from(draft.set.values())[0])).toEqual({ 31 | foo: 'bar', 32 | }); 33 | // !new props 34 | Array.from(draft.set.values())[0].bar = { foobar: 'str' }; 35 | Array.from(draft.set.values())[0].bar!.foobar = 'baz'; 36 | expect(isDraft(Array.from(draft.set.values())[0].bar)).toBe(false); 37 | expect(() => 38 | original(Array.from(draft.set.values())[0].bar) 39 | ).toThrowError(); 40 | 41 | draft.map.get('foo')!.foo = 'baz'; 42 | expect(isDraft(draft.map.get('foo'))).toBe(true); 43 | expect(original(draft.map.get('foo'))).toEqual({ foo: 'bar' }); 44 | // !new props 45 | draft.map.get('foo')!.bar = { foobar: 'str' }; 46 | draft.map.get('foo')!.bar!.foobar = 'baz'; 47 | expect(isDraft(draft.map.get('foo')!.bar)).toBe(false); 48 | expect(() => original(draft.map.get('foo')!.bar)).toThrowError(); 49 | 50 | draft.obj.foo = 'baz'; 51 | expect(isDraft(draft.obj)).toBe(true); 52 | expect(original(draft.obj)).toEqual({ foo: 'bar' }); 53 | // !new props 54 | draft.obj.bar = { foobar: 'str' }; 55 | draft.obj.bar!.foobar = 'baz'; 56 | expect(isDraft(draft.obj.bar)).toBe(false); 57 | expect(() => original(draft.obj.bar)).toThrowError(); 58 | } 59 | ); 60 | }); 61 | 62 | test('should return undefined for an object that is not proxied', () => { 63 | expect(() => original({})).toThrowError( 64 | `original() is only used for a draft, parameter: [object Object]` 65 | ); 66 | expect(() => original(3)).toThrowError( 67 | `original() is only used for a draft, parameter: 3` 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/performance/new-set-api.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/test/performance/new-set-api.jpg -------------------------------------------------------------------------------- /test/performance/read-draft/mockPhysics.ts: -------------------------------------------------------------------------------- 1 | // our pretend physics engine 2 | interface Vec2 { 3 | x: number; 4 | y: number; 5 | } 6 | 7 | interface Ball { 8 | position: Vec2; 9 | velocity: Vec2; 10 | radius: number; 11 | mass: number; 12 | data?: any; 13 | } 14 | 15 | interface Table { 16 | balls: Ball[]; 17 | } 18 | 19 | function createVector(x: number, y: number): Vec2 { 20 | return { x, y }; 21 | } 22 | 23 | function createBall(): Ball { 24 | return { 25 | position: createVector(Math.random() * 100, Math.random() * 100), 26 | velocity: createVector(-1 + Math.random() * 2, -1 + Math.random() * 2), 27 | radius: 10, 28 | mass: 10, 29 | data: null, 30 | }; 31 | } 32 | 33 | export function createTable(ballCount: number): Table { 34 | const table: Table = { 35 | balls: [], 36 | }; 37 | for (let i = 0; i < ballCount; i++) { 38 | table.balls.push(createBall()); 39 | } 40 | 41 | return table; 42 | } 43 | 44 | export function updateTable(table: Table, readsOnly: boolean): Table { 45 | // let i = 0; 46 | for (const ball of table.balls) { 47 | // i++; 48 | if (readsOnly) { 49 | // @ts-ignore 50 | const result1 = ball.velocity.x + ball.velocity.y; 51 | // @ts-ignore 52 | const result2 = ball.position.x + ball.position.y; 53 | } else { 54 | ball.position.x += ball.velocity.x; 55 | ball.position.y += ball.velocity.y; 56 | ball.velocity.x *= 0.999; 57 | ball.velocity.x *= 0.999; 58 | } 59 | } 60 | 61 | // @ts-ignore 62 | for (const ballA of table.balls) { 63 | // @ts-ignore 64 | for (const ballB of table.balls) { 65 | // i++; 66 | if (ballA !== ballB) { 67 | if (readsOnly) { 68 | // @ts-ignore 69 | const result1 = ballA.velocity.x + ballB.velocity.y; 70 | // @ts-ignore 71 | const result2 = ballA.position.x + ballB.position.y; 72 | } else { 73 | // perform some collision or other 74 | const dx = ballB.position.x - ballA.position.y; 75 | const dy = ballB.position.y - ballA.position.y; 76 | const len = Math.sqrt(dx * dx + dy * dy); 77 | if (len < ballB.radius + ballA.radius) { 78 | // collision! 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | // console.log('i', i); 86 | 87 | return table; 88 | } 89 | -------------------------------------------------------------------------------- /test/performance/sample.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { produce } from 'immer'; 3 | import { create } from '../..'; 4 | 5 | const objKeySize = 5000; 6 | const objLength = objKeySize - 1; 7 | 8 | const arrSize = 50000; 9 | const arrLength = arrSize - 1; 10 | // https://github.com/immerjs/immer/issues/867 by default 11 | console.time(`create - object: ${objKeySize}`); 12 | const dataSource = Object.fromEntries( 13 | [...Array(objKeySize).keys()].map((key) => [ 14 | key, 15 | { key, data: { value: key } }, 16 | ]) 17 | ); 18 | console.timeEnd(`create - object: ${objKeySize}`); 19 | 20 | console.time(`reducer - object: ${objKeySize}`); 21 | { 22 | const _dataSource = { 23 | ...dataSource, 24 | [objLength]: { key: objLength, data: { value: objLength } }, 25 | }; 26 | } 27 | console.timeEnd(`reducer - object: ${objKeySize}`); 28 | 29 | console.time(`mutative - object: update: ${objKeySize}`); 30 | create(dataSource, (draft) => { 31 | draft[objLength].data.value = objLength; 32 | }); 33 | 34 | console.timeEnd(`mutative - object: update: ${objKeySize}`); 35 | 36 | const a = produce(dataSource, () => {}); 37 | console.time(`immer - object: update: ${objKeySize}`); 38 | produce(a, (draft) => { 39 | draft[objLength].data.value = objLength; 40 | }); 41 | 42 | console.timeEnd(`immer - object: update: ${objKeySize}`); 43 | 44 | console.log('==========================='); 45 | 46 | console.time(`create - array: ${arrSize}`); 47 | const dataSource1 = [...Array(arrSize).keys()].map((key) => [ 48 | key, 49 | { key, data: { value: key } }, 50 | ]); 51 | console.timeEnd(`create - array: ${arrSize}`); 52 | 53 | console.time(`reducer - array: ${arrSize}`); 54 | { 55 | const _dataSource = [ 56 | ...dataSource1.slice(0, arrLength), 57 | [arrLength, { key: arrLength, data: { value: arrLength } }], 58 | ]; 59 | } 60 | console.timeEnd(`reducer - array: ${arrSize}`); 61 | 62 | console.time(`mutative - array: update: ${arrSize}`); 63 | create(dataSource1, (draft) => { 64 | // @ts-ignore 65 | draft[arrLength][1].data.value = arrLength; 66 | }); 67 | 68 | console.timeEnd(`mutative - array: update: ${arrSize}`); 69 | 70 | const f = produce(dataSource1, () => {}); 71 | console.time(`immer - array: update: ${arrSize}`); 72 | produce(f, (draft) => { 73 | // @ts-ignore 74 | draft[arrLength][1].data.value = arrLength; 75 | }); 76 | 77 | console.timeEnd(`immer - array: update: ${arrSize}`); 78 | -------------------------------------------------------------------------------- /test/rawReturn.0.snap.ts: -------------------------------------------------------------------------------- 1 | import { create, rawReturn } from '../index'; 2 | 3 | const baseState = { foo: { bar: 'str' }, arr: [] }; 4 | const state = create( 5 | baseState, 6 | (draft) => { 7 | return rawReturn(baseState); 8 | }, 9 | ); 10 | expect(state).toBe(baseState); 11 | -------------------------------------------------------------------------------- /test/unsafe.0.snap.ts: -------------------------------------------------------------------------------- 1 | import { create, unsafe } from '../index'; 2 | 3 | class Foobar { 4 | bar = 1; 5 | } 6 | 7 | const baseState = { foobar: new Foobar() }; 8 | const state = create( 9 | baseState, 10 | (draft) => { 11 | unsafe(() => { 12 | draft.foobar.bar = 2; 13 | }); 14 | }, 15 | { 16 | strict: true, 17 | } 18 | ); 19 | 20 | expect(state).toBe(baseState); 21 | expect(state.foobar).toBe(baseState.foobar); 22 | expect(state.foobar.bar).toBe(2); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types", "global.d.ts"], 4 | "compilerOptions": { 5 | "target": "ES2015", 6 | "module": "esnext", 7 | "lib": ["dom", "esnext"], 8 | "importHelpers": true, 9 | // output .d.ts declaration files for consumers 10 | "declaration": true, 11 | // output .js.map sourcemap files for consumers 12 | "sourceMap": true, 13 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 14 | "rootDir": "./src", 15 | // stricter type-checking for stronger correctness. Recommended by TS 16 | "strict": true, 17 | // linter checks for common issues 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | // use Node's module resolution algorithm, instead of the legacy TS one 24 | "moduleResolution": "node", 25 | // transpile JSX to React.createElement 26 | "jsx": "react", 27 | // interop between ESM and CJS modules. Recommended by TS 28 | "esModuleInterop": true, 29 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 30 | "skipLibCheck": true, 31 | // error out if import and file system have a casing mismatch. Recommended by TS 32 | "forceConsistentCasingInFileNames": true, 33 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 34 | "noEmit": true, 35 | "downlevelIteration": true 36 | }, 37 | "ts-node": { 38 | "compilerOptions": { 39 | "module": "CommonJS" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 3](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /website/blog/authors.yml: -------------------------------------------------------------------------------- 1 | unadlib: 2 | name: Michael Lin 3 | title: Author of Mutative 4 | url: https://unadlib.github.io 5 | image_url: https://github.com/unadlib.png 6 | -------------------------------------------------------------------------------- /website/blog/releases/1.0/img/benchmark-array.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/blog/releases/1.0/img/benchmark-array.jpg -------------------------------------------------------------------------------- /website/blog/releases/1.0/img/benchmark-object.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/blog/releases/1.0/img/benchmark-object.jpg -------------------------------------------------------------------------------- /website/blog/releases/1.0/img/benchmark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/blog/releases/1.0/img/benchmark.jpg -------------------------------------------------------------------------------- /website/blog/releases/1.0/img/social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/blog/releases/1.0/img/social-card.jpg -------------------------------------------------------------------------------- /website/docs/advanced-guides/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Advanced Guides", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /website/docs/advanced-guides/auto-freeze.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Auto Freeze 6 | 7 | Enable autoFreeze, and return frozen state, and enable circular reference checking only in development mode. 8 | 9 | ## Enable Auto Freeze 10 | 11 | The `enableAutoFreeze` option is likely a feature that controls the immutability of objects in the state. When enabled, this option would automatically freeze state objects to prevent them from being modified. This is a common practice in immutable state management, ensuring that state objects are not changed inadvertently, which can lead to unpredictable behaviors in applications. 12 | 13 | Key aspects of the `enableAutoFreeze` option include: 14 | 15 | - **Immutability Enforcement**: The primary purpose of `enableAutoFreeze` is to enforce immutability in the state objects. By freezing the objects, it ensures that they cannot be modified once they are created or updated. 16 | 17 | - **Debugging Aid**: This option can be particularly helpful in a development environment where detecting unintended mutations can be crucial. By freezing state objects, any attempts to modify them would result in an error, helping developers identify and fix bugs related to state mutations. 18 | 19 | - **Optional Use**: As an option, `enableAutoFreeze` provides flexibility. Developers can choose to enable it in development environments for added safety and disable it in production for improved performance, depending on their requirements. 20 | 21 | - **Compatibility with State Management Practices**: This feature aligns with the principles of immutable state management, a practice widely adopted in modern application development for its benefits in predictability, simplicity, and ease of debugging. 22 | 23 | In summary, the `enableAutoFreeze` option is a useful feature for developers who want to enforce immutability in their application's state. It provides both a safeguard against unintended mutations and a tool for maintaining clean and predictable state management. 24 | 25 | :::tip 26 | In order to the security and protection of the updated data in development mode, and the performance in production mode, we recommend that you `enable` auto freeze in development and `disable` it in production. 27 | ::: 28 | -------------------------------------------------------------------------------- /website/docs/advanced-guides/currying.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Currying 6 | 7 | Mutative allows for the creation of more modular and reusable state manipulation functions. This is achieved through a technique called currying, which transforms a function that takes multiple arguments into a sequence of functions. 8 | 9 | ## Create a docs version 10 | 11 | Currying in Mutative refers to the technique of transforming a function that takes multiple arguments into a sequence of functions, each with a single argument. This approach is particularly useful in the context of state management, where it allows for greater flexibility and modularity in handling state updates. 12 | 13 | Key aspects of currying in Mutative include: 14 | 15 | - **Modular State Management**: By using currying, developers can create more granular and composable functions to handle specific aspects of state updates. This enhances code readability and maintainability. 16 | 17 | - **Flexibility in Function Invocation**: Currying allows for functions to be called with fewer arguments than they were defined to accept. This is useful in scenarios where state manipulation functions might need to be partially applied or reused across different contexts. 18 | 19 | - **Enhanced Code Reusability**: With currying, the same base function can be adapted for different use cases by partially applying different arguments. This promotes code reusability and cleaner code architecture. 20 | 21 | - **Integration with State Drafts**: Currying works well with Mutative's draft-based state management system, allowing developers to apply incremental changes to the state in a controlled and predictable manner. 22 | 23 | Currying in Mutative offers a powerful way to handle state updates with more precision and clarity. It aligns with the principles of functional programming, bringing added benefits like modularity and reusability to state management. This makes Mutative a versatile tool for developers working on complex applications where efficient and maintainable state manipulation is key. 24 | 25 | ## Base Currying 26 | 27 | ### create draft 28 | 29 | Support set options such as `const [draft, finalize] = create(baseState, { enableAutoFreeze: true })`. 30 | 31 | ```ts 32 | const [draft, finalize] = create(baseState); 33 | draft.foobar.bar = 'baz'; 34 | const state = finalize(); 35 | ``` 36 | ### create producer 37 | 38 | Also support set options such as `const produce = create((draft) => {}, { enableAutoFreeze: true })`. 39 | 40 | ```ts 41 | const produce = create((draft) => { 42 | draft.foobar.bar = 'baz'; 43 | }); 44 | const state = produce(baseState); 45 | ``` 46 | 47 | ## Enable Patches 48 | 49 | ```ts 50 | const baseState = { 51 | foobar: { foo: 'str', bar: 'str' }, 52 | baz: { text: 'str' }, 53 | }; 54 | 55 | const [draft, finalize] = create(baseState, { enablePatches: true }); 56 | draft.foobar.bar = 'baz'; 57 | const [state, patches, inversePatches] = finalize(); 58 | 59 | expect(state).toEqual(expectedResult); 60 | expect(state).not.toBe(baseState); 61 | expect(state.foobar).not.toBe(baseState.foobar); 62 | expect(state.baz).toBe(baseState.baz); 63 | expect(patches).toEqual([ 64 | { 65 | op: 'replace', 66 | path: ['foobar', 'bar'], 67 | value: 'baz', 68 | }, 69 | ]); 70 | expect(inversePatches).toEqual([ 71 | { 72 | op: 'replace', 73 | path: ['foobar', 'bar'], 74 | value: 'str', 75 | }, 76 | ]); 77 | ``` 78 | 79 | :::tip 80 | You can use the custom `create()` function [option](/docs/api-reference/create#createstate-fn-options---options) to implement your own currying function. 81 | ::: 82 | -------------------------------------------------------------------------------- /website/docs/advanced-guides/mark.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Marking data structure 6 | 7 | Mark options provides additional controls or flags to determine `immutability` or `mutability` of data nodes in drafts function execution process, or `customize shallow copy` for creating drafts. 8 | 9 | ## Customize mark 10 | 11 | Mark options are a set of configurations that dictate how the `immutable` or `mutable` handles state during the drafting and updating process. These options offer developers more control and flexibility in managing state changes, allowing for customized behavior based on the specific requirements of the application. 12 | 13 | Key aspects of mark options include: 14 | 15 | - **Customizable State Management**: Mark options enable developers to tailor the state management process. This includes defining how state changes are tracked, how drafts are created about shallow copy, and how changes are finalized. 16 | 17 | - **Enhanced Flexibility**: Mark options support array marking, which allows for more granular and composable control over state updates. This is particularly useful in scenarios where different parts of the state might require different handling. 18 | 19 | - **Non-invasive marking**: Mark options are designed to be non-invasive, meaning that they do not affect the state itself. This ensures that the state remains unaltered and can be safely accessed at any time. 20 | 21 | ```ts 22 | class FooBar { 23 | constructor(public bar?: string) {} 24 | } 25 | 26 | const bar = { 27 | bar: 'str', 28 | }; 29 | 30 | const baseState = { 31 | bar, 32 | date: new Date(0), 33 | fooBar: new FooBar('str'), 34 | }; 35 | 36 | const nextState = create( 37 | baseState, 38 | (draft) => { 39 | draft.date.setFullYear(2000); 40 | draft.bar.bar = 'new str'; 41 | draft.fooBar.bar = 'new str'; 42 | }, 43 | { 44 | mark: (target, { mutable, immutable }) => { 45 | if (target instanceof Date) return () => new Date(target.getTime()); 46 | if (target === bar) return mutable; 47 | if (target instanceof FooBar) return immutable; 48 | }, 49 | } 50 | ); 51 | expect(nextState).not.toBe(baseState); 52 | expect(nextState.bar).toBe(baseState.bar); 53 | expect(nextState.date).not.toBe(baseState.date); 54 | expect(nextState.fooBar).not.toBe(baseState.fooBar); 55 | ``` 56 | 57 | :::tip 58 | Set a mark to determine if the value is mutable or if an instance is an immutable, and it can also return a shallow copy function(AutoFreeze and Patches should both be disabled, Some patches operation might not be equivalent). When the mark function is (target) => 'immutable', it means all the objects in the state structure are immutable. In this specific case, you can totally turn on AutoFreeze and Patches. mark supports multiple marks, and the marks are executed in order, and the first mark that returns a value will be used. 59 | ::: 60 | -------------------------------------------------------------------------------- /website/docs/advanced-guides/migration.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | --- 4 | 5 | # Migration from Immer to Mutative 6 | 7 | Mutative is a drop-in replacement for Immer, so you can use it with a few changes. 8 | 9 | :::tip 10 | [mutative-compat](https://github.com/exuanbo/mutative-compat) - Mutative wrapper with full Immer API compatibility, you can use it to quickly migrate from Immer to Mutative. 11 | ::: 12 | 13 | ## `produce()` -> `create()` 14 | 15 | Mutative auto freezing option is **disabled by default**, Immer auto freezing option is enabled by default (if disabled, Immer performance will have a more huge drop). 16 | 17 | > You need to check if auto freezing has any impact on your project. If it depends on auto freezing, you can enable it yourself in Mutative. 18 | 19 | ```ts 20 | import produce from 'immer'; 21 | 22 | const baseState = { 23 | list: [{ text: 'coding' }, { text: 'learning' }], 24 | }; 25 | 26 | const nextState = produce(baseState, (draft) => { 27 | draft[1].done = true; 28 | draft.push({ title: 'something' }); 29 | }); 30 | ``` 31 | 32 | Use Mutative 33 | 34 | ```ts 35 | import { create } from 'mutative'; 36 | 37 | const baseState = { 38 | list: [{ text: 'coding' }, { text: 'learning' }], 39 | }; 40 | 41 | const nextState = create(baseState, (draft) => { 42 | draft[1].done = true; 43 | draft.push({ title: 'something' }); 44 | }); 45 | ``` 46 | 47 | ## `Patches` 48 | 49 | ```ts 50 | import { produceWithPatches, applyPatches } from 'immer'; 51 | 52 | enablePatches(); 53 | 54 | const baseState = { 55 | info: { 56 | name: 'Michael', 57 | age: 33, 58 | }, 59 | }; 60 | 61 | const [nextState, patches, inversePatches] = produceWithPatches( 62 | baseState, 63 | (draft) => { 64 | draft.info.age++; 65 | } 66 | ); 67 | 68 | const state = applyPatches(nextState, inversePatches); 69 | 70 | expect(state).toEqual(baseState); 71 | ``` 72 | 73 | Use Mutative 74 | 75 | ```ts 76 | import { create, apply } from 'mutative'; 77 | 78 | const baseState = { 79 | info: { 80 | name: 'Michael', 81 | age: 33, 82 | }, 83 | }; 84 | 85 | const [nextState, patches, inversePatches] = create( 86 | baseState, 87 | (draft) => { 88 | draft.info.age++; 89 | }, 90 | { 91 | enablePatches: true, 92 | } 93 | ); 94 | 95 | const state = apply(nextState, inversePatches); 96 | 97 | expect(state).toEqual(baseState); 98 | ``` 99 | 100 | ## Return `undefined` 101 | 102 | ```ts 103 | import produce, { nothing } from 'immer'; 104 | 105 | const nextState = produce(baseState, (draft) => { 106 | return nothing; 107 | }); 108 | ``` 109 | 110 | Use Mutative 111 | 112 | ```ts 113 | import { create, rawReturn } from 'mutative'; 114 | 115 | const nextState = create(baseState, (draft) => { 116 | return rawReturn(undefined); 117 | }); 118 | ``` 119 | 120 | :::tip 121 | For more on how Mutative differs from Immer, visit [Mutative vs Immer](/docs/extra-topics/comparison-with-immer). 122 | ::: 123 | -------------------------------------------------------------------------------- /website/docs/advanced-guides/strict-mode.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Strict Mode 6 | 7 | Strict mode in Mutative is a feature designed to reinforce the library’s immutability guarantees by ensuring that state mutations adhere to the principles of immutable update patterns. 8 | 9 | ## Strict mode options 10 | 11 | strict option is `boolean` type, the default is `false`. 12 | 13 | - Forbid accessing non-draftable values in strict mode(unless using [unsafe()](/docs/api-reference/unsafe)). 14 | 15 | - When strict mode is enabled, mutable data can only be accessed using [`unsafe()`](/docs/api-reference/unsafe). 16 | 17 | :::tip 18 | **It is recommended to enable `strict` in development mode and disable `strict` in production mode.** This will ensure safe explicit returns and also keep good performance in the production build. If the value that does not mix any current draft or is `undefined` is returned, then use [rawReturn()](/docs/api-reference/rawreturn). 19 | 20 | If you'd like to enable strict mode by default in a development build and turn it off for production, you can use `strict: process.env.NODE_ENV !== 'production'`. 21 | ::: 22 | 23 | ## Enable strict mode 24 | 25 | Mutative's strict mode is a safeguard that developers can activate to enforce immutable state updates, thereby maintaining the integrity and predictability of state transitions. When strict mode is enabled, any attempt to directly modify the original state within a draft will throw an error, serving as an immediate alert to the developer that an immutable update rule has been violated. This helps catch potentially harmful mutations during development, allowing developers to address issues before they impact the application's stability. 26 | 27 | Key characteristics of strict mode include: 28 | 29 | - **Enforcing immutability**: By preventing direct state mutations, strict mode ensures that changes to the application state are made without side effects, preserving the original state until an explicit update is committed. 30 | - **Debugging aid**: It acts as a development aid by providing immediate feedback on improper state mutations, thus helping to identify and rectify bugs early in the development process. 31 | - **Compatibility with advanced operations**: Even in strict mode, Mutative provides APIs like [`unsafe()`](/docs/api-reference/unsafe) and [`rawReturn()`](/docs/api-reference/rawreturn) that allow developers to explicitly bypass immutability checks when necessary. These APIs must be used with a clear understanding of their implications to ensure they do not compromise the application’s state integrity. 32 | 33 | Strict mode is indicative of Mutative's commitment to offering robust, error-resistant state management. It is a testament to the library's philosophy of providing tools that encourage best practices while still offering the flexibility to handle advanced state manipulation scenarios when needed. As such, strict mode is an invaluable feature for developers aiming to build applications with a solid foundation in immutable state management principles. 34 | 35 | ```ts 36 | class Foobar { 37 | bar = 1; 38 | } 39 | 40 | const foobar = new Foobar(); 41 | const foobar0 = new Foobar(); 42 | const data = { 43 | foo: { 44 | bar: 'str', 45 | }, 46 | foobar, 47 | foobar0, 48 | }; 49 | 50 | const state = create( 51 | data, 52 | (draft) => { 53 | unsafe(() => { 54 | draft.foobar.bar = 2; 55 | }); 56 | // it will throw an error in strict mode 57 | draft.foobar0.bar = 3; 58 | }, 59 | { 60 | strict: true, 61 | } 62 | ); 63 | ``` 64 | -------------------------------------------------------------------------------- /website/docs/advanced-guides/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Typescript 6 | 7 | Mutative support is built into the typescript types. You can use the `Draft` and `Immutable` types to annotate your code. 8 | 9 | ## Using TypeScript 10 | 11 | - `castDraft()` 12 | - `castImmutable()` 13 | - `castMutable()` 14 | - `Draft` 15 | - `Immutable` 16 | - `Patches` 17 | - `Patch` 18 | - `Options` 19 | -------------------------------------------------------------------------------- /website/docs/api-reference/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "API Reference", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /website/docs/api-reference/apply.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # apply() 6 | 7 | Use `apply()` for applying [patches](/docs/advanced-guides/pathes) to get the new state. 8 | 9 | ## Usage 10 | 11 | ```ts 12 | import { create, apply } from 'mutative'; 13 | 14 | const baseState = { 15 | foo: 'bar', 16 | list: [{ text: 'todo' }], 17 | }; 18 | 19 | const [state, patches, inversePatches] = create( 20 | baseState, 21 | (draft) => { 22 | draft.foo = 'foobar'; 23 | draft.list.push({ text: 'learning' }); 24 | }, 25 | { 26 | enablePatches: true, 27 | } 28 | ); 29 | 30 | // you can apply patches to get the new state 31 | const nextState = apply(baseState, patches); 32 | 33 | expect(nextState).toEqual(state); 34 | const prevState = apply(state, inversePatches); 35 | expect(prevState).toEqual(baseState); 36 | ``` 37 | 38 | 39 | ### `apply(state, patches, options)` 40 | 41 | The options parameter is optional and supports two types of configurations: 42 | 43 | 1. Immutable options (similar to create options but without `enablePatches`): 44 | 45 | - `strict` - `boolean`, forbid accessing non-draftable values in strict mode 46 | - `enableAutoFreeze` - `boolean`, enable autoFreeze and return frozen state 47 | - `mark` - mark function to determine if a value is mutable/immutable 48 | 49 | ```ts 50 | const baseState = { foo: { bar: 'test' } }; 51 | 52 | // This will create a new state. 53 | const result = apply(baseState, [ 54 | { 55 | op: 'replace', 56 | path: ['foo', 'bar'], 57 | value: 'test2', 58 | }, 59 | ]); 60 | expect(baseState).not.toEqual({ foo: { bar: 'test2' } }); 61 | expect(result).toEqual({ foo: { bar: 'test2' } }); 62 | ``` 63 | 64 | 2. Mutable option(Mutative v1.2.0+): 65 | - `mutable` - `boolean`, if true the state will be mutated directly instead of creating a new state 66 | 67 | Example with mutable option: 68 | 69 | ```ts 70 | const baseState = { foo: { bar: 'test' } }; 71 | 72 | // This will modify baseState directly 73 | apply( 74 | baseState, 75 | [ 76 | { 77 | op: 'replace', 78 | path: ['foo', 'bar'], 79 | value: 'test2', 80 | }, 81 | ], 82 | { 83 | mutable: true, 84 | } 85 | ); 86 | expect(baseState).toEqual({ foo: { bar: 'test2' } }); 87 | ``` 88 | 89 | > ⚠️Note: The mutable option cannot be combined with other options. When using mutable option, apply() will return void instead of a new state. 90 | 91 | -------------------------------------------------------------------------------- /website/docs/api-reference/create.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # create() 6 | 7 | Use `create()` for draft mutation to get a new state, which also supports currying. 8 | 9 | ## Usage 10 | 11 | ```ts 12 | import { create } from 'mutative'; 13 | 14 | const baseState = { 15 | foo: 'bar', 16 | list: [{ text: 'todo' }], 17 | }; 18 | 19 | const state = create(baseState, (draft) => { 20 | draft.foo = 'foobar'; 21 | draft.list.push({ text: 'learning' }); 22 | }); 23 | ``` 24 | 25 | In this basic example, the changes to the draft are 'mutative' within the draft callback, and `create()` is finally executed with a new immutable state. 26 | 27 | ## create(state, fn, options) - options 28 | 29 | Then options is optional. 30 | 31 | - [strict](/docs/advanced-guides/strict-mode) - `boolean`, the default is false. 32 | 33 | > Forbid accessing non-draftable values in strict mode(unless using [unsafe()](/docs/api-reference/unsafe)). 34 | 35 | > When strict mode is enabled, mutable data can only be accessed using [`unsafe()`](/docs/api-reference/unsafe). 36 | 37 | > **It is recommended to enable `strict` in development mode and disable `strict` in production mode.** This will ensure safe explicit returns and also keep good performance in the production build. If the value that does not mix any current draft or is `undefined` is returned, then use [rawReturn()](/docs/api-reference/rawreturn). 38 | 39 | - [enablePatches](/docs/advanced-guides/pathes) - `boolean | { pathAsArray?: boolean; arrayLengthAssignment?: boolean; }`, the default is false. 40 | 41 | > Enable patch, and return the patches/inversePatches. 42 | 43 | > If you need to set the shape of the generated patch in more detail, then you can set `pathAsArray` and `arrayLengthAssignment`。`pathAsArray` default value is `true`, if it's `true`, the path will be an array, otherwise it is a string; `arrayLengthAssignment` default value is `true`, if it's `true`, the array length will be included in the patches, otherwise no include array length(**NOTE**: If `arrayLengthAssignment` is `false`, it is fully compatible with JSON Patch spec, but it may have additional performance loss), [view related discussions](https://github.com/unadlib/mutative/issues/6). 44 | 45 | - [enableAutoFreeze](/docs/advanced-guides/auto-freeze) - `boolean`, the default is false. 46 | 47 | > Enable autoFreeze, and return frozen state, and enable circular reference checking only in `development` mode. 48 | 49 | - [mark](/docs/advanced-guides/mark) - `(target) => ('mutable'|'immutable'|function) | (target) => ('mutable'|'immutable'|function)[]` 50 | > Set a mark to determine if the value is mutable or if an instance is an immutable, and it can also return a shallow copy function(`AutoFreeze` and `Patches` should both be disabled, Some patches operation might not be equivalent). 51 | > When the mark function is (target) => 'immutable', it means all the objects in the state structure are immutable. In this specific case, you can totally turn on `AutoFreeze` and `Patches`. 52 | > `mark` supports multiple marks, and the marks are executed in order, and the first mark that returns a value will be used. 53 | 54 | ## Currying 55 | 56 | ```ts 57 | const [draft, finalize] = create(baseState); 58 | draft.foobar.bar = 'baz'; 59 | const state = finalize(); 60 | ``` 61 | 62 | :::tip 63 | Support set options: 64 | ```ts 65 | const [draft, finalize] = create(baseState, { enableAutoFreeze: true }); 66 | ``` 67 | 68 | ::: 69 | 70 | More details about currying, please see [currying](/docs/advanced-guides/currying). 71 | -------------------------------------------------------------------------------- /website/docs/api-reference/current.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # current() 6 | 7 | Get the current value from a draft. 8 | 9 | ## Usage 10 | 11 | The `current()` API provides developers with a way to capture the current state of a draft within the state management process. This function creates a copy of the state at the exact point in time it is called, without the draft's Proxy wrappers. This can be extremely beneficial for debugging, as it allows developers to inspect the state without the additional complexity of proxies. 12 | 13 | Moreover, references to the object returned by `current()` can be safely retained outside of the `create` function without the risk of unintended side effects. It essentially gives you a snapshot of the state in the midst of mutation operations. 14 | 15 | Here's how `current()` behaves within the Mutative: 16 | 17 | - Objects generated by `current()` will have structural sharing with their original counterparts wherever no modifications have been made. 18 | - If a draft has not been changed, it is usually true that [`original(draft)`](/docs/api-reference/original) will be strictly equal to `current(draft)`, although this is not a strict guarantee. 19 | - Any subsequent alterations to the draft will not impact the snapshot provided by `current()`, with the exception of undraftable object references that remain mutable. 20 | - Objects retrieved via `current()` are not frozen. 21 | 22 | ```ts 23 | const baseState = { 24 | foo: 'bar', 25 | list: [{ text: 'todo' }], 26 | }; 27 | 28 | const state = create(baseState, (draft) => { 29 | draft.foo = 'foobar'; 30 | draft.list.push({ text: 'learning' }); 31 | expect(current(draft.list)).toEqual([{ text: 'todo' }, { text: 'learning' }]); 32 | }); 33 | ``` 34 | 35 | :::tip 36 | It is important to note that `current()` should be used judiciously. It is a potentially resource-intensive operation, especially if the draft is a large amount of data. Lastly, `current()` must only be invoked on draft objects and not on original state objects or finalized states. 37 | ::: 38 | -------------------------------------------------------------------------------- /website/docs/api-reference/isdraft.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 9 3 | --- 4 | 5 | # isDraft() 6 | 7 | Check if a value is a draft. 8 | 9 | ## Usage 10 | 11 | ```ts 12 | const baseState = { 13 | date: new Date(), 14 | list: [{ text: 'todo' }], 15 | }; 16 | 17 | const state = create(baseState, (draft) => { 18 | expect(isDraft(draft.date)).toBeFalsy(); 19 | expect(isDraft(draft.list)).toBeTruthy(); 20 | }); 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /website/docs/api-reference/isdraftable.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 8 3 | --- 4 | 5 | # isDraftable() 6 | 7 | Check if a value is draftable 8 | 9 | ## Usage 10 | 11 | ```ts 12 | const baseState = { 13 | date: new Date(), 14 | list: [{ text: 'todo' }], 15 | }; 16 | 17 | expect(isDraftable(baseState.date)).toBeFalsy(); 18 | expect(isDraftable(baseState.list)).toBeTruthy(); 19 | ``` 20 | 21 | :::tip 22 | You can set a mark to determine if the value is draftable, and the mark function should be the same as passing in `create()` mark option. 23 | ::: 24 | 25 | 26 | -------------------------------------------------------------------------------- /website/docs/api-reference/makecreator.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # makeCreator() 6 | 7 | `makeCreator()` only takes [options](/docs/api-reference/create#createstate-fn-options---options) as the first argument, resulting in a custom `create()` function. 8 | 9 | ## Usage 10 | 11 | This function can be particularly useful when dealing with drafts in state management. After initiating changes using mutative create, a draft state is created, which is a mutable proxy of the original state. This draft state can be modified without affecting the original state. The current() function allows you to access the state of this draft at any time, providing a snapshot of the state at the moment of invocation. 12 | 13 | ```ts 14 | const baseState = { 15 | foo: { 16 | bar: 'str', 17 | }, 18 | }; 19 | 20 | const create = makeCreator({ 21 | enablePatches: true, 22 | }); 23 | 24 | const [state, patches, inversePatches] = create(baseState, (draft) => { 25 | draft.foo.bar = 'new str'; 26 | }); 27 | 28 | // you can use the custom `create()` function option to override the default `makeCreator()` options like this: 29 | const nextState = create( 30 | baseState, 31 | (draft) => { 32 | draft.foo.bar = 'new str'; 33 | }, 34 | { 35 | enablePatches: false, 36 | } 37 | ); 38 | ``` 39 | -------------------------------------------------------------------------------- /website/docs/api-reference/marksimplebject.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 10 3 | --- 4 | 5 | # markSimpleObject() 6 | 7 | `markSimpleObject()` is a mark function that marks all objects as immutable. 8 | 9 | ## Usage 10 | 11 | If you want to mark all objects as immutable(raw object, plain object without prototype chains or cross-iframe objects with same domain), you can use `markSimpleObject()`. 12 | 13 | ```ts 14 | const baseState = { 15 | foo: { 16 | bar: 'str', 17 | }, 18 | simpleObject: Object.create(null), 19 | }; 20 | 21 | const state = create( 22 | baseState, 23 | (draft) => { 24 | draft.foo.bar = 'new str'; 25 | draft.simpleObject.a = 'a'; 26 | }, 27 | { 28 | mark: markSimpleObject, 29 | } 30 | ); 31 | 32 | expect(state.simpleObject).not.toBe(baseState.simpleObject); 33 | ``` 34 | 35 | -------------------------------------------------------------------------------- /website/docs/api-reference/original.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # original() 6 | 7 | Get the original value from a draft. 8 | 9 | ## Usage 10 | 11 | The `original()` is an essential function that allows you to obtain the original state from a draft state. When working with immutable state management, it is often necessary to reference the state before any changes were made to understand the transformations that have occurred. This is where `original()` comes into play. 12 | 13 | When you make changes to a state using Mutative's drafting system, you are essentially working on a mutable proxy that safely encapsulates changes without affecting the actual state. However, there may be instances where you need to compare the draft to the original, untouched state. The `original()` function provides a direct reference to this untouched state, enabling such comparisons and validations. 14 | 15 | Here are some key points about the `original()` API: 16 | 17 | - It returns a reference to the state before any modifications were made, allowing you to view the state as it was when the draft was first created. 18 | - This is particularly useful when you need to compare the current draft to the original state to determine what changes have occurred. 19 | - `original()` helps maintain the integrity of the immutable state by providing access to the unaltered state, ensuring that changes can be tracked and managed effectively. 20 | - It is especially useful for debugging purposes and when you need to make decisions based on the history of the state changes. 21 | - Just like with other Mutative APIs, `original()` is designed to be straightforward and easy to use, fitting seamlessly into the library’s philosophy of simplifying immutable state management. 22 | 23 | In summary, `original()` is a function that underscores Mutative's commitment to providing tools that make immutable state management both efficient and intuitive, allowing developers to work with a clear understanding of the state throughout the mutation process. 24 | 25 | ```ts 26 | const baseState = { 27 | foo: 'bar', 28 | list: [{ text: 'todo' }], 29 | }; 30 | 31 | const state = create(baseState, (draft) => { 32 | draft.foo = 'foobar'; 33 | draft.list.push({ text: 'learning' }); 34 | expect(original(draft.list)).toEqual([{ text: 'todo' }]); 35 | expect(original(draft.list)).not.toBe(draft.list); 36 | expect(original(draft.list)).toBe(baseState.list); 37 | }); 38 | ``` 39 | 40 | :::tip 41 | `original()` must only be invoked on draft objects and not on original state objects or finalized states. 42 | ::: 43 | -------------------------------------------------------------------------------- /website/docs/api-reference/rawreturn.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # rawReturn() 6 | 7 | For return values that do not contain any drafts, you can use `rawReturn()` to wrap this return value to improve performance. It ensure that the return value is only returned explicitly. 8 | 9 | ## Usage 10 | 11 | The `rawReturn()` API offers developers an advanced control mechanism over the return value of state mutation operations. Unlike the standard behavior where the state manipulation function returns the updated draft state, `rawReturn()` allows the return of a raw value directly, bypassing the draft mechanism. 12 | 13 | When you invoke `rawReturn()` within a state manipulation function, you instruct Mutative to use the value you pass to `rawReturn()` as the final result of the current operation. This capability is particularly useful when the desired outcome of an operation is not a new draft state but a specific value that reflects a computed result or a status code. 14 | 15 | Key aspects of the `rawReturn()` API include: 16 | 17 | - It overrides the default draft return, enabling a direct and explicit return of any value from a state mutation function. 18 | - This function can be used to return values such as `undefined`, `null`, or any other primitive or object, offering more flexibility in state management scenarios. `rawReturn()` can not return drafts or contain sub-drafts in draft tree, in strict mode `rawReturn()` will also warn if the return value contains drafts. 19 | - It is particularly useful in cases where you want to terminate the draft operation early and return a predetermined value. 20 | - The API helps maintain clear and explicit control over what is returned from mutation operations, aiding in creating predictable state management flows. 21 | 22 | In essence, `rawReturn()` expands the versatility of state management in Mutative, enabling developers to return immediate values from within draft operations, thereby enhancing the control they have over state transitions and outcomes. 23 | 24 | :::tip 25 | If using Redux, you can use `rawReturn()` to return the default state directly. 26 | ::: 27 | 28 | ```ts 29 | const baseState = { id: 'test' }; 30 | const state = create(baseState as { id: string } | undefined, (draft) => { 31 | return rawReturn(undefined); 32 | }); 33 | expect(state).toBe(undefined); 34 | ``` 35 | 36 | > If the return value mixes drafts, you should not use `rawReturn()`. 37 | 38 | ```ts 39 | const baseState = { a: 1, b: { c: 1 } }; 40 | const state = create(baseState, (draft) => { 41 | if (draft.b.c === 1) { 42 | return { 43 | ...draft, 44 | a: 2, 45 | }; 46 | } 47 | }); 48 | expect(state).toEqual({ a: 2, b: { c: 1 } }); 49 | expect(isDraft(state.b)).toBeFalsy(); 50 | ``` 51 | 52 | If you use `rawReturn()`, we recommend that you enable [`strict` mode](/docs/advanced-guides/strict-mode) in development. 53 | 54 | ```ts 55 | const baseState = { a: 1, b: { c: 1 } }; 56 | const state = create( 57 | baseState, 58 | (draft) => { 59 | if (draft.b.c === 1) { 60 | // it will warn `The return value contains drafts, please don't use 'rawReturn()' to wrap the return value.` in strict mode. 61 | return rawReturn({ 62 | ...draft, 63 | a: 2, 64 | }); 65 | } 66 | }, 67 | { 68 | strict: true, 69 | } 70 | ); 71 | expect(state).toEqual({ a: 2, b: { c: 1 } }); 72 | expect(isDraft(state.b)).toBeFalsy(); 73 | ``` 74 | 75 | :::warning 76 | In strict mode, if the return value contains drafts, it will warn: 77 | ``` 78 | The return value contains drafts, please don't use 'rawReturn()' to wrap the return value 79 | ``` 80 | 81 | If the return value may contain a draft, then please do NOT use `rawReturn()` to wrap the return value. 82 | ::: 83 | -------------------------------------------------------------------------------- /website/docs/api-reference/unsafe.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | --- 4 | 5 | # unsafe() 6 | 7 | When [strict mode](/docs/advanced-guides/strict-mode) is enabled, mutable data can only be accessed using `unsafe()`. 8 | 9 | ## Usage 10 | 11 | The `unsafe()` provides a means to perform non-standard state mutations which are not typically allowed within the Mutative strict immutability constraints. This API enables direct mutations on draft states or original objects, allowing developers to bypass the protective layers that prevent accidental state mutations. 12 | 13 | When used, `unsafe()` allows for direct assignment and manipulation of properties within a draft state, which can be necessary for certain operations that require a level of flexibility beyond the standard immutable update patterns. For example, it can be used when interacting with complex objects or when integrating with third-party libraries that may not adhere to immutable update patterns. 14 | 15 | Key features of the `unsafe()` API include: 16 | 17 | - Allowing mutations that would be restricted under the library’s strict mode. 18 | - Providing the ability to directly manipulate draft state properties. 19 | - Enabling integration with objects or libraries that require direct state mutation. 20 | - Assisting in scenarios where controlled mutations are necessary and do not undermine the overall integrity of the state management process. 21 | 22 | It's important to note that while `unsafe()` can be a powerful tool, it should be used with caution. Since it allows operations that circumvent the core immutability principles of Mutative, it should only be used when such operations are absolutely necessary and when the developer is aware of the potential risks involved. The `unsafe()` API underscores Mutative's commitment to offering robust and flexible state management tools while also catering to advanced use cases where exceptions to the immutability rules are required. 23 | 24 | ```ts 25 | const baseState = { 26 | list: [], 27 | date: new Date(), 28 | }; 29 | 30 | const state = create( 31 | baseState, 32 | (draft) => { 33 | unsafe(() => { 34 | draft.date.setFullYear(2000); 35 | }); 36 | // or return the mutable data: 37 | // const date = unsafe(() => draft.date); 38 | }, 39 | { 40 | strict: true, 41 | } 42 | ); 43 | ``` 44 | 45 | -------------------------------------------------------------------------------- /website/docs/extra-topics/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Extra Topics", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /website/docs/extra-topics/comparison-with-immer.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Comparison with Immer 6 | 7 | Mutative is a high-performance immutable update library, and Immer is a popular immutable update library. This page compares the differences between Mutative and Immer. 8 | 9 | ## Difference between Mutative and Immer 10 | 11 | | | Mutative | Immer | 12 | | :------------------------------------ | -------: | :---: | 13 | | Custom shallow copy | ✅ | ❌ | 14 | | Strict mode | ✅ | ❌ | 15 | | No data freeze by default | ✅ | ❌ | 16 | | Non-invasive marking | ✅ | ❌ | 17 | | Complete freeze data | ✅ | ❌ | 18 | | Non-global config | ✅ | ❌ | 19 | | async draft function | ✅ | ❌ | 20 | | Fully compatible with JSON Patch spec | ✅ | ❌ | 21 | | new Set methods(Mutative v1.1.0+) | ✅ | ❌ | 22 | 23 | Mutative has fewer bugs such as accidental draft escapes than Immer, [view details](https://github.com/unadlib/mutative/blob/main/test/immer-non-support.test.ts). 24 | 25 | ## Mutative vs Immer Performance 26 | 27 | > Mutative passed all of Immer's test cases. 28 | 29 | Measure(ops/sec) to update 50K arrays and 1K objects, bigger is better([view source](https://github.com/unadlib/mutative/blob/main/test/performance/benchmark.ts)). [Mutative v1.1.0 vs Immer v10.1.1] 30 | 31 | ![Benchmark](img/benchmark.jpg) 32 | 33 | ``` 34 | Naive handcrafted reducer - No Freeze x 4,670 ops/sec ±0.64% (96 runs sampled) 35 | Mutative - No Freeze x 6,747 ops/sec ±0.61% (95 runs sampled) 36 | Immer - No Freeze x 5.65 ops/sec ±1.53% (19 runs sampled) 37 | 38 | Mutative - Freeze x 1,062 ops/sec ±0.74% (95 runs sampled) 39 | Immer - Freeze x 394 ops/sec ±0.85% (93 runs sampled) 40 | 41 | Mutative - Patches and No Freeze x 1,011 ops/sec ±0.24% (98 runs sampled) 42 | Immer - Patches and No Freeze x 5.64 ops/sec ±0.22% (19 runs sampled) 43 | 44 | Mutative - Patches and Freeze x 545 ops/sec ±1.19% (94 runs sampled) 45 | Immer - Patches and Freeze x 215 ops/sec ±0.70% (86 runs sampled) 46 | 47 | The fastest method is Mutative - No Freeze 48 | ``` 49 | 50 | Run `yarn benchmark` to measure performance. 51 | 52 | > OS: macOS 14.7, CPU: Apple M1 Max, Node.js: v22.11.0 53 | 54 | Immer relies on auto-freeze to be enabled, if auto-freeze is disabled, Immer will have a huge performance drop and Mutative will have a huge performance lead, especially with large data structures it will have a performance lead of more than 50x. 55 | 56 | So if you are using Immer, you will have to enable auto-freeze for performance. Mutative is disabled auto-freeze by default. With the default configuration of both, we can see the 17x performance gap between Mutative (`6,747 ops/sec`) and Immer (`394 ops/sec`). 57 | 58 | Overall, Mutative has a huge performance lead over Immer in [more performance testing scenarios](https://github.com/unadlib/mutative/tree/main/test/performance). Run `yarn performance` to get all the performance results locally. 59 | -------------------------------------------------------------------------------- /website/docs/extra-topics/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Contributing 6 | 7 | Mutative goal is to provide efficient and immutable updates. The focus is on performance improvements and providing better APIs for better development experiences. We are still working on it and welcome PRs that may help Mutative. 8 | 9 | ### Development Workflow 10 | 11 | - Clone Mutative repo. 12 | - Run `yarn install` to install all the dependencies. 13 | - Run `yarn prettier` to format the code. 14 | - `yarn test --watch` runs an interactive test watcher. 15 | - Run `yarn commit` to make a git commit. 16 | - Create a pull request. 17 | -------------------------------------------------------------------------------- /website/docs/extra-topics/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # FAQ 6 | 7 | If you have any questions about mutative, you can feedback on [GitHub Discussions](https://github.com/unadlib/mutative/issues). 8 | 9 | ## Q&A 10 | 11 | - Why does Mutative have such good performance? 12 | 13 | Mutative optimization focus on shallow copy optimization, more complete lazy drafts, finalization process optimization, and more. 14 | 15 | - I'm already using Immer, can I migrate smoothly to Mutative? 16 | 17 | Yes. Unless you have to be compatible with Internet Explorer, Mutative supports almost all of Immer features, and you can easily migrate from Immer to Mutative. 18 | 19 | > Migration is also not possible for React Native that does not support Proxy. React Native uses a new JS engine during refactoring - Hermes, and it (if < v0.59 or when using the Hermes engine on React Native < v0.64) does [not support Proxy on Android](https://github.com/facebook/hermes/issues/33), but [React Native v0.64 with the Hermes engine support Proxy](https://reactnative.dev/blog/2021/03/12/version-0.64#hermes-with-proxy-support). 20 | 21 | - Can Mutative be integrated with Redux? 22 | 23 | Yes. Mutative supports return values for reducer, and `redux-toolkit` is considering support for [configurable `produce()`](https://github.com/reduxjs/redux-toolkit/pull/3074). 24 | -------------------------------------------------------------------------------- /website/docs/extra-topics/img/benchmark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/docs/extra-topics/img/benchmark.jpg -------------------------------------------------------------------------------- /website/docs/extra-topics/mutative-ecosystem.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Mutative Ecosystem 6 | 7 | The following are other libraries created based on Mutative: 8 | 9 | - [use-mutative](https://github.com/mutativejs/use-mutative) - A 2-6x faster alternative to useState with spread operation 10 | - [use-travel](https://github.com/mutativejs/use-travel) - A React hook for state time travel with undo, redo, reset and archive functionalities. 11 | - [mutative-compat](https://github.com/exuanbo/mutative-compat) - Mutative wrapper with full Immer API compatibility. 12 | - [mutability](https://github.com/mutativejs/mutability) - A JavaScript library for transactional mutable updates. 13 | - [zustand-mutative](https://github.com/mutativejs/zustand-mutative) - A Mutative middleware for Zustand enhances the efficiency of immutable state updates. 14 | - [jotai-mutative](https://github.com/mutativejs/jotai-mutative) - A Mutative extension for Jotai. 15 | - [usm](https://github.com/unadlib/usm) - A universal state modular library, supports Redux(5.x), MobX(6.x), Vuex(4.x) and Angular(2.0+). 16 | - [xstate-mutative](https://github.com/mutativejs/xstate-mutative) - A faster and more flexible utilities for using Mutative with XState. 17 | - [reactant](https://github.com/unadlib/reactant) - Reactant - A framework for building React applications with Mutative. 18 | -------------------------------------------------------------------------------- /website/docs/getting-started/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Getting Started", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "5 minutes to learn the most important Mutative concepts." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/docs/getting-started/concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Concepts 6 | 7 | Mutative is based on the Proxy, its core concepts are `draft` and `patch`. 8 | 9 | ## Base Workflow 10 | 11 | Mutative Workflow depicted in the image illustrates a three-stage process: 12 | 13 | ![mutative workflow](img/mutative-workflow.png) 14 | 15 | - The "Current State" represents the initial, unchanged state. 16 | - The "Draft" stage indicates a mutable phase where changes are made to a draft, marked in red, showing where modifications are occurring. The dotted lines suggest that these are accessed and drafts are created. 17 | - Finally, the "Next State" is the immutable data after the changes are finalized. 18 | 19 | ```ts 20 | const baseState = { 21 | a0: { 22 | b0: {}, 23 | }, 24 | a1: { 25 | b1: {}, 26 | b2: { 27 | c0: 0, 28 | }, 29 | }, 30 | a2: {}, 31 | } 32 | 33 | const nextState = create(baseState, (draft) => { 34 | const { a0 } = draft; 35 | // If it is draftable, once it has been accessed, it will generate a corresponding draft. 36 | expect(isDraft(a0)).toBeTruthy(); 37 | // each node is a draft, and the draft is a proxy object 38 | draft.a1.b2.c0 = 1; 39 | }); 40 | 41 | expect(nextState).not.toBe(baseState); 42 | expect(nextState.a0).toBe(baseState.a0); // generated draft, but not changed 43 | expect(nextState.a2).toBe(baseState.a2); // no generated draft, not changed 44 | expect(nextState.a1).not.toBe(baseState.a1); 45 | expect(nextState.a1.b2).not.toBe(baseState.a1.b2); 46 | expect(nextState.a1.b2.c0).toBe(1); 47 | ``` 48 | 49 | ## Drafts 50 | 51 | Using Mutative to produce a new immutable data(next state) from intermediate drafts. 52 | 53 | Mutative creates a draft copy based on the current state. The `draft` is a mutable `Proxy` object, which behaves the same as the original object. Those mutations are recorded and used to produce the next state once the draft function is done. Additionally, if the patches is enabled, it will also produce a `patches`. 54 | 55 | ## Patches 56 | 57 | `Patches` are operation patches for immutable updates, consisting of an array where each element is a `Patch`. A `Patch` is an object that contains a `path`, a `value`, and an `op`. The `path` is an array representing the path to an object; the `value` is a new value for the object; and `op` is a string representing the type of operation, which can be `add`, `remove`, or `replace`. 58 | 59 | By applying `patches`, immutable updates can be made to an object based on these patches. These patches enable immutable updates without modifying the original object, acting as instructions for the update process. 60 | 61 | ## Mark 62 | 63 | If a data structure mixes mutable and immutable data, Mutative supports marking both immutable and mutable data. It allows for non-invasive marking of nodes within this data tree, meaning the original object structure does not require an additional marking symbol. Mutative can maintain the original characteristics of the structure tree's nodes. And The option allows you to mark the immutable data with custom shallow copy. 64 | 65 | It is used to mark the immutable data that needs to be updated, and the mutable data that needs to be accessed. You pass the `mark` option to `create()` to mark the immutable data. 66 | -------------------------------------------------------------------------------- /website/docs/getting-started/img/all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/docs/getting-started/img/all.jpg -------------------------------------------------------------------------------- /website/docs/getting-started/img/benchmark-array.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/docs/getting-started/img/benchmark-array.jpg -------------------------------------------------------------------------------- /website/docs/getting-started/img/benchmark-object.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/docs/getting-started/img/benchmark-object.jpg -------------------------------------------------------------------------------- /website/docs/getting-started/img/benchmark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/docs/getting-started/img/benchmark.jpg -------------------------------------------------------------------------------- /website/docs/getting-started/img/mutative-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/docs/getting-started/img/mutative-workflow.png -------------------------------------------------------------------------------- /website/docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Installation 6 | 7 | Mutative is a npm package that can be used in Node.js and browsers, it is also compatible with TypeScript. 8 | 9 | ## Using Mutative from npm/Yarn/pnpm 10 | 11 | ```bash npm2yarn 12 | npm install mutative 13 | ``` 14 | 15 | Mutative size is only `4.12 KB` (minified and gzipped), it is very lightweight. 16 | 17 | ## Using Mutative from CDN 18 | 19 | - Unpkg: `` 20 | - JSDelivr: `` 21 | -------------------------------------------------------------------------------- /website/docs/getting-started/mutative-with-react.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Using Mutative with React 6 | 7 | You can use Mutative with React by using the `useMutative()` hook. 8 | 9 | ## use-mutative Hook 10 | 11 | A hook to use [mutative](https://github.com/unadlib/mutative) as a React hook to efficient update react state immutable with mutable way. 12 | 13 | ## Installation 14 | 15 | ```bash npm2yarn 16 | npm install mutative use-mutative 17 | ``` 18 | 19 | ## API 20 | 21 | ### useMutative 22 | 23 | Provide you can create immutable state easily with mutable way. 24 | 25 | ```tsx 26 | export function App() { 27 | const [state, setState] = useMutative( 28 | { 29 | foo: 'bar', 30 | list: [ 31 | { text: 'todo' }, 32 | ], 33 | }, 34 | ); 35 | 46 | 57 | } 58 | ``` 59 | 60 | ### useMutativeReducer 61 | 62 | Provide you can create immutable state easily with mutable way in reducer way. 63 | 64 | ```tsx 65 | function reducer( 66 | draft: State, 67 | action: { type: 'reset' | 'increment' | 'decrement' } 68 | ) { 69 | switch (action.type) { 70 | case 'reset': 71 | return initialState; 72 | case 'increment': 73 | return void draft.count++; 74 | case 'decrement': 75 | return void draft.count--; 76 | } 77 | } 78 | 79 | export function App() { 80 | const [state, dispatch] = useMutativeReducer(reducer, initialState); 81 | 82 | return ( 83 |
84 | Count: {state.count} 85 |
86 | 87 | 88 | 89 |
90 | ); 91 | } 92 | ``` 93 | 94 | ### Patches 95 | 96 | In some cases, you may want to get that patches from your update, we can pass `{ enablePatches: true }` options in `useMutative` or `useMutativeReducer`, that can provide you the ability to get that patches from pervious action. 97 | 98 | ```tsx 99 | const [state, setState, patches, inversePatches] = useMutative(initState, { 100 | enablePatches: true, 101 | }); 102 | 103 | const [state, dispatch, patchState] = useMutativeReducer( 104 | reducer, 105 | initState, 106 | initializer, 107 | { enablePatches: true } 108 | ); 109 | 110 | // actions be that actions that are applied in previous state. 111 | const [actions, patchGroup] = patchState; 112 | 113 | const [patches, inversePatches] = patches; 114 | ``` 115 | 116 | patches format will follow https://jsonpatch.com/, but the `"path"` field be array structure. 117 | -------------------------------------------------------------------------------- /website/docs/getting-started/usages.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Usages 6 | 7 | 8 | ## Use `create()` to create a new state 9 | 10 | ```ts 11 | import { create } from 'mutative'; 12 | 13 | const baseState = { 14 | foo: 'bar', 15 | list: [{ text: 'coding' }], 16 | }; 17 | 18 | const state = create(baseState, (draft) => { 19 | draft.list.push({ text: 'learning' }); 20 | }); 21 | 22 | expect(state).not.toBe(baseState); 23 | expect(state.list).not.toBe(baseState.list); 24 | ``` 25 | 26 | `create(baseState, (draft) => void, options?: Options): newState` 27 | 28 | The first argument of `create()` is the base state. Mutative drafts it and passes it to the arguments of the draft function, and performs the draft mutation until the draft function finishes, then Mutative will finalize it and produce the new state. 29 | 30 | Use `create()` for more advanced features by setting [`create()`](/docs/api-reference/create) options. 31 | -------------------------------------------------------------------------------- /website/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Introduction 6 | 7 | **Mutative** - A JavaScript library for efficient immutable updates, 2-6x faster than naive handcrafted reducer, and more than 10x faster than Immer. 8 | 9 | ## What is Mutative? 10 | 11 | Mutative can help simplify the updating of immutable data structures, such as those used in React and Redux. It allows you to write code in a mutable way for the draft object, and ultimately it produces a new immutable data structure (the next state), avoiding unnecessary accidental mutations or complex deep updates with spread operations. 12 | 13 | - **Syntax conciseness**: Mutative makes code more concise and intuitive by providing a draft object that can be directly modified. Traditional reducers require manual handling of object and array immutability, which often involves complex spread and copying operations. 14 | 15 | - **Performance**: Mutative provides higher performance than manually written reducers, especially when processing large data structures. 16 | 17 | - **Error reduction**: Writing immutable updates by hand is usually difficult, prone to errors, and cumbersome. It is easy to make the mistake of directly modifying the state, rather than returning the next state. Mutative avoids this type of problem through its design, as all modifications occur on the draft object, and finally a new immutable data is produced. 18 | 19 | For example, assume there is a state object that includes a list. We aim to mark the last item in the list as completed and then add a new item: 20 | 21 | ```ts 22 | const state = { 23 | list: [ 24 | { text: 'Learn TypeScript', done: true }, 25 | { text: 'Learn React', done: true }, 26 | { text: 'Learn Redux', done: false }, 27 | ], 28 | }; 29 | ``` 30 | 31 | If we were to use traditional immutable data updates, we might write it like this: 32 | 33 | ```ts 34 | const nextState = { 35 | ...state, 36 | list: [ 37 | ...state.list.slice(0, 2), 38 | { 39 | ...state.list[2], 40 | done: true, 41 | }, 42 | { text: 'Learn Mutative', done: true }, 43 | ], 44 | }; 45 | ``` 46 | 47 | Using Mutative, we could write it like this: 48 | 49 | ```ts 50 | import { create } from 'mutative'; 51 | 52 | const nextState = create(state, (draft) => { 53 | draft.list[2].done = true; 54 | draft.list.push({ text: 'Learn Mutative', done: true }); 55 | }); 56 | ``` 57 | 58 | This is the basic usage of Mutative, which can help us implement immutable updates more simply. 59 | 60 | ## Motivation 61 | 62 | Immer helps us write simpler immutable updates with `mutative` logic. 63 | 64 | But its performance issue causes a runtime performance overhead. Immer must have auto-freeze enabled by default(Performance will be worse if auto-freeze is disabled), such immutable state with Immer is not common. In scenarios such as cross-processing, remote data transfer, etc., these immutable data must be constantly frozen. 65 | 66 | There are more parts that could be improved, such as better type inference, non-intrusive markup, support for more types of immutability, Safer immutability, [more edge cases](https://github.com/unadlib/mutative/blob/main/test/immer-non-support.test.ts), and so on. 67 | 68 | This is why Mutative was created. 69 | 70 | ## Features and Benefits 71 | 72 | - **Mutation makes immutable updates** - Immutable data structures supporting objects, arrays, Sets and Maps. 73 | - **High performance** - 10x faster than immer by default, even faster than naive handcrafted reducer. 74 | - **Optional freezing state** - No freezing of immutable data by default. 75 | - **Support for JSON Patch** - Full compliance with JSON Patch specification. 76 | - **Custom shallow copy** - Support for more types of immutable data. 77 | - **Support mark for immutable and mutable data** - Allows for non-invasive marking. 78 | - **Safer mutable data access in strict mode** - It brings more secure immutable updates. 79 | - **Support for reducer** - Support reducer function and any other immutable state library. 80 | 81 | ## Credits 82 | 83 | Mutative is inspired by [Immer](https://immerjs.github.io/immer/). It is a great library that has helped many people write simpler immutable updates. Mutative is based on the same idea, but it has a different implementation and more features. 84 | -------------------------------------------------------------------------------- /website/i18n/en/docusaurus-plugin-content-blog/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": { 3 | "message": "Blog", 4 | "description": "The title for the blog used in SEO" 5 | }, 6 | "description": { 7 | "message": "Blog", 8 | "description": "The description for the blog used in SEO" 9 | }, 10 | "sidebar.title": { 11 | "message": "Recent posts", 12 | "description": "The label for the left sidebar" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/i18n/en/docusaurus-plugin-content-docs/current.json: -------------------------------------------------------------------------------- 1 | { 2 | "version.label": { 3 | "message": "Next", 4 | "description": "The label for version current" 5 | }, 6 | "sidebar.tutorialSidebar.category.Tutorial - Basics": { 7 | "message": "Tutorial - Basics", 8 | "description": "The label for category Tutorial - Basics in sidebar tutorialSidebar" 9 | }, 10 | "sidebar.tutorialSidebar.category.Tutorial - Basics.link.generated-index.description": { 11 | "message": "5 minutes to learn the most important Docusaurus concepts.", 12 | "description": "The generated-index page description for category Tutorial - Basics in sidebar tutorialSidebar" 13 | }, 14 | "sidebar.tutorialSidebar.category.Tutorial - Extras": { 15 | "message": "Tutorial - Extras", 16 | "description": "The label for category Tutorial - Extras in sidebar tutorialSidebar" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /website/i18n/en/docusaurus-theme-classic/footer.json: -------------------------------------------------------------------------------- 1 | { 2 | "link.title.Docs": { 3 | "message": "Docs", 4 | "description": "The title of the footer links column with title=Docs in the footer" 5 | }, 6 | "link.title.Community": { 7 | "message": "Community", 8 | "description": "The title of the footer links column with title=Community in the footer" 9 | }, 10 | "link.title.More": { 11 | "message": "More", 12 | "description": "The title of the footer links column with title=More in the footer" 13 | }, 14 | "link.item.label.Tutorial": { 15 | "message": "Tutorial", 16 | "description": "The label of footer link with label=Tutorial linking to /docs/intro" 17 | }, 18 | "link.item.label.Stack Overflow": { 19 | "message": "Stack Overflow", 20 | "description": "The label of footer link with label=Stack Overflow linking to https://stackoverflow.com/questions/tagged/mutative" 21 | }, 22 | "link.item.label.Discord": { 23 | "message": "Discord", 24 | "description": "The label of footer link with label=Discord linking to https://discordapp.com/invite/docusaurus" 25 | }, 26 | "link.item.label.Twitter": { 27 | "message": "Twitter", 28 | "description": "The label of footer link with label=Twitter linking to https://twitter.com/docusaurus" 29 | }, 30 | "link.item.label.Blog": { 31 | "message": "Blog", 32 | "description": "The label of footer link with label=Blog linking to /blog" 33 | }, 34 | "link.item.label.GitHub": { 35 | "message": "GitHub", 36 | "description": "The label of footer link with label=GitHub linking to https://github.com/unadlib/mutative" 37 | }, 38 | "copyright": { 39 | "message": "Copyright © 2023 Mutative, Inc.", 40 | "description": "The footer copyright" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /website/i18n/en/docusaurus-theme-classic/navbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": { 3 | "message": "Mutative", 4 | "description": "The title in the navbar" 5 | }, 6 | "logo.alt": { 7 | "message": "Mutative Logo", 8 | "description": "The alt text of navbar logo" 9 | }, 10 | "item.label.Tutorial": { 11 | "message": "Tutorial", 12 | "description": "Navbar item with label Tutorial" 13 | }, 14 | "item.label.Blog": { 15 | "message": "Blog", 16 | "description": "Navbar item with label Blog" 17 | }, 18 | "item.label.GitHub": { 19 | "message": "GitHub", 20 | "description": "Navbar item with label GitHub" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-blog/authors.yml: -------------------------------------------------------------------------------- 1 | unadlib: 2 | name: Michael Lin 3 | title: Author of Mutative 4 | url: https://unadlib.github.io 5 | image_url: https://github.com/unadlib.png 6 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-blog/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": { 3 | "message": "Blog", 4 | "description": "The title for the blog used in SEO" 5 | }, 6 | "description": { 7 | "message": "Blog", 8 | "description": "The description for the blog used in SEO" 9 | }, 10 | "sidebar.title": { 11 | "message": "Recent posts", 12 | "description": "The label for the left sidebar" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-blog/releases/1.0/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Announcing Mutative 1.0 3 | authors: unadlib 4 | tags: [release] 5 | image: ./social-card.png 6 | date: 2023-10-31 7 | --- 8 | 9 | // TODO 10 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-blog/releases/1.0/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/i18n/zh-CN/docusaurus-plugin-content-blog/releases/1.0/social-card.png -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current.json: -------------------------------------------------------------------------------- 1 | { 2 | "version.label": { 3 | "message": "Next", 4 | "description": "The label for version current" 5 | }, 6 | "sidebar.tutorialSidebar.category.Tutorial - Basics": { 7 | "message": "Tutorial - Basics", 8 | "description": "The label for category Tutorial - Basics in sidebar tutorialSidebar" 9 | }, 10 | "sidebar.tutorialSidebar.category.Tutorial - Basics.link.generated-index.description": { 11 | "message": "5 minutes to learn the most important Docusaurus concepts.", 12 | "description": "The generated-index page description for category Tutorial - Basics in sidebar tutorialSidebar" 13 | }, 14 | "sidebar.tutorialSidebar.category.Tutorial - Extras": { 15 | "message": "Tutorial - Extras", 16 | "description": "The label for category Tutorial - Extras in sidebar tutorialSidebar" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Tutorial Intro 6 | 7 | Let's discover **Docusaurus in less than 5 minutes**. 8 | 9 | ## Getting Started 10 | 11 | Get started by **creating a new site**. 12 | 13 | Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**. 14 | 15 | ### What you'll need 16 | 17 | - [Node.js](https://nodejs.org/en/download/) version 18.0 or above: 18 | - When installing Node.js, you are recommended to check all checkboxes related to dependencies. 19 | 20 | ## Generate a new site 21 | 22 | Generate a new Docusaurus site using the **classic template**. 23 | 24 | The classic template will automatically be added to your project after you run the command: 25 | 26 | ```bash 27 | npm init docusaurus@latest my-website classic 28 | ``` 29 | 30 | You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. 31 | 32 | The command also installs all necessary dependencies you need to run Docusaurus. 33 | 34 | ## Start your site 35 | 36 | Run the development server: 37 | 38 | ```bash 39 | cd my-website 40 | npm run start 41 | ``` 42 | 43 | The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there. 44 | 45 | The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/. 46 | 47 | Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes. 48 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-basics/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Tutorial - Basics", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "5 minutes to learn the most important Docusaurus concepts." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-basics/congratulations.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Congratulations! 6 | 7 | You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. 8 | 9 | Docusaurus has **much more to offer**! 10 | 11 | Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. 12 | 13 | Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/unadlib/mutative/discussions/4610) 14 | 15 | ## What's next? 16 | 17 | - Read the [official documentation](https://docusaurus.io/) 18 | - Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config) 19 | - Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration) 20 | - Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout) 21 | - Add a [search bar](https://docusaurus.io/docs/search) 22 | - Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase) 23 | - Get involved in the [Docusaurus Community](https://docusaurus.io/community/support) 24 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-basics/create-a-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Create a Blog Post 6 | 7 | Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... 8 | 9 | ## Create your first Post 10 | 11 | Create a file at `blog/2021-02-28-greetings.md`: 12 | 13 | ```md title="blog/2021-02-28-greetings.md" 14 | --- 15 | slug: greetings 16 | title: Greetings! 17 | authors: 18 | - name: Joel Marcey 19 | title: Co-creator of Docusaurus 1 20 | url: https://github.com/JoelMarcey 21 | image_url: https://github.com/JoelMarcey.png 22 | - name: Sébastien Lorber 23 | title: Docusaurus maintainer 24 | url: https://sebastienlorber.com 25 | image_url: https://github.com/slorber.png 26 | tags: [greetings] 27 | --- 28 | 29 | Congratulations, you have made your first post! 30 | 31 | Feel free to play around and edit this post as much you like. 32 | ``` 33 | 34 | A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings). 35 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-basics/create-a-document.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Create a Document 6 | 7 | Documents are **groups of pages** connected through: 8 | 9 | - a **sidebar** 10 | - **previous/next navigation** 11 | - **versioning** 12 | 13 | ## Create your first Doc 14 | 15 | Create a Markdown file at `docs/hello.md`: 16 | 17 | ```md title="docs/hello.md" 18 | # Hello 19 | 20 | This is my **first Docusaurus document**! 21 | ``` 22 | 23 | A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello). 24 | 25 | ## Configure the Sidebar 26 | 27 | Docusaurus automatically **creates a sidebar** from the `docs` folder. 28 | 29 | Add metadata to customize the sidebar label and position: 30 | 31 | ```md title="docs/hello.md" {1-4} 32 | --- 33 | sidebar_label: 'Hi!' 34 | sidebar_position: 3 35 | --- 36 | 37 | # Hello 38 | 39 | This is my **first Docusaurus document**! 40 | ``` 41 | 42 | It is also possible to create your sidebar explicitly in `sidebars.js`: 43 | 44 | ```js title="sidebars.js" 45 | export default { 46 | tutorialSidebar: [ 47 | 'intro', 48 | // highlight-next-line 49 | 'hello', 50 | { 51 | type: 'category', 52 | label: 'Tutorial', 53 | items: ['tutorial-basics/create-a-document'], 54 | }, 55 | ], 56 | }; 57 | ``` 58 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-basics/create-a-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Create a Page 6 | 7 | Add **Markdown or React** files to `src/pages` to create a **standalone page**: 8 | 9 | - `src/pages/index.js` → `localhost:3000/` 10 | - `src/pages/foo.md` → `localhost:3000/foo` 11 | - `src/pages/foo/bar.js` → `localhost:3000/foo/bar` 12 | 13 | ## Create your first React Page 14 | 15 | Create a file at `src/pages/my-react-page.js`: 16 | 17 | ```jsx live 18 | function Clock(props) { 19 | const [date, setDate] = useState(new Date()); 20 | useEffect(() => { 21 | const timerID = setInterval(() => tick(), 1000); 22 | 23 | return function cleanup() { 24 | clearInterval(timerID); 25 | }; 26 | }); 27 | 28 | function tick() { 29 | setDate(new Date()); 30 | } 31 | 32 | return ( 33 |
34 |

It is {date.toLocaleTimeString()}.

35 |
36 | ); 37 | } 38 | ``` 39 | 40 | A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). 41 | 42 | ## Create your first Markdown Page 43 | 44 | Create a file at `src/pages/my-markdown-page.md`: 45 | 46 | ```mdx title="src/pages/my-markdown-page.md" 47 | # My Markdown page 48 | 49 | This is a Markdown page 50 | ``` 51 | 52 | A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). 53 | 54 | ```mermaid 55 | graph TD; 56 | A-->B; 57 | A-->C; 58 | B-->D; 59 | C-->D; 60 | ``` 61 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-basics/deploy-your-site.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Deploy your site 6 | 7 | Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). 8 | 9 | It builds your site as simple **static HTML, JavaScript and CSS files**. 10 | 11 | ## Build your site 12 | 13 | Build your site **for production**: 14 | 15 | ```bash 16 | npm run build 17 | ``` 18 | 19 | The static files are generated in the `build` folder. 20 | 21 | ## Deploy your site 22 | 23 | Test your production build locally: 24 | 25 | ```bash 26 | npm run serve 27 | ``` 28 | 29 | The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). 30 | 31 | You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). 32 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-basics/markdown-features.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Markdown Features 6 | 7 | Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**. 8 | 9 | ## Front Matter 10 | 11 | Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/): 12 | 13 | ```text title="my-doc.md" 14 | // highlight-start 15 | --- 16 | id: my-doc-id 17 | title: My document title 18 | description: My document description 19 | slug: /my-custom-url 20 | --- 21 | // highlight-end 22 | 23 | ## Markdown heading 24 | 25 | Markdown text with [links](./hello.md) 26 | ``` 27 | 28 | ## Links 29 | 30 | Regular Markdown links are supported, using url paths or relative file paths. 31 | 32 | ```md 33 | Let's see how to [Create a page](/create-a-page). 34 | ``` 35 | 36 | ```md 37 | Let's see how to [Create a page](./create-a-page.md). 38 | ``` 39 | 40 | **Result:** Let's see how to [Create a page](./create-a-page.md). 41 | 42 | ## Images 43 | 44 | Regular Markdown images are supported. 45 | 46 | You can use absolute paths to reference images in the static directory (`static/img/mutative.png`): 47 | 48 | ```md 49 | ![Docusaurus logo](/img/mutative.png) 50 | ``` 51 | 52 | ![Docusaurus logo](/img/mutative.png) 53 | 54 | You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them: 55 | 56 | ```md 57 | ![Docusaurus logo](./img/mutative.png) 58 | ``` 59 | 60 | ## Code Blocks 61 | 62 | Markdown code blocks are supported with Syntax highlighting. 63 | 64 | ```jsx title="src/components/HelloDocusaurus.js" 65 | function HelloDocusaurus() { 66 | return ( 67 |

Hello, Docusaurus!

68 | ) 69 | } 70 | ``` 71 | 72 | ```jsx title="src/components/HelloDocusaurus.js" 73 | function HelloDocusaurus() { 74 | return

Hello, Docusaurus!

; 75 | } 76 | ``` 77 | 78 | ## Admonitions 79 | 80 | Docusaurus has a special syntax to create admonitions and callouts: 81 | 82 | :::tip My tip 83 | 84 | Use this awesome feature option 85 | 86 | ::: 87 | 88 | :::danger Take care 89 | 90 | This action is dangerous 91 | 92 | ::: 93 | 94 | :::tip My tip 95 | 96 | Use this awesome feature option 97 | 98 | ::: 99 | 100 | :::danger Take care 101 | 102 | This action is dangerous 103 | 104 | ::: 105 | 106 | ## MDX and React Components 107 | 108 | [MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**: 109 | 110 | ```jsx 111 | export const Highlight = ({children, color}) => ( 112 | { 121 | alert(`You clicked the color ${color} with label ${children}`) 122 | }}> 123 | {children} 124 | 125 | ); 126 | 127 | This is Docusaurus green ! 128 | 129 | This is Facebook blue ! 130 | ``` 131 | 132 | export const Highlight = ({children, color}) => ( 133 | { 142 | alert(`You clicked the color ${color} with label ${children}`); 143 | }}> 144 | {children} 145 | 146 | ); 147 | 148 | This is Docusaurus green ! 149 | 150 | This is Facebook blue ! 151 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-extras/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Tutorial - Extras", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-extras/img/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-extras/img/docsVersionDropdown.png -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-extras/img/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unadlib/mutative/433c9ed41483086b31c2f3a7addb5208aef29f40/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-extras/img/localeDropdown.png -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-extras/manage-docs-versions.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Manage Docs Versions 6 | 7 | Docusaurus can manage multiple versions of your docs. 8 | 9 | ## Create a docs version 10 | 11 | Release a version 1.0 of your project: 12 | 13 | ```bash 14 | npm run docusaurus docs:version 1.0 15 | ``` 16 | 17 | The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created. 18 | 19 | Your docs now have 2 versions: 20 | 21 | - `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs 22 | - `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs** 23 | 24 | ## Add a Version Dropdown 25 | 26 | To navigate seamlessly across versions, add a version dropdown. 27 | 28 | Modify the `docusaurus.config.js` file: 29 | 30 | ```js title="docusaurus.config.js" 31 | export default { 32 | themeConfig: { 33 | navbar: { 34 | items: [ 35 | // highlight-start 36 | { 37 | type: 'docsVersionDropdown', 38 | }, 39 | // highlight-end 40 | ], 41 | }, 42 | }, 43 | }; 44 | ``` 45 | 46 | The docs version dropdown appears in your navbar: 47 | 48 | ![Docs Version Dropdown](./img/docsVersionDropdown.png) 49 | 50 | ## Update an existing version 51 | 52 | It is possible to edit versioned docs in their respective folder: 53 | 54 | - `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello` 55 | - `docs/hello.md` updates `http://localhost:3000/docs/next/hello` 56 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-docs/current/tutorial-extras/translate-your-site.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Translate your site 6 | 7 | Let's translate `docs/intro.md` to French. 8 | 9 | ## Configure i18n 10 | 11 | Modify `docusaurus.config.js` to add support for the `fr` locale: 12 | 13 | ```js title="docusaurus.config.js" 14 | export default { 15 | i18n: { 16 | defaultLocale: 'en', 17 | locales: ['en', 'fr'], 18 | }, 19 | }; 20 | ``` 21 | 22 | ## Translate a doc 23 | 24 | Copy the `docs/intro.md` file to the `i18n/fr` folder: 25 | 26 | ```bash 27 | mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ 28 | 29 | cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md 30 | ``` 31 | 32 | Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French. 33 | 34 | ## Start your localized site 35 | 36 | Start your site on the French locale: 37 | 38 | ```bash 39 | npm run start -- --locale fr 40 | ``` 41 | 42 | Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated. 43 | 44 | :::caution 45 | 46 | In development, you can only use one locale at a time. 47 | 48 | ::: 49 | 50 | ## Add a Locale Dropdown 51 | 52 | To navigate seamlessly across languages, add a locale dropdown. 53 | 54 | Modify the `docusaurus.config.js` file: 55 | 56 | ```js title="docusaurus.config.js" 57 | export default { 58 | themeConfig: { 59 | navbar: { 60 | items: [ 61 | // highlight-start 62 | { 63 | type: 'localeDropdown', 64 | }, 65 | // highlight-end 66 | ], 67 | }, 68 | }, 69 | }; 70 | ``` 71 | 72 | The locale dropdown now appears in your navbar: 73 | 74 | ![Locale Dropdown](./img/localeDropdown.png) 75 | 76 | ## Build your localized site 77 | 78 | Build your site for a specific locale: 79 | 80 | ```bash 81 | npm run build -- --locale fr 82 | ``` 83 | 84 | Or build your site to include all the locales at once: 85 | 86 | ```bash 87 | npm run build 88 | ``` 89 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-plugin-content-pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-theme-classic/footer.json: -------------------------------------------------------------------------------- 1 | { 2 | "link.title.Docs": { 3 | "message": "Docs", 4 | "description": "The title of the footer links column with title=Docs in the footer" 5 | }, 6 | "link.title.Community": { 7 | "message": "Community", 8 | "description": "The title of the footer links column with title=Community in the footer" 9 | }, 10 | "link.title.More": { 11 | "message": "More", 12 | "description": "The title of the footer links column with title=More in the footer" 13 | }, 14 | "link.item.label.Tutorial": { 15 | "message": "Tutorial", 16 | "description": "The label of footer link with label=Tutorial linking to /docs/intro" 17 | }, 18 | "link.item.label.Stack Overflow": { 19 | "message": "Stack Overflow", 20 | "description": "The label of footer link with label=Stack Overflow linking to https://stackoverflow.com/questions/tagged/mutative" 21 | }, 22 | "link.item.label.Discord": { 23 | "message": "Discord", 24 | "description": "The label of footer link with label=Discord linking to https://discord.gg/vC9Uy3rEfA" 25 | }, 26 | "link.item.label.Twitter": { 27 | "message": "Twitter", 28 | "description": "The label of footer link with label=Twitter linking to https://twitter.com/unadlib" 29 | }, 30 | "link.item.label.Blog": { 31 | "message": "Blog", 32 | "description": "The label of footer link with label=Blog linking to /blog" 33 | }, 34 | "link.item.label.GitHub": { 35 | "message": "GitHub", 36 | "description": "The label of footer link with label=GitHub linking to https://github.com/unadlib/mutative" 37 | }, 38 | "copyright": { 39 | "message": "Copyright © 2023 Mutative, Inc.", 40 | "description": "The footer copyright" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /website/i18n/zh-CN/docusaurus-theme-classic/navbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": { 3 | "message": "Mutative", 4 | "description": "The title in the navbar" 5 | }, 6 | "logo.alt": { 7 | "message": "Mutative Logo", 8 | "description": "The alt text of navbar logo" 9 | }, 10 | "item.label.Tutorial": { 11 | "message": "Tutorial", 12 | "description": "Navbar item with label Tutorial" 13 | }, 14 | "item.label.Blog": { 15 | "message": "Blog", 16 | "description": "Navbar item with label Blog" 17 | }, 18 | "item.label.GitHub": { 19 | "message": "GitHub", 20 | "description": "Navbar item with label GitHub" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "^3.3.2", 18 | "@docusaurus/preset-classic": "^3.3.2", 19 | "@docusaurus/remark-plugin-npm2yarn": "^3.3.2", 20 | "@docusaurus/theme-live-codeblock": "^3.3.2", 21 | "@docusaurus/theme-mermaid": "^3.3.2", 22 | "@mdx-js/react": "^3.0.0", 23 | "clsx": "^1.2.1", 24 | "prism-react-renderer": "^2.1.0", 25 | "react": "^18.0.0", 26 | "react-dom": "^18.0.0" 27 | }, 28 | "devDependencies": { 29 | "@docusaurus/module-type-aliases": "^3.2.1", 30 | "@docusaurus/types": "^3.2.1" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 3 chrome version", 40 | "last 3 firefox version", 41 | "last 5 safari version" 42 | ] 43 | }, 44 | "engines": { 45 | "node": ">=18.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | export default sidebars; 34 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | /* eslint-disable react/jsx-props-no-spreading */ 3 | /* eslint-disable react/no-array-index-key */ 4 | /* eslint-disable prettier/prettier */ 5 | /* eslint-disable react/prop-types */ 6 | /* eslint-disable react/react-in-jsx-scope */ 7 | /* eslint-disable react/jsx-filename-extension */ 8 | import clsx from 'clsx'; 9 | import Heading from '@theme/Heading'; 10 | import styles from './styles.module.css'; 11 | 12 | const FeatureList = [ 13 | { 14 | title: 'Simplifies Immutable Updates', 15 | description: ( 16 | <> 17 | Writing immutable updates by hand is usually difficult, prone to errors, 18 | and cumbersome. Mutative helps us write simpler immutable updates with{' '} 19 | mutative logic. 20 | 21 | ), 22 | }, 23 | { 24 | title: 'High Performance', 25 | description: ( 26 | <> 27 | Mutative is faster than naive handcrafted reducer, and more than 10x 28 | faster than Immer. 29 | 30 | ), 31 | }, 32 | { 33 | title: 'Powerful', 34 | description: ( 35 | <> 36 | Mutative supports custom shallow copying. It enables the custom marking 37 | of mutable and immutable data. It also supports JSON patches 38 | specification, strict mode, Reducers, and more. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({ Svg, title, description }) { 45 | return ( 46 |
47 |
48 | {title} 49 |

{description}

50 |
51 |
52 | ); 53 | } 54 | 55 | export default function HomepageFeatures() { 56 | return ( 57 |
58 |
59 |
60 | {FeatureList.map((props, idx) => ( 61 | 62 | ))} 63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 4rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #ff8800; 10 | --ifm-color-primary-dark: #e67a00; 11 | --ifm-color-primary-darker: #d97400; 12 | --ifm-color-primary-darkest: #b35f00; 13 | --ifm-color-primary-light: #ff941a; 14 | --ifm-color-primary-lighter: #ff9a26; 15 | --ifm-color-primary-lightest: #ffac4d; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #fb931f; 23 | --ifm-color-primary-dark: #f98604; 24 | --ifm-color-primary-darker: #eb7e04; 25 | --ifm-color-primary-darkest: #c26803; 26 | --ifm-color-primary-light: #fba03b; 27 | --ifm-color-primary-lighter: #fca749; 28 | --ifm-color-primary-lightest: #fcbb72; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | /* eslint-disable react/no-danger */ 3 | /* eslint-disable react/jsx-curly-brace-presence */ 4 | /* eslint-disable react/jsx-filename-extension */ 5 | /* eslint-disable react/react-in-jsx-scope */ 6 | import clsx from 'clsx'; 7 | import Link from '@docusaurus/Link'; 8 | import Translate, { translate } from '@docusaurus/Translate'; 9 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 10 | import Layout from '@theme/Layout'; 11 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 12 | 13 | import Heading from '@theme/Heading'; 14 | import styles from './index.module.css'; 15 | 16 | function HomepageHeader() { 17 | return ( 18 |
19 |
20 | 21 | {'Mutative'} 29 | Better immutability
Faster immutable updates', 34 | }} 35 | /> 36 |
37 |
38 | 39 | Get Started 40 | 41 | 42 |