├── .npmignore ├── .prettierignore ├── .npmrc ├── Demo.gif ├── spec ├── disableProxy.js ├── support │ └── jasmine.json ├── libs │ └── deep-freeze.js ├── ImmutableAssign6Spec.js ├── ImmutableAssign3Spec.js ├── ImmutableAssign5Spec.js └── ImmutableAssign4Spec.js ├── benchmarks.png ├── .prettierrc.js ├── typings ├── deep-freeze │ └── deep-freeze.d.ts └── jasmine │ └── jasmine.d.ts ├── tsconfig.json ├── .gitignore ├── LICENSE ├── debug ├── test-intellisense.ts ├── test-intellisense.js ├── README.md ├── debug.html └── benchmarks.js ├── allCustomLaunchers.json ├── package.json ├── .github └── workflows │ └── npm-publish.yml ├── src ├── iassign.d.ts ├── iassign.js └── iassign.ts ├── deploy ├── iassign.d.ts └── iassign.js ├── karma.conf.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !deploy/**/* 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | spec/*.js 2 | *.md 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} 2 | -------------------------------------------------------------------------------- /Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineforce/ImmutableAssign/HEAD/Demo.gif -------------------------------------------------------------------------------- /spec/disableProxy.js: -------------------------------------------------------------------------------- 1 | 2 | console.debug("Disable proxy.") 3 | Proxy = undefined; -------------------------------------------------------------------------------- /benchmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineforce/ImmutableAssign/HEAD/benchmarks.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | printWidth: 80, 4 | trailingComma: 'none', 5 | arrowParens: 'always', 6 | singleQuote: true, 7 | semi: true, 8 | useTabs: false 9 | }; 10 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /typings/deep-freeze/deep-freeze.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for deep-freeze 2 | // Project: https://github.com/substack/deep-freeze 3 | // Definitions by: Bart van der Schoor 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | declare namespace DeepFreeze { 7 | export interface DeepFreezeInterface { 8 | (obj: T): T; 9 | } 10 | } 11 | 12 | declare module "deep-freeze" { 13 | let deepFreeze: DeepFreeze.DeepFreezeInterface; 14 | export = deepFreeze; 15 | } 16 | -------------------------------------------------------------------------------- /spec/libs/deep-freeze.js: -------------------------------------------------------------------------------- 1 | function deepFreeze (o) { 2 | Object.freeze(o); 3 | 4 | var oIsFunction = typeof o === "function"; 5 | var hasOwnProp = Object.prototype.hasOwnProperty; 6 | 7 | Object.getOwnPropertyNames(o).forEach(function (prop) { 8 | if (hasOwnProp.call(o, prop) 9 | && (oIsFunction ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments' : true ) 10 | && o[prop] !== null 11 | && (typeof o[prop] === "object" || typeof o[prop] === "function") 12 | && !Object.isFrozen(o[prop])) { 13 | deepFreeze(o[prop]); 14 | } 15 | }); 16 | 17 | return o; 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2015", 7 | "dom" 8 | ], 9 | "removeComments": false, 10 | // "noUnusedLocals": true, 11 | "jsx": "preserve", 12 | // Declaration is manually created by me 13 | // "declaration": true 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true 16 | }, 17 | "compileOnSave": true, 18 | "exclude": [ 19 | "node_modules", 20 | "bower_components", 21 | "deploy" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .tmp/ 40 | 41 | .vscode/ 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 engineforce 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 | -------------------------------------------------------------------------------- /debug/test-intellisense.ts: -------------------------------------------------------------------------------- 1 | const iassign: IIassign = require('../src/iassign'); 2 | 3 | // Deep freeze both input and output, can be used in development to make sure they don't change. 4 | iassign.setOption({ freeze: true }); 5 | 6 | function test1() { 7 | const map1 = { a: 1, b: 2, c: 3 }; 8 | 9 | // 1: Calling iassign() to update map1.b, using overload 2 10 | const map2 = iassign(map1, function(m) { 11 | m.b = 50; 12 | return m; 13 | }); 14 | 15 | console.log(map1); 16 | console.log(map2); 17 | } 18 | 19 | function test2() { 20 | const list1 = [1, 2]; 21 | 22 | // 2.1: Calling iassign() to push items to list1, using overload 2 23 | const list2 = iassign(list1, function(l) { 24 | l.push(3, 4, 5); 25 | return l; 26 | }); 27 | 28 | // list2 = [1, 2, 3, 4, 5] 29 | // list2 !== list1 30 | 31 | // 2.2: Calling iassign() to unshift item to list2, using overload 2 32 | const list3 = iassign(list2, function(l) { 33 | l.unshift(0); 34 | return l; 35 | }); 36 | 37 | // list3 = [0, 1, 2, 3, 4, 5] 38 | // list3 !== list2 39 | 40 | // 2.3, Calling iassign() to concat list1, list2 and list3, using overload 2 41 | const list4 = iassign(list1, function(l) { 42 | return l.concat(list2, list3); 43 | }); 44 | 45 | // list4 = [1, 2, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5] 46 | // list4 !== list1 47 | 48 | // 2.4, Calling iassign() to concat sort list4, using overload 2 49 | const list5 = iassign(list4, function(l) { 50 | return l.sort(); 51 | }); 52 | 53 | // list5 = [0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5] 54 | // list5 !== list4 55 | } 56 | 57 | function test3() { 58 | const nested1 = { 59 | a: { 60 | b: { 61 | c: [3, 4, 5], 62 | cc: { dd: { ee: { ff: { gg: { hh: { ii: { jj: {} } } } } } } } 63 | } 64 | } 65 | }; 66 | iassign( 67 | nested1, 68 | n => n.a.b.c, 69 | c => { 70 | c.push(10); 71 | return c; 72 | } 73 | ); 74 | } 75 | 76 | test1(); 77 | test2(); 78 | test3(); 79 | -------------------------------------------------------------------------------- /debug/test-intellisense.js: -------------------------------------------------------------------------------- 1 | var iassign = require('../src/iassign'); 2 | // Deep freeze both input and output, can be used in development to make sure they don't change. 3 | iassign.setOption({ freeze: true }); 4 | function test1() { 5 | var map1 = { a: 1, b: 2, c: 3 }; 6 | // 1: Calling iassign() to update map1.b, using overload 2 7 | var map2 = iassign(map1, function (m) { 8 | m.b = 50; 9 | return m; 10 | }); 11 | console.log(map1); 12 | console.log(map2); 13 | } 14 | function test2() { 15 | var list1 = [1, 2]; 16 | // 2.1: Calling iassign() to push items to list1, using overload 2 17 | var list2 = iassign(list1, function (l) { 18 | l.push(3, 4, 5); 19 | return l; 20 | }); 21 | // list2 = [1, 2, 3, 4, 5] 22 | // list2 !== list1 23 | // 2.2: Calling iassign() to unshift item to list2, using overload 2 24 | var list3 = iassign(list2, function (l) { 25 | l.unshift(0); 26 | return l; 27 | }); 28 | // list3 = [0, 1, 2, 3, 4, 5] 29 | // list3 !== list2 30 | // 2.3, Calling iassign() to concat list1, list2 and list3, using overload 2 31 | var list4 = iassign(list1, function (l) { 32 | return l.concat(list2, list3); 33 | }); 34 | // list4 = [1, 2, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5] 35 | // list4 !== list1 36 | // 2.4, Calling iassign() to concat sort list4, using overload 2 37 | var list5 = iassign(list4, function (l) { 38 | return l.sort(); 39 | }); 40 | // list5 = [0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5] 41 | // list5 !== list4 42 | } 43 | function test3() { 44 | var nested1 = { 45 | a: { 46 | b: { 47 | c: [3, 4, 5], 48 | cc: { dd: { ee: { ff: { gg: { hh: { ii: { jj: {} } } } } } } } 49 | } 50 | } 51 | }; 52 | iassign(nested1, function (n) { return n.a.b.c; }, function (c) { 53 | c.push(10); 54 | return c; 55 | }); 56 | } 57 | test1(); 58 | test2(); 59 | test3(); 60 | -------------------------------------------------------------------------------- /allCustomLaunchers.json: -------------------------------------------------------------------------------- 1 | { 2 | "sl_ie11": { 3 | "base": "SauceLabs", 4 | "browserName": "internet explorer", 5 | "platform": "Windows 10", 6 | "version": "11" 7 | }, 8 | "sl_ie10": { 9 | "base": "SauceLabs", 10 | "browserName": "internet explorer", 11 | "platform": "Windows 8", 12 | "version": "10" 13 | }, 14 | "sl_edge": { 15 | "base": "SauceLabs", 16 | "browserName": "MicrosoftEdge", 17 | "platform": "Windows 10", 18 | "version": "latest" 19 | }, 20 | "sl_chrome": { 21 | "base": "SauceLabs", 22 | "browserName": "chrome", 23 | "platform": "Windows 10", 24 | "version": "latest" 25 | }, 26 | "sl_firefox": { 27 | "base": "SauceLabs", 28 | "browserName": "firefox", 29 | "platform": "Windows 10", 30 | "version": "latest" 31 | }, 32 | "sl_mac_chrome": { 33 | "base": "SauceLabs", 34 | "browserName": "chrome", 35 | "platform": "macOS 10.12", 36 | "version": "latest" 37 | }, 38 | "sl_mac_safari": { 39 | "base": "SauceLabs", 40 | "browserName": "safari", 41 | "platform": "macOS 10.13", 42 | "version": "latest" 43 | }, 44 | "sl_mac_firefox": { 45 | "base": "SauceLabs", 46 | "browserName": "firefox", 47 | "platform": "macOS 10.12", 48 | "version": "latest" 49 | }, 50 | "sl_ios_11": { 51 | "base": "SauceLabs", 52 | "browserName": "Browser", 53 | "platform": "iOS", 54 | "version": "11.3", 55 | "deviceName": "iPhone 6s Simulator" 56 | }, 57 | "sl_ios": { 58 | "base": "SauceLabs", 59 | "browserName": "iphone", 60 | "platform": "iOS" 61 | }, 62 | "sl_android_5": { 63 | "base": "SauceLabs", 64 | "browserName": "Browser", 65 | "platform": "Android", 66 | "version": "5.1", 67 | "deviceName": "Android GoogleAPI Emulator" 68 | }, 69 | "sl_android_6": { 70 | "base": "SauceLabs", 71 | "browserName": "Browser", 72 | "platform": "Android", 73 | "version": "6.0", 74 | "deviceName": "Android GoogleAPI Emulator" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immutable-assign", 3 | "version": "2.1.5", 4 | "description": "Lightweight immutable helper that allows you to continue working with Plain JavaScript Objects", 5 | "main": "deploy/iassign.js", 6 | "types": "deploy/iassign.d.ts", 7 | "scripts": { 8 | "test": "node node_modules/istanbul/lib/cli.js cover node_modules/jasmine/bin/jasmine.js", 9 | "test-karma": "karma start ./karma.conf.js", 10 | "test-karma-win10": "node_modules/.bin/karma start --browsers Chrome,Fireforx,Edge,IE", 11 | "test-karma-win7": "node_modules/.bin/karma start --browsers Chrome,Fireforx,IE", 12 | "test-karma-mac": "node_modules/.bin/karma start --browsers Safari,Chrome", 13 | "test-karma-mac-no-proxy": "NO_PROXY='true' npm run test-karma-mac", 14 | "debug": "node --inspect --inspect-brk node_modules/jasmine/bin/jasmine.js", 15 | "build": "tsc && rm -rf deploy && mkdir deploy && cp -v src/*.{js,d.ts} deploy/", 16 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", 17 | "benchmarks": "node debug/benchmarks" 18 | }, 19 | "author": { 20 | "name": "engineforce", 21 | "url": "https://github.com/engineforce" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/engineforce/ImmutableAssign.git" 26 | }, 27 | "homepage": "https://github.com/engineforce/ImmutableAssign", 28 | "license": "MIT", 29 | "keywords": [ 30 | "immutable", 31 | "typescript", 32 | "javascript", 33 | "data", 34 | "stateless" 35 | ], 36 | "optionalDependencies": { 37 | "deep-freeze-strict": "^1.1.1" 38 | }, 39 | "devDependencies": { 40 | "@types/lodash": "^4.14.170", 41 | "braces": "^3.0.2", 42 | "chalk": "^4.1.1", 43 | "core-js": "^3.15.2", 44 | "coveralls": "^3.1.1", 45 | "deep-freeze": "^0.0.1", 46 | "edge-launcher": "^1.2.2", 47 | "expect": "^27.0.6", 48 | "immer": "^9.0.3", 49 | "immutable": "^4.0.0-rc.12", 50 | "istanbul": "^0.4.5", 51 | "jasmine": "^3.7.0", 52 | "karma": "^6.3.4", 53 | "karma-chrome-launcher": "^3.1.0", 54 | "karma-edge-launcher": "^0.4.2", 55 | "karma-firefox-launcher": "^2.1.1", 56 | "karma-ie-launcher": "^1.0.0", 57 | "karma-jasmine": "^4.0.1", 58 | "karma-phantomjs-launcher": "^1.0.4", 59 | "karma-safari-launcher": "^1.0.0", 60 | "karma-sauce-launcher": "^4.3.6", 61 | "lodash": "^4.17.21", 62 | "minimatch": "^3.0.4", 63 | "seamless-immutable": "^7.1.4", 64 | "timm": "^1.7.1", 65 | "typescript": "^4.3.5" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | schedule: 9 | - cron: '0 0 * * *' 10 | 11 | env: 12 | CI: true 13 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 14 | IASSIGN_SAUCE_USERNAME: ${{ secrets.IASSIGN_SAUCE_USERNAME }} 15 | IASSIGN_SAUCE_ACCESS_KEY: ${{ secrets.IASSIGN_SAUCE_ACCESS_KEY }} 16 | IASSIGN_QA_SAUCE_USERNAME: ${{ secrets.IASSIGN_QA_SAUCE_USERNAME }} 17 | IASSIGN_QA_SAUCE_ACCESS_KEY: ${{ secrets.IASSIGN_QA_SAUCE_ACCESS_KEY }} 18 | GIT_COMMIT: ${{ github.sha }} 19 | BUILD_NUMBER: ${{ github.run_id }} 20 | GIT_BRANCH: ${{ github.ref }} 21 | COVERALLS_SERVICE_NAME: GitHub 22 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 23 | 24 | 25 | jobs: 26 | build: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: actions/setup-node@v2 32 | with: 33 | node-version: 14 34 | 35 | - name: pre-browser-test 36 | run: | 37 | export LAUNCHERS_COUNT=$(node -p -e "Object.keys(require('./allCustomLaunchers.json')).length") 38 | export TESTS_COUNT=12 39 | test $LAUNCHERS_COUNT == $TESTS_COUNT || { echo "Number of browser tests ($"TESTS_COUNT") is not the same as number of launchers ($LAUNCHERS_COUNT)"; false; } 40 | - run: npm ci 41 | - run: npm test 42 | 43 | browser-tests: 44 | runs-on: ubuntu-latest 45 | needs: build 46 | strategy: 47 | fail-fast: true 48 | max-parallel: 3 49 | matrix: 50 | job: [0,1,2,3,4,5,6,7,8,9,10,11] 51 | 52 | steps: 53 | - uses: actions/checkout@v2 54 | - uses: actions/setup-node@v2 55 | with: 56 | node-version: 14 57 | registry-url: https://registry.npmjs.org/ 58 | - run: | 59 | npm ci 60 | CUSTOM_JOB_INDEX=${{ matrix.job }} npm run test-karma 61 | 62 | deploy: 63 | runs-on: ubuntu-latest 64 | needs: browser-tests 65 | if: github.ref == 'refs/heads/master' 66 | 67 | steps: 68 | - uses: actions/checkout@v2 69 | - uses: actions/setup-node@v2 70 | with: 71 | node-version: 14 72 | registry-url: https://registry.npmjs.org/ 73 | 74 | - name: Deploy 75 | run: | 76 | npm install 77 | rm -rf ./coverage 78 | npm test 79 | npm run coveralls 80 | export REMOTE_VERSION=$(npm dist-tag ls immutable-assign | cut -d' ' -f 2) 81 | export LOCAL_VERSION=$(node -p -e "require('./package.json').version") 82 | test "$REMOTE_VERSION" != "$LOCAL_VERSION" && npm publish || true 83 | env: 84 | COVERALLS_GIT_BRANCH: master 85 | -------------------------------------------------------------------------------- /src/iassign.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace ImmutableAssign { 2 | interface ICopyFunc { 3 | (value: T, propName: string): T; 4 | } 5 | 6 | interface IIassignOption { 7 | freeze?: boolean; // Deep freeze both input and output 8 | freezeInput?: boolean; // Deep freeze input 9 | freezeOutput?: boolean; // Deep freeze output 10 | useConstructor?: boolean; // Uses the constructor to create new instances 11 | copyFunc?: ICopyFunc; // Custom copy function, can be used to handle special types, e.g., Map, Set 12 | disableAllCheck?: boolean; 13 | disableHasReturnCheck?: boolean; 14 | // Disable validation for extra statements in the getProp() function, 15 | // which is needed when running the coverage, e.g., istanbul.js does add 16 | // instrument statements in our getProp() function, which can be safely ignored. 17 | disableExtraStatementCheck?: boolean; 18 | 19 | // Return the same object if setProp() returns its parameter (i.e., reference pointer not changed). 20 | ignoreIfNoChange?: boolean; 21 | } 22 | 23 | type getPropFunc = ( 24 | obj: TObj, 25 | context: TContext 26 | ) => TProp; 27 | type setPropFunc = (prop: TProp) => TProp; 28 | 29 | interface IIassign extends IIassignOption { 30 | // Intellisense for the TObj parameter in getProp will only work if we remove the auto added closing bracket of iassign, 31 | // and manually add the closing bracket at last. i.e., 32 | // 33 | // 1. Type iassign( in the editor 34 | // 2. Most editor will auto complete with closing bracket, e.g., iassign() 35 | // 3. If we continue to type without removing the closing bracket, e.g., iassign(nested, (n) => n.), 36 | // editor such as VS Code will not show any intellisense for "n" 37 | // 4. We must remove the closing bracket of iassign(), and intellisense will be shown for "n" 38 | ( 39 | obj: TObj, 40 | setProp: setPropFunc, 41 | option?: IIassignOption 42 | ): TObj; 43 | 44 | ( 45 | obj: TObj, 46 | getProp: getPropFunc, 47 | setProp: setPropFunc, 48 | context?: TContext, 49 | option?: IIassignOption 50 | ): TObj; 51 | 52 | ( 53 | obj: TObj, 54 | propPaths: (string | number)[], 55 | setProp: setPropFunc, 56 | context?: TContext, 57 | option?: IIassignOption 58 | ): TObj; 59 | 60 | // functional programming friendly style, moved obj to the last parameter and supports currying 61 | fp( 62 | option: IIassignOption, 63 | getPropOrPropPath: 64 | | getPropFunc 65 | | (string | number)[], 66 | setProp: setPropFunc, 67 | context?: TContext, 68 | obj?: TObj 69 | ): TObj; 70 | 71 | // In ES6, you cannot set property on imported module directly, because they are default 72 | // to readonly, in this case you need to use this method. 73 | setOption(option: IIassignOption): void; 74 | 75 | deepFreeze(obj: T): T; 76 | 77 | // ES6 default export 78 | default: ImmutableAssign.IIassign; 79 | } 80 | } 81 | 82 | declare var iassign: ImmutableAssign.IIassign; 83 | export as namespace iassign; 84 | export = iassign; 85 | -------------------------------------------------------------------------------- /deploy/iassign.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace ImmutableAssign { 2 | interface ICopyFunc { 3 | (value: T, propName: string): T; 4 | } 5 | 6 | interface IIassignOption { 7 | freeze?: boolean; // Deep freeze both input and output 8 | freezeInput?: boolean; // Deep freeze input 9 | freezeOutput?: boolean; // Deep freeze output 10 | useConstructor?: boolean; // Uses the constructor to create new instances 11 | copyFunc?: ICopyFunc; // Custom copy function, can be used to handle special types, e.g., Map, Set 12 | disableAllCheck?: boolean; 13 | disableHasReturnCheck?: boolean; 14 | // Disable validation for extra statements in the getProp() function, 15 | // which is needed when running the coverage, e.g., istanbul.js does add 16 | // instrument statements in our getProp() function, which can be safely ignored. 17 | disableExtraStatementCheck?: boolean; 18 | 19 | // Return the same object if setProp() returns its parameter (i.e., reference pointer not changed). 20 | ignoreIfNoChange?: boolean; 21 | } 22 | 23 | type getPropFunc = ( 24 | obj: TObj, 25 | context: TContext 26 | ) => TProp; 27 | type setPropFunc = (prop: TProp) => TProp; 28 | 29 | interface IIassign extends IIassignOption { 30 | // Intellisense for the TObj parameter in getProp will only work if we remove the auto added closing bracket of iassign, 31 | // and manually add the closing bracket at last. i.e., 32 | // 33 | // 1. Type iassign( in the editor 34 | // 2. Most editor will auto complete with closing bracket, e.g., iassign() 35 | // 3. If we continue to type without removing the closing bracket, e.g., iassign(nested, (n) => n.), 36 | // editor such as VS Code will not show any intellisense for "n" 37 | // 4. We must remove the closing bracket of iassign(), and intellisense will be shown for "n" 38 | ( 39 | obj: TObj, 40 | setProp: setPropFunc, 41 | option?: IIassignOption 42 | ): TObj; 43 | 44 | ( 45 | obj: TObj, 46 | getProp: getPropFunc, 47 | setProp: setPropFunc, 48 | context?: TContext, 49 | option?: IIassignOption 50 | ): TObj; 51 | 52 | ( 53 | obj: TObj, 54 | propPaths: (string | number)[], 55 | setProp: setPropFunc, 56 | context?: TContext, 57 | option?: IIassignOption 58 | ): TObj; 59 | 60 | // functional programming friendly style, moved obj to the last parameter and supports currying 61 | fp( 62 | option: IIassignOption, 63 | getPropOrPropPath: 64 | | getPropFunc 65 | | (string | number)[], 66 | setProp: setPropFunc, 67 | context?: TContext, 68 | obj?: TObj 69 | ): TObj; 70 | 71 | // In ES6, you cannot set property on imported module directly, because they are default 72 | // to readonly, in this case you need to use this method. 73 | setOption(option: IIassignOption): void; 74 | 75 | deepFreeze(obj: T): T; 76 | 77 | // ES6 default export 78 | default: ImmutableAssign.IIassign; 79 | } 80 | } 81 | 82 | declare var iassign: ImmutableAssign.IIassign; 83 | export as namespace iassign; 84 | export = iassign; 85 | -------------------------------------------------------------------------------- /spec/ImmutableAssign6Spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Test working with ES6 arrow function 4 | 5 | (function (root, factory) { 6 | 7 | if (typeof module === 'object' && typeof module.exports === 'object') { 8 | var v = factory(require, exports); if (v !== undefined) module.exports = v; 9 | } 10 | else if (typeof define === 'function' && define.amd) { 11 | define(["require", "exports"], factory); 12 | } else { 13 | // Browser globals (root is window) 14 | var browserRequire = function (name) { 15 | if ((name == "deep-freeze" || name == "deep-freeze-strict") && root.deepFreeze) { 16 | return root.deepFreeze; 17 | } 18 | 19 | if (name == "lodash" && root._) { 20 | return root._; 21 | } 22 | 23 | if (name == "immutable" && root.Immutable) { 24 | return root.Immutable; 25 | } 26 | 27 | if (name.indexOf("iassign") > -1 && root.iassign) { 28 | return root.iassign; 29 | } 30 | 31 | throw new Error("Unable to require: " + name); 32 | } 33 | 34 | factory(browserRequire, {}); 35 | } 36 | })(this, function (require, exports) { 37 | 38 | var iassign = require("../src/iassign"); 39 | var noDeepFreeze = false; 40 | try { 41 | var deepFreeze = require("deep-freeze-strict"); 42 | } 43 | catch (ex) { 44 | deepFreeze = function () { }; 45 | noDeepFreeze = true; 46 | console.warn("Cannot load deep-freeze module.", ex); 47 | } 48 | var _ = require("lodash"); 49 | var immutable = require("immutable"); 50 | 51 | if (typeof (global) !== "undefined") { 52 | console.log("In node"); 53 | if (global.process.env.running_under_istanbul) { 54 | // Handle coverage tool that may modify the source code. 55 | iassign.disableExtraStatementCheck = true; 56 | } 57 | } 58 | 59 | if (typeof (window) !== "undefined") { 60 | console.log("In browser"); 61 | if (window.__coverage__) { 62 | iassign.disableExtraStatementCheck = true; 63 | } 64 | } 65 | 66 | // Test if arrow function is supported 67 | try { 68 | new Function("(y => y)"); 69 | console.log("arrow function is supported.") 70 | } 71 | catch (err) { 72 | console.log("arrow function is not supported.") 73 | return; 74 | } 75 | 76 | describe("Test 6", function () { 77 | beforeEach(function () { 78 | }); 79 | if (typeof Proxy != "undefined") { 80 | it("Arrow function 1: with {}", function () { 81 | var nested1 = { a:{ b:{ c:[ {id: 3}, {id: 4}, {id: 5}], d: 1 } } }; 82 | 83 | var getPropCallCount = 0; 84 | var nested2 = iassign( 85 | nested1, 86 | function (n) { 87 | getPropCallCount++ 88 | var f = n.a.b.c[1] 89 | return f 90 | }, 91 | function (c1) { return { id: 12}; } 92 | ); 93 | 94 | expect(nested2).toEqual({ a:{ b:{ c:[ {id: 3}, {id: 12}, {id: 5}], d: 1 } } }) 95 | expect(getPropCallCount).toEqual(1) 96 | }); 97 | } 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /debug/README.md: -------------------------------------------------------------------------------- 1 | ## Performance test results 2 | 3 | ``` 4 | $ npm run benchmarks 5 | 6 | Mutable 7 | Object: read (x500000): 10 ms 8 | Object: write (x100000): 3 ms 9 | Object: deep read (x500000): 4 ms 10 | Object: deep write (x100000): 3 ms 11 | Object: very deep read (x500000): 32 ms 12 | Object: very deep write (x100000): 10 ms 13 | Object: merge (x100000): 18 ms 14 | Array: read (x500000): 5 ms 15 | Array: write (x100000): 3 ms 16 | Array: deep read (x500000): 6 ms 17 | Array: deep write (x100000): 4 ms 18 | Total elapsed = 57 ms (read) + 41 ms (write) = 98 ms. 19 | 20 | Immutable (Object.assign) 21 | Object: read (x500000): 13 ms 22 | Object: write (x100000): 97 ms 23 | Object: deep read (x500000): 9 ms 24 | Object: deep write (x100000): 167 ms 25 | Object: very deep read (x500000): 28 ms 26 | Object: very deep write (x100000): 245 ms 27 | Object: merge (x100000): 116 ms 28 | Array: read (x500000): 8 ms 29 | Array: write (x100000): 347 ms 30 | Array: deep read (x500000): 8 ms 31 | Array: deep write (x100000): 367 ms 32 | Total elapsed = 66 ms (read) + 1339 ms (write) = 1405 ms. 33 | 34 | Immutable (immutable-assign) 35 | Object: read (x500000): 14 ms 36 | Object: write (x100000): 37 ms 37 | Object: deep read (x500000): 7 ms 38 | Object: deep write (x100000): 219 ms 39 | Object: very deep read (x500000): 41 ms 40 | Object: very deep write (x100000): 597 ms 41 | Object: merge (x100000): 37 ms 42 | Array: read (x500000): 7 ms 43 | Array: write (x100000): 373 ms 44 | Array: deep read (x500000): 8 ms 45 | Array: deep write (x100000): 1002 ms 46 | Total elapsed = 77 ms (read) + 2265 ms (write) = 2342 ms. 47 | 48 | Immutable (immer setAutoFreeze(false)) 49 | Object: read (x500000): 10 ms 50 | Object: write (x100000): 211 ms 51 | Object: deep read (x500000): 6 ms 52 | Object: deep write (x100000): 371 ms 53 | Object: very deep read (x500000): 38 ms 54 | Object: very deep write (x100000): 679 ms 55 | Object: merge (x100000): 259 ms 56 | Array: read (x500000): 5 ms 57 | Array: write (x100000): 1489 ms 58 | Array: deep read (x500000): 6 ms 59 | Array: deep write (x100000): 1697 ms 60 | Total elapsed = 65 ms (read) + 4706 ms (write) = 4771 ms. 61 | 62 | Immutable (immutable.js) 63 | Object: read (x500000): 12 ms 64 | Object: write (x100000): 21 ms 65 | Object: deep read (x500000): 57 ms 66 | Object: deep write (x100000): 50 ms 67 | Object: very deep read (x500000): 107 ms 68 | Object: very deep write (x100000): 77 ms 69 | Object: merge (x100000): 498 ms 70 | Array: read (x500000): 17 ms 71 | Array: write (x100000): 94 ms 72 | Array: deep read (x500000): 61 ms 73 | Array: deep write (x100000): 107 ms 74 | Total elapsed = 254 ms (read) + 847 ms (write) = 1101 ms. 75 | 76 | Immutable (seamless-immutable production) 77 | Object: read (x500000): 14 ms 78 | Object: write (x100000): 364 ms 79 | Object: deep read (x500000): 7 ms 80 | Object: deep write (x100000): 790 ms 81 | Object: very deep read (x500000): 39 ms 82 | Object: very deep write (x100000): 1749 ms 83 | Object: merge (x100000): 408 ms 84 | Array: read (x500000): 6 ms 85 | Array: write (x100000): 12699 ms 86 | Array: deep read (x500000): 7 ms 87 | Array: deep write (x100000): 13678 ms 88 | Total elapsed = 73 ms (read) + 29688 ms (write) = 29761 ms. 89 | 90 | Immutable (Object.assign) + deep freeze 91 | Object: read (x500000): 10 ms 92 | Object: write (x100000): 184 ms 93 | Object: deep read (x500000): 16 ms 94 | Object: deep write (x100000): 320 ms 95 | Object: very deep read (x500000): 29 ms 96 | Object: very deep write (x100000): 488 ms 97 | Object: merge (x100000): 206 ms 98 | Array: read (x500000): 9 ms 99 | Array: write (x100000): 10351 ms 100 | Array: deep read (x500000): 15 ms 101 | Array: deep write (x100000): 10678 ms 102 | Total elapsed = 79 ms (read) + 22227 ms (write) = 22306 ms. 103 | 104 | Immutable (immutable-assign) + deep freeze 105 | Object: read (x500000): 12 ms 106 | Object: write (x100000): 31 ms 107 | Object: deep read (x500000): 19 ms 108 | Object: deep write (x100000): 265 ms 109 | Object: very deep read (x500000): 43 ms 110 | Object: very deep write (x100000): 431 ms 111 | Object: merge (x100000): 31 ms 112 | Array: read (x500000): 8 ms 113 | Array: write (x100000): 10658 ms 114 | Array: deep read (x500000): 14 ms 115 | Array: deep write (x100000): 20971 ms 116 | Total elapsed = 96 ms (read) + 32387 ms (write) = 32483 ms. 117 | 118 | Immutable (immer) + deep freeze 119 | Object: read (x500000): 15 ms 120 | Object: write (x100000): 334 ms 121 | Object: deep read (x500000): 15 ms 122 | Object: deep write (x100000): 611 ms 123 | Object: very deep read (x500000): 29 ms 124 | Object: very deep write (x100000): 932 ms 125 | Object: merge (x100000): 374 ms 126 | Array: read (x500000): 9 ms 127 | Array: write (x100000): 12394 ms 128 | Array: deep read (x500000): 14 ms 129 | Array: deep write (x100000): 13539 ms 130 | Total elapsed = 82 ms (read) + 28184 ms (write) = 28266 ms. 131 | ``` 132 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Aug 10 2016 13:14:23 GMT+1000 (AUS Eastern Standard Time) 3 | var { readFileSync } = require('fs'); 4 | var { toPairs, fromPairs, take } = require('lodash'); 5 | var assert = require('assert') 6 | var { 7 | NO_PROXY, 8 | GIT_COMMIT, 9 | BUILD_NUMBER, 10 | GIT_BRANCH, 11 | CUSTOM_JOB_INDEX, 12 | IASSIGN_SAUCE_USERNAME, 13 | IASSIGN_SAUCE_ACCESS_KEY, 14 | IASSIGN_QA_SAUCE_USERNAME, 15 | IASSIGN_QA_SAUCE_ACCESS_KEY 16 | } = process.env; 17 | 18 | console.log("ENV", { 19 | NO_PROXY, 20 | GIT_COMMIT, 21 | BUILD_NUMBER, 22 | GIT_BRANCH, 23 | CUSTOM_JOB_INDEX, 24 | IASSIGN_SAUCE_USERNAME, 25 | IASSIGN_SAUCE_ACCESS_KEY: `${mask(IASSIGN_SAUCE_ACCESS_KEY)}`, 26 | IASSIGN_QA_SAUCE_USERNAME, 27 | IASSIGN_QA_SAUCE_ACCESS_KEY: `${mask(IASSIGN_QA_SAUCE_ACCESS_KEY)}`, 28 | }) 29 | 30 | function mask(text, size) { 31 | size = size || 3 32 | 33 | if (!text) { 34 | return text 35 | } 36 | 37 | if (typeof text !== "string") { 38 | return text 39 | } 40 | 41 | return text.slice(0, size) + '...' + text.slice(-size) 42 | } 43 | 44 | var customLaunchers = []; 45 | 46 | if (process.argv.indexOf('--browsers') <= -1) { 47 | assert(GIT_COMMIT, 'GIT_COMMIT must exist'); 48 | assert(BUILD_NUMBER, 'BUILD_NUMBER must exist'); 49 | assert(GIT_BRANCH, 'GIT_BRANCH must exist'); 50 | // assert(CUSTOM_JOB_INDEX, 'CUSTOM_JOB_INDEX must exist'); 51 | // CUSTOM_JOB_INDEX = parseInt(CUSTOM_JOB_INDEX); 52 | 53 | var buildId = GIT_COMMIT + '-' + BUILD_NUMBER; 54 | 55 | if (GIT_BRANCH !== 'refs/heads/master') { 56 | var SAUCE_USERNAME = IASSIGN_QA_SAUCE_USERNAME; 57 | var SAUCE_ACCESS_KEY = IASSIGN_QA_SAUCE_ACCESS_KEY; 58 | } else { 59 | var SAUCE_USERNAME = IASSIGN_SAUCE_USERNAME; 60 | var SAUCE_ACCESS_KEY = IASSIGN_SAUCE_ACCESS_KEY; 61 | } 62 | 63 | assert(SAUCE_USERNAME, 'SAUCE_USERNAME must exist'); 64 | assert(SAUCE_ACCESS_KEY, 'SAUCE_ACCESS_KEY must exist'); 65 | 66 | console.log("SAUCE_LABS", { 67 | SAUCE_USERNAME: `${mask(SAUCE_USERNAME, 2)}`, 68 | SAUCE_ACCESS_KEY: `${mask(SAUCE_ACCESS_KEY)}`, 69 | }) 70 | 71 | var allCustomLaunchers = JSON.parse( 72 | readFileSync(`./allCustomLaunchers.json`, 'utf8') 73 | ); 74 | 75 | var customLaunchers = allCustomLaunchers 76 | if (CUSTOM_JOB_INDEX !== undefined) { 77 | customLaunchers = fromPairs([ 78 | toPairs(allCustomLaunchers)[CUSTOM_JOB_INDEX] 79 | ]); 80 | } 81 | 82 | console.log({ 83 | GIT_BRANCH, 84 | buildId, 85 | CUSTOM_JOB_INDEX, 86 | customLaunchers, 87 | browserCount: Object.keys(allCustomLaunchers).length 88 | }); 89 | } 90 | 91 | module.exports = function(config) { 92 | config.set({ 93 | // base path that will be used to resolve all patterns (eg. files, exclude) 94 | basePath: '', 95 | 96 | // frameworks to use 97 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 98 | frameworks: ['jasmine'], 99 | 100 | // list of files/patterns to load in the browser 101 | files: [ 102 | ...(NO_PROXY ? ['spec/disableProxy.js'] : []), 103 | 'spec/libs/*.js', 104 | 'node_modules/lodash/lodash.js', 105 | 'node_modules/immutable/dist/immutable.js', 106 | // 'node_modules/immer/dist/immer.umd.js', 107 | 'src/*.js', 108 | 'spec/**/*Spec.js' 109 | ], 110 | 111 | // list of files to exclude 112 | exclude: [], 113 | 114 | // preprocess matching files before serving them to the browser 115 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 116 | preprocessors: {}, 117 | 118 | // test results reporter to use 119 | // possible values: 'dots', 'progress' 120 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 121 | reporters: ['progress'], 122 | 123 | // web server port 124 | port: 9876, 125 | 126 | // enable / disable colors in the output (reporters and logs) 127 | colors: true, 128 | 129 | // level of logging 130 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 131 | logLevel: config.LOG_INFO, 132 | 133 | // enable / disable watching file and executing tests whenever any file changes 134 | autoWatch: true, 135 | 136 | sauceLabs: { 137 | testName: 'Immutable Assign Unit Tests', 138 | username: SAUCE_USERNAME, 139 | accessKey: SAUCE_ACCESS_KEY, 140 | startConnect: true, 141 | build: buildId 142 | }, 143 | customLaunchers: customLaunchers, 144 | browsers: Object.keys(customLaunchers), 145 | reporters: ['dots', 'saucelabs'], 146 | 147 | // start these browsers 148 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 149 | // 150 | // If browsers are not installed to the default location, you need to set the environment variable pointing to the installation path. 151 | // E.g., SET FIREFOX_BIN="C:\Program Files (x86)\MozillaFirefox4x\firefox.exe" 152 | // Refer to http://karma-runner.github.io/1.0/config/browsers.html 153 | //browsers: ["Chrome", "Firefox", "IE", "PhantomJS"], 154 | 155 | // To Run Edge, please install https://github.com/nicolasmccurdy/karma-edge-launcher manually. 156 | //browsers: ["Edge"], 157 | //browsers: ["Chrome", "Firefox", "IE", "Edge", "PhantomJS"], 158 | 159 | // Continuous Integration mode 160 | // if true, Karma captures browsers, runs the tests and exits 161 | singleRun: true, 162 | 163 | captureTimeout: 300000, 164 | browserNoActivityTimeout: 300000, 165 | retryLimit: 3, 166 | 167 | // Concurrency level 168 | // how many browser should be started simultaneous 169 | concurrency: 2 170 | }); 171 | }; 172 | -------------------------------------------------------------------------------- /debug/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | 12 | 13 | 14 | 15 | 16 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /spec/ImmutableAssign3Spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Test working with ES6 arrow function 4 | 5 | (function (root, factory) { 6 | 7 | if (typeof module === 'object' && typeof module.exports === 'object') { 8 | var v = factory(require, exports); if (v !== undefined) module.exports = v; 9 | } 10 | else if (typeof define === 'function' && define.amd) { 11 | define(["require", "exports"], factory); 12 | } else { 13 | // Browser globals (root is window) 14 | var browserRequire = function (name) { 15 | if ((name == "deep-freeze" || name == "deep-freeze-strict") && root.deepFreeze) { 16 | return root.deepFreeze; 17 | } 18 | 19 | if (name == "lodash" && root._) { 20 | return root._; 21 | } 22 | 23 | if (name == "immutable" && root.Immutable) { 24 | return root.Immutable; 25 | } 26 | 27 | if (name.indexOf("iassign") > -1 && root.iassign) { 28 | return root.iassign; 29 | } 30 | 31 | throw new Error("Unable to require: " + name); 32 | } 33 | 34 | factory(browserRequire, {}); 35 | } 36 | })(this, function (require, exports) { 37 | 38 | var iassign = require("../src/iassign"); 39 | var noDeepFreeze = false; 40 | try { 41 | var deepFreeze = require("deep-freeze-strict"); 42 | } 43 | catch (ex) { 44 | deepFreeze = function () { }; 45 | noDeepFreeze = true; 46 | console.warn("Cannot load deep-freeze module.", ex); 47 | } 48 | var _ = require("lodash"); 49 | var immutable = require("immutable"); 50 | 51 | if (typeof (global) !== "undefined") { 52 | console.log("In node"); 53 | if (global.process.env.running_under_istanbul) { 54 | // Handle coverage tool that may modify the source code. 55 | iassign.disableExtraStatementCheck = true; 56 | } 57 | } 58 | 59 | if (typeof (window) !== "undefined") { 60 | console.log("In browser"); 61 | if (window.__coverage__) { 62 | iassign.disableExtraStatementCheck = true; 63 | } 64 | } 65 | 66 | // Test if arrow function is supported 67 | try { 68 | new Function("(y => y)"); 69 | console.log("arrow function is supported.") 70 | } 71 | catch (err) { 72 | console.log("arrow function is not supported.") 73 | return; 74 | } 75 | 76 | describe("Test 3", function () { 77 | beforeEach(function () { 78 | }); 79 | it("Arrow function 1: with {}", function () { 80 | var o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }], [{ d: 31, e: 32 }]] } } }; 81 | deepFreeze(o1); 82 | 83 | var o2; 84 | eval("o2 = iassign(o1, (o) => { return o.a.b.c[0][0] }, (ci) => { ci.d++; return ci; });") 85 | 86 | expect(o2).not.toBe(o1); 87 | expect(o2.a).not.toBe(o1.a); 88 | expect(o2.a.b).not.toBe(o1.a.b); 89 | expect(o2.a.b.c).not.toBe(o1.a.b.c); 90 | expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]); 91 | expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]); 92 | expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d); 93 | 94 | expect(o2.a.b.c[0][0].d).toBe(12); 95 | }); 96 | 97 | it("Arrow function 2: without {} and return", function () { 98 | var o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }], [{ d: 31, e: 32 }]] } } }; 99 | deepFreeze(o1); 100 | 101 | var o2; 102 | eval("o2 = iassign(o1, (o) => o.a.b.c[0][0], ci => { ci.d++; return ci; });"); 103 | 104 | expect(o2).not.toBe(o1); 105 | expect(o2.a).not.toBe(o1.a); 106 | expect(o2.a.b).not.toBe(o1.a.b); 107 | expect(o2.a.b.c).not.toBe(o1.a.b.c); 108 | expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]); 109 | expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]); 110 | expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d); 111 | 112 | expect(o2.a.b.c[0][0].d).toBe(12); 113 | }); 114 | 115 | it("Arrow function 3: using context parameter", function () { 116 | var o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }]] } } }; 117 | deepFreeze(o1); // Ensure o1 is not changed, for testing only 118 | 119 | // 120 | // Calling iassign() to push increment to o1.a.b.c[0].d 121 | // 122 | var p1 = { a: 0 }; 123 | var o2; 124 | eval("o2 = iassign(o1, (o, ctx) => o.a.b.c[ctx.p1.a][0], (ci) => { ci.d++; return ci; }, { p1: p1 });"); 125 | 126 | expect(o2).not.toBe(o1); 127 | expect(o2.a).not.toBe(o1.a); 128 | expect(o2.a.b).not.toBe(o1.a.b); 129 | expect(o2.a.b.c).not.toBe(o1.a.b.c); 130 | expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]); 131 | expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]); 132 | expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d); 133 | expect(o2.a.b.c[0][0].d).toBe(12); 134 | }); 135 | 136 | it("Arrow function 4: without (), {} and return", function () { 137 | var o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }], [{ d: 31, e: 32 }]] } } }; 138 | deepFreeze(o1); 139 | 140 | var o2; 141 | eval("o2 = iassign(o1, o => o.a.b.c[0][0], (ci) => { ci.d++; return ci; });"); 142 | 143 | expect(o2).not.toBe(o1); 144 | expect(o2.a).not.toBe(o1.a); 145 | expect(o2.a.b).not.toBe(o1.a.b); 146 | expect(o2.a.b.c).not.toBe(o1.a.b.c); 147 | expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]); 148 | expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]); 149 | expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d); 150 | 151 | expect(o2.a.b.c[0][0].d).toBe(12); 152 | }); 153 | 154 | it("Arrow function 5: arrow function for set without ()), {} and return", function () { 155 | var o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }], [{ d: 31, e: 32 }]] } } }; 156 | deepFreeze(o1); 157 | 158 | var o2; 159 | eval("o2 = iassign(o1, o => o.a.b.c[0][0].d, d => d+1);"); 160 | 161 | expect(o2).not.toBe(o1); 162 | expect(o2.a).not.toBe(o1.a); 163 | expect(o2.a.b).not.toBe(o1.a.b); 164 | expect(o2.a.b.c).not.toBe(o1.a.b.c); 165 | expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]); 166 | expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]); 167 | expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d); 168 | 169 | expect(o2.a.b.c[0][0].d).toBe(12); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /spec/ImmutableAssign5Spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Test working with immutable.js 4 | 5 | (function (root, factory) { 6 | 7 | if (typeof module === 'object' && typeof module.exports === 'object') { 8 | var v = factory(require, exports); if (v !== undefined) module.exports = v; 9 | } 10 | else if (typeof define === 'function' && define.amd) { 11 | define(["require", "exports"], factory); 12 | } else { 13 | // Browser globals (root is window) 14 | var browserRequire = function (name) { 15 | if ((name == "deep-freeze" || name == "deep-freeze-strict") && root.deepFreeze) { 16 | return root.deepFreeze; 17 | } 18 | 19 | if (name == "lodash" && root._) { 20 | return root._; 21 | } 22 | 23 | if (name == "immutable" && root.Immutable) { 24 | return root.Immutable; 25 | } 26 | 27 | if (name.indexOf("iassign") > -1 && root.iassign) { 28 | return root.iassign; 29 | } 30 | 31 | throw new Error("Unable to require: " + name); 32 | } 33 | 34 | factory(browserRequire, {}); 35 | } 36 | })(this, function (require, exports) { 37 | 38 | var iassign = require("../src/iassign"); 39 | var noDeepFreeze = false; 40 | try { 41 | var deepFreeze = require("deep-freeze-strict"); 42 | } 43 | catch (ex) { 44 | deepFreeze = function () { }; 45 | noDeepFreeze = true; 46 | console.warn("Cannot load deep-freeze module.", ex); 47 | } 48 | var _ = require("lodash"); 49 | var immutable = require("immutable"); 50 | 51 | if (typeof (global) !== "undefined") { 52 | console.log("In node"); 53 | if (global.process.env.running_under_istanbul) { 54 | // Handle coverage tool that may modify the source code. 55 | iassign.disableExtraStatementCheck = true; 56 | } 57 | } 58 | 59 | if (typeof (window) !== "undefined") { 60 | console.log("In browser"); 61 | if (window.__coverage__) { 62 | iassign.disableExtraStatementCheck = true; 63 | } 64 | } 65 | 66 | describe("Test 5", function () { 67 | beforeEach(function () { 68 | }); 69 | it("Issue 12: Support Immutable.js Map", function () { 70 | 71 | var map1 = immutable.Map({"a": "value a"}); 72 | 73 | iassign.setOption({ 74 | copyFunc: function (value, propName) { 75 | // No need to copy 76 | //if (immutable.isImmutable(value)) { // Added in v4+ 77 | if (immutable.Iterable.isIterable(value)) { 78 | return value; 79 | } 80 | } 81 | }); 82 | 83 | var map2 = iassign( 84 | map1, 85 | function(m) { return m.set(1, 'first'); } 86 | ); 87 | 88 | expect(map2).not.toBe(map1); 89 | 90 | expect(map2.size).toBe(2); 91 | expect(map2.get("a")).toBe("value a"); 92 | expect(map2.get(1)).toBe("first"); 93 | expect(map1.size).toBe(1); 94 | expect(map1.get("a")).toBe("value a"); 95 | expect(map1.get(1)).toBe(undefined); 96 | 97 | var map3 = iassign( 98 | map2, 99 | function(m) { return m.set(2, 'second'); } 100 | ); 101 | 102 | expect(map3).not.toBe(map2); 103 | expect(map3).not.toBe(map1); 104 | 105 | expect(map3.size).toBe(3); 106 | expect(map3.get("a")).toBe("value a"); 107 | expect(map3.get(1)).toBe("first"); 108 | expect(map3.get(2)).toBe("second"); 109 | 110 | expect(map2.size).toBe(2); 111 | expect(map2.get("a")).toBe("value a"); 112 | expect(map2.get(1)).toBe("first"); 113 | expect(map2.get(2)).toBe(undefined); 114 | 115 | expect(map1.size).toBe(1); 116 | expect(map1.get("a")).toBe("value a"); 117 | expect(map1.get(1)).toBe(undefined); 118 | expect(map1.get(2)).toBe(undefined); 119 | }); 120 | 121 | it("Issue 12: Support Immutable.js Map in nested object", function () { 122 | var o1 = { a: { b: { c: [[{ d: immutable.Map({"a": "value a"}), e: 12 }], [{ d: 21, e: 22 }], [{ d: 31, e: 32 }]] } } }; 123 | deepFreeze(o1); 124 | 125 | iassign.setOption({ 126 | copyFunc: function (value, propName) { 127 | if (immutable.Iterable.isIterable(value)) { 128 | return value; 129 | } 130 | } 131 | }); 132 | 133 | var o2 = iassign( 134 | o1, 135 | function (o) { return o.a.b.c[0][0].d }, 136 | function (d) { return d.set(1, "first"); }); 137 | 138 | expect(o2).not.toBe(o1); 139 | expect(o2.a).not.toBe(o1.a); 140 | expect(o2.a.b).not.toBe(o1.a.b); 141 | expect(o2.a.b.c).not.toBe(o1.a.b.c); 142 | expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]); 143 | expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]); 144 | expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d); 145 | 146 | expect(o2.a.b.c[0][0].d.size).toBe(2); 147 | expect(o2.a.b.c[0][0].d.get("a")).toBe("value a"); 148 | expect(o2.a.b.c[0][0].d.get(1)).toBe("first"); 149 | expect(o1.a.b.c[0][0].d.size).toBe(1); 150 | expect(o1.a.b.c[0][0].d.get("a")).toBe("value a"); 151 | expect(o1.a.b.c[0][0].d.get(1)).toBe(undefined); 152 | 153 | var o3 = iassign( 154 | o2, 155 | function (o) { return o.a.b.c[0][0].d }, 156 | function (d) { return d.set(2, "second"); }); 157 | 158 | expect(o3).not.toBe(o2); 159 | expect(o3.a).not.toBe(o2.a); 160 | expect(o3.a.b).not.toBe(o2.a.b); 161 | expect(o3.a.b.c).not.toBe(o2.a.b.c); 162 | expect(o3.a.b.c[0]).not.toBe(o2.a.b.c[0]); 163 | expect(o3.a.b.c[0][0]).not.toBe(o2.a.b.c[0][0]); 164 | expect(o3.a.b.c[0][0].d).not.toBe(o2.a.b.c[0][0].d); 165 | 166 | expect(o3.a.b.c[0][0].d.size).toBe(3); 167 | expect(o3.a.b.c[0][0].d.get("a")).toBe("value a"); 168 | expect(o3.a.b.c[0][0].d.get(1)).toBe("first"); 169 | expect(o3.a.b.c[0][0].d.get(2)).toBe("second"); 170 | expect(o2.a.b.c[0][0].d.size).toBe(2); 171 | expect(o2.a.b.c[0][0].d.get("a")).toBe("value a"); 172 | expect(o2.a.b.c[0][0].d.get(1)).toBe("first"); 173 | expect(o2.a.b.c[0][0].d.get(2)).toBe(undefined); 174 | }); 175 | 176 | it("Issue 12: Support Immutable.js Set", function () { 177 | 178 | var set1 = immutable.Set(["a"]); 179 | 180 | iassign.setOption({ 181 | copyFunc: function (value, propName) { 182 | if (immutable.Iterable.isIterable(value)) { 183 | return value; 184 | } 185 | } 186 | }); 187 | 188 | var set2 = iassign( 189 | set1, 190 | function(m) { return m.add(1); } 191 | ); 192 | 193 | expect(set2).not.toBe(set1); 194 | 195 | expect(set2.size).toBe(2); 196 | expect(set2.has("a")).toBe(true); 197 | expect(set2.has(1)).toBe(true); 198 | 199 | expect(set1.size).toBe(1); 200 | expect(set1.has("a")).toBe(true); 201 | expect(set1.has(1)).toBe(false); 202 | 203 | var set3 = iassign( 204 | set2, 205 | function(m) { return m.add(2); } 206 | ); 207 | 208 | expect(set3).not.toBe(set2); 209 | expect(set3).not.toBe(set1); 210 | 211 | expect(set3.size).toBe(3); 212 | expect(set3.has("a")).toBe(true); 213 | expect(set3.has(1)).toBe(true); 214 | expect(set3.has(2)).toBe(true); 215 | 216 | expect(set2.size).toBe(2); 217 | expect(set2.has("a")).toBe(true); 218 | expect(set2.has(1)).toBe(true); 219 | expect(set2.has(2)).toBe(false); 220 | 221 | expect(set1.size).toBe(1); 222 | expect(set1.has("a")).toBe(true); 223 | expect(set1.has(1)).toBe(false); 224 | expect(set1.has(2)).toBe(false); 225 | }); 226 | 227 | it("Issue 12: Support Immutable.js Set in nested object", function () { 228 | var o1 = { a: { b: { c: [[{ d: immutable.Set(["a"]), e: 12 }], [{ d: 21, e: 22 }], [{ d: 31, e: 32 }]] } } }; 229 | deepFreeze(o1); 230 | 231 | iassign.setOption({ 232 | copyFunc: function (value, propName) { 233 | if (immutable.Iterable.isIterable(value)) { 234 | return value; 235 | } 236 | } 237 | }); 238 | 239 | var o2 = iassign( 240 | o1, 241 | function (o) { return o.a.b.c[0][0].d }, 242 | function (d) { return d.add(1); }); 243 | 244 | expect(o2).not.toBe(o1); 245 | expect(o2.a).not.toBe(o1.a); 246 | expect(o2.a.b).not.toBe(o1.a.b); 247 | expect(o2.a.b.c).not.toBe(o1.a.b.c); 248 | expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]); 249 | expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]); 250 | expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d); 251 | 252 | expect(o2.a.b.c[0][0].d.size).toBe(2); 253 | expect(o2.a.b.c[0][0].d.has("a")).toBe(true); 254 | expect(o2.a.b.c[0][0].d.has(1)).toBe(true); 255 | expect(o1.a.b.c[0][0].d.size).toBe(1); 256 | expect(o1.a.b.c[0][0].d.has("a")).toBe(true); 257 | expect(o1.a.b.c[0][0].d.has(1)).toBe(false); 258 | 259 | var o3 = iassign( 260 | o2, 261 | function (o) { return o.a.b.c[0][0].d }, 262 | function (d) { return d.add(2); }); 263 | 264 | expect(o3).not.toBe(o2); 265 | expect(o3.a).not.toBe(o2.a); 266 | expect(o3.a.b).not.toBe(o2.a.b); 267 | expect(o3.a.b.c).not.toBe(o2.a.b.c); 268 | expect(o3.a.b.c[0]).not.toBe(o2.a.b.c[0]); 269 | expect(o3.a.b.c[0][0]).not.toBe(o2.a.b.c[0][0]); 270 | expect(o3.a.b.c[0][0].d).not.toBe(o2.a.b.c[0][0].d); 271 | 272 | expect(o3.a.b.c[0][0].d.size).toBe(3); 273 | expect(o3.a.b.c[0][0].d.has("a")).toBe(true); 274 | expect(o3.a.b.c[0][0].d.has(1)).toBe(true); 275 | expect(o3.a.b.c[0][0].d.has(2)).toBe(true); 276 | expect(o2.a.b.c[0][0].d.size).toBe(2); 277 | expect(o2.a.b.c[0][0].d.has("a")).toBe(true); 278 | expect(o2.a.b.c[0][0].d.has(1)).toBe(true); 279 | expect(o2.a.b.c[0][0].d.has(2)).toBe(false); 280 | }); 281 | }); 282 | }); 283 | -------------------------------------------------------------------------------- /spec/ImmutableAssign4Spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Test working with ES6 Map and Set 4 | 5 | (function (root, factory) { 6 | 7 | if (typeof module === 'object' && typeof module.exports === 'object') { 8 | var v = factory(require, exports); if (v !== undefined) module.exports = v; 9 | } 10 | else if (typeof define === 'function' && define.amd) { 11 | define(["require", "exports"], factory); 12 | } else { 13 | // Browser globals (root is window) 14 | var browserRequire = function (name) { 15 | if ((name == "deep-freeze" || name == "deep-freeze-strict") && root.deepFreeze) { 16 | return root.deepFreeze; 17 | } 18 | 19 | if (name == "lodash" && root._) { 20 | return root._; 21 | } 22 | 23 | if (name == "immutable" && root.Immutable) { 24 | return root.Immutable; 25 | } 26 | 27 | if (name.indexOf("iassign") > -1 && root.iassign) { 28 | return root.iassign; 29 | } 30 | 31 | throw new Error("Unable to require: " + name); 32 | } 33 | 34 | factory(browserRequire, {}); 35 | } 36 | })(this, function (require, exports) { 37 | 38 | var iassign = require("../src/iassign"); 39 | var noDeepFreeze = false; 40 | try { 41 | var deepFreeze = require("deep-freeze-strict"); 42 | } 43 | catch (ex) { 44 | deepFreeze = function () { }; 45 | noDeepFreeze = true; 46 | console.warn("Cannot load deep-freeze module.", ex); 47 | } 48 | var _ = require("lodash"); 49 | var immutable = require("immutable"); 50 | 51 | if (typeof (global) !== "undefined") { 52 | console.log("In node"); 53 | if (global.process.env.running_under_istanbul) { 54 | // Handle coverage tool that may modify the source code. 55 | iassign.disableExtraStatementCheck = true; 56 | } 57 | } 58 | 59 | if (typeof (window) !== "undefined") { 60 | console.log("In browser"); 61 | if (window.__coverage__) { 62 | iassign.disableExtraStatementCheck = true; 63 | } 64 | } 65 | 66 | // Test if arrow function is supported 67 | if (typeof Map === "undefined") 68 | return; 69 | 70 | if (typeof Set === "undefined") 71 | return; 72 | 73 | describe("Test 4", function () { 74 | beforeEach(function () { 75 | }); 76 | it("Issue 12: Support ES6 Map", function () { 77 | 78 | var map1 = new Map(); 79 | map1.set("a", "value a"); 80 | 81 | iassign.setOption({ 82 | copyFunc: function (value, propName) { 83 | if (value instanceof Map) { 84 | return new Map(value); 85 | } 86 | } 87 | }); 88 | 89 | var map2 = iassign( 90 | map1, 91 | function (m) { m.set(1, 'first'); return m; } 92 | ); 93 | 94 | expect(map2).not.toBe(map1); 95 | 96 | expect(map2.size).toBe(2); 97 | expect(map2.get("a")).toBe("value a"); 98 | expect(map2.get(1)).toBe("first"); 99 | expect(map1.size).toBe(1); 100 | expect(map1.get("a")).toBe("value a"); 101 | expect(map1.get(1)).toBe(undefined); 102 | 103 | var map3 = iassign( 104 | map2, 105 | function (m) { m.set(2, 'second'); return m; } 106 | ); 107 | 108 | expect(map3).not.toBe(map2); 109 | expect(map3).not.toBe(map1); 110 | 111 | expect(map3.size).toBe(3); 112 | expect(map3.get("a")).toBe("value a"); 113 | expect(map3.get(1)).toBe("first"); 114 | expect(map3.get(2)).toBe("second"); 115 | 116 | expect(map2.size).toBe(2); 117 | expect(map2.get("a")).toBe("value a"); 118 | expect(map2.get(1)).toBe("first"); 119 | expect(map2.get(2)).toBe(undefined); 120 | 121 | expect(map1.size).toBe(1); 122 | expect(map1.get("a")).toBe("value a"); 123 | expect(map1.get(1)).toBe(undefined); 124 | expect(map1.get(2)).toBe(undefined); 125 | }); 126 | 127 | it("Issue 12: Support ES6 Map in nested object", function () { 128 | var o1 = { a: { b: { c: [[{ d: new Map(), e: 12 }], [{ d: 21, e: 22 }], [{ d: 31, e: 32 }]] } } }; 129 | o1.a.b.c[0][0].d.set("a", "value a"); 130 | deepFreeze(o1); 131 | 132 | iassign.setOption({ 133 | copyFunc: function (value, propName) { 134 | if (value instanceof Map) { 135 | return new Map(value); 136 | } 137 | } 138 | }); 139 | 140 | var o2 = iassign( 141 | o1, 142 | function (o) { return o.a.b.c[0][0].d }, 143 | function (d) { d.set(1, "first"); return d; }); 144 | 145 | expect(o2).not.toBe(o1); 146 | expect(o2.a).not.toBe(o1.a); 147 | expect(o2.a.b).not.toBe(o1.a.b); 148 | expect(o2.a.b.c).not.toBe(o1.a.b.c); 149 | expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]); 150 | expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]); 151 | expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d); 152 | 153 | expect(o2.a.b.c[0][0].d.size).toBe(2); 154 | expect(o2.a.b.c[0][0].d.get("a")).toBe("value a"); 155 | expect(o2.a.b.c[0][0].d.get(1)).toBe("first"); 156 | expect(o1.a.b.c[0][0].d.size).toBe(1); 157 | expect(o1.a.b.c[0][0].d.get("a")).toBe("value a"); 158 | expect(o1.a.b.c[0][0].d.get(1)).toBe(undefined); 159 | 160 | var o3 = iassign( 161 | o2, 162 | function (o) { return o.a.b.c[0][0].d }, 163 | function (d) { d.set(2, "second"); return d; }); 164 | 165 | expect(o3).not.toBe(o2); 166 | expect(o3.a).not.toBe(o2.a); 167 | expect(o3.a.b).not.toBe(o2.a.b); 168 | expect(o3.a.b.c).not.toBe(o2.a.b.c); 169 | expect(o3.a.b.c[0]).not.toBe(o2.a.b.c[0]); 170 | expect(o3.a.b.c[0][0]).not.toBe(o2.a.b.c[0][0]); 171 | expect(o3.a.b.c[0][0].d).not.toBe(o2.a.b.c[0][0].d); 172 | 173 | expect(o3.a.b.c[0][0].d.size).toBe(3); 174 | expect(o3.a.b.c[0][0].d.get("a")).toBe("value a"); 175 | expect(o3.a.b.c[0][0].d.get(1)).toBe("first"); 176 | expect(o3.a.b.c[0][0].d.get(2)).toBe("second"); 177 | expect(o2.a.b.c[0][0].d.size).toBe(2); 178 | expect(o2.a.b.c[0][0].d.get("a")).toBe("value a"); 179 | expect(o2.a.b.c[0][0].d.get(1)).toBe("first"); 180 | expect(o2.a.b.c[0][0].d.get(2)).toBe(undefined); 181 | }); 182 | 183 | it("Issue 12: Support ES6 Set", function () { 184 | 185 | var set1 = new Set(); 186 | set1.add("a"); 187 | 188 | iassign.setOption({ 189 | copyFunc: function (value, propName) { 190 | if (value instanceof Set) { 191 | return new Set(value); 192 | } 193 | } 194 | }); 195 | 196 | var set2 = iassign( 197 | set1, 198 | function (m) { m.add(1); return m; } 199 | ); 200 | 201 | expect(set2).not.toBe(set1); 202 | 203 | expect(set2.size).toBe(2); 204 | expect(set2.has("a")).toBe(true); 205 | expect(set2.has(1)).toBe(true); 206 | 207 | expect(set1.size).toBe(1); 208 | expect(set1.has("a")).toBe(true); 209 | expect(set1.has(1)).toBe(false); 210 | 211 | var set3 = iassign( 212 | set2, 213 | function(m) { m.add(2); return m; } 214 | ); 215 | 216 | expect(set3).not.toBe(set2); 217 | expect(set3).not.toBe(set1); 218 | 219 | expect(set3.size).toBe(3); 220 | expect(set3.has("a")).toBe(true); 221 | expect(set3.has(1)).toBe(true); 222 | expect(set3.has(2)).toBe(true); 223 | 224 | expect(set2.size).toBe(2); 225 | expect(set2.has("a")).toBe(true); 226 | expect(set2.has(1)).toBe(true); 227 | expect(set2.has(2)).toBe(false); 228 | 229 | expect(set1.size).toBe(1); 230 | expect(set1.has("a")).toBe(true); 231 | expect(set1.has(1)).toBe(false); 232 | expect(set1.has(2)).toBe(false); 233 | }); 234 | 235 | it("Issue 12: Support ES6 Set in nested object", function () { 236 | var o1 = { a: { b: { c: [[{ d: new Set(), e: 12 }], [{ d: 21, e: 22 }], [{ d: 31, e: 32 }]] } } }; 237 | o1.a.b.c[0][0].d.add("a"); 238 | deepFreeze(o1); 239 | 240 | iassign.setOption({ 241 | copyFunc: function (value, propName) { 242 | if (value instanceof Set) { 243 | return new Set(value); 244 | } 245 | } 246 | }); 247 | 248 | var o2 = iassign( 249 | o1, 250 | function (o) { return o.a.b.c[0][0].d }, 251 | function (d) { d.add(1); return d; }); 252 | 253 | expect(o2).not.toBe(o1); 254 | expect(o2.a).not.toBe(o1.a); 255 | expect(o2.a.b).not.toBe(o1.a.b); 256 | expect(o2.a.b.c).not.toBe(o1.a.b.c); 257 | expect(o2.a.b.c[0]).not.toBe(o1.a.b.c[0]); 258 | expect(o2.a.b.c[0][0]).not.toBe(o1.a.b.c[0][0]); 259 | expect(o2.a.b.c[0][0].d).not.toBe(o1.a.b.c[0][0].d); 260 | 261 | expect(o2.a.b.c[0][0].d.size).toBe(2); 262 | expect(o2.a.b.c[0][0].d.has("a")).toBe(true); 263 | expect(o2.a.b.c[0][0].d.has(1)).toBe(true); 264 | expect(o1.a.b.c[0][0].d.size).toBe(1); 265 | expect(o1.a.b.c[0][0].d.has("a")).toBe(true); 266 | expect(o1.a.b.c[0][0].d.has(1)).toBe(false); 267 | 268 | var o3 = iassign( 269 | o2, 270 | function (o) { return o.a.b.c[0][0].d }, 271 | function (d) { d.add(2); return d; }); 272 | 273 | expect(o3).not.toBe(o2); 274 | expect(o3.a).not.toBe(o2.a); 275 | expect(o3.a.b).not.toBe(o2.a.b); 276 | expect(o3.a.b.c).not.toBe(o2.a.b.c); 277 | expect(o3.a.b.c[0]).not.toBe(o2.a.b.c[0]); 278 | expect(o3.a.b.c[0][0]).not.toBe(o2.a.b.c[0][0]); 279 | expect(o3.a.b.c[0][0].d).not.toBe(o2.a.b.c[0][0].d); 280 | 281 | expect(o3.a.b.c[0][0].d.size).toBe(3); 282 | expect(o3.a.b.c[0][0].d.has("a")).toBe(true); 283 | expect(o3.a.b.c[0][0].d.has(1)).toBe(true); 284 | expect(o3.a.b.c[0][0].d.has(2)).toBe(true); 285 | expect(o2.a.b.c[0][0].d.size).toBe(2); 286 | expect(o2.a.b.c[0][0].d.has("a")).toBe(true); 287 | expect(o2.a.b.c[0][0].d.has(1)).toBe(true); 288 | expect(o2.a.b.c[0][0].d.has(2)).toBe(false); 289 | }); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /typings/jasmine/jasmine.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Jasmine 2.2 2 | // Project: http://jasmine.github.io/ 3 | // Definitions by: Boris Yankov , Theodore Brown , David Pärsson 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | 7 | // For ddescribe / iit use : https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/karma-jasmine/karma-jasmine.d.ts 8 | 9 | declare function describe(description: string, specDefinitions: () => void): void; 10 | declare function fdescribe(description: string, specDefinitions: () => void): void; 11 | declare function xdescribe(description: string, specDefinitions: () => void): void; 12 | 13 | declare function it(expectation: string, assertion?: () => void, timeout?: number): void; 14 | declare function it(expectation: string, assertion?: (done: DoneFn) => void, timeout?: number): void; 15 | declare function fit(expectation: string, assertion?: () => void, timeout?: number): void; 16 | declare function fit(expectation: string, assertion?: (done: DoneFn) => void, timeout?: number): void; 17 | declare function xit(expectation: string, assertion?: () => void, timeout?: number): void; 18 | declare function xit(expectation: string, assertion?: (done: DoneFn) => void, timeout?: number): void; 19 | 20 | /** If you call the function pending anywhere in the spec body, no matter the expectations, the spec will be marked pending. */ 21 | declare function pending(reason?: string): void; 22 | 23 | declare function beforeEach(action: () => void, timeout?: number): void; 24 | declare function beforeEach(action: (done: DoneFn) => void, timeout?: number): void; 25 | declare function afterEach(action: () => void, timeout?: number): void; 26 | declare function afterEach(action: (done: DoneFn) => void, timeout?: number): void; 27 | 28 | declare function beforeAll(action: () => void, timeout?: number): void; 29 | declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void; 30 | declare function afterAll(action: () => void, timeout?: number): void; 31 | declare function afterAll(action: (done: DoneFn) => void, timeout?: number): void; 32 | 33 | declare function expect(spy: Function): jasmine.Matchers; 34 | declare function expect(actual: any): jasmine.Matchers; 35 | 36 | declare function fail(e?: any): void; 37 | /** Action method that should be called when the async work is complete */ 38 | interface DoneFn extends Function { 39 | (): void; 40 | 41 | /** fails the spec and indicates that it has completed. If the message is an Error, Error.message is used */ 42 | fail: (message?: Error|string) => void; 43 | } 44 | 45 | declare function spyOn(object: any, method: string): jasmine.Spy; 46 | 47 | declare function runs(asyncMethod: Function): void; 48 | declare function waitsFor(latchMethod: () => boolean, failureMessage?: string, timeout?: number): void; 49 | declare function waits(timeout?: number): void; 50 | 51 | declare namespace jasmine { 52 | 53 | var clock: () => Clock; 54 | 55 | function any(aclass: any): Any; 56 | function anything(): Any; 57 | function arrayContaining(sample: any[]): ArrayContaining; 58 | function objectContaining(sample: any): ObjectContaining; 59 | function createSpy(name: string, originalFn?: Function): Spy; 60 | function createSpyObj(baseName: string, methodNames: any[]): any; 61 | function createSpyObj(baseName: string, methodNames: any[]): T; 62 | function pp(value: any): string; 63 | function getEnv(): Env; 64 | function addCustomEqualityTester(equalityTester: CustomEqualityTester): void; 65 | function addMatchers(matchers: CustomMatcherFactories): void; 66 | function stringMatching(str: string): Any; 67 | function stringMatching(str: RegExp): Any; 68 | 69 | interface Any { 70 | 71 | new (expectedClass: any): any; 72 | 73 | jasmineMatches(other: any): boolean; 74 | jasmineToString(): string; 75 | } 76 | 77 | // taken from TypeScript lib.core.es6.d.ts, applicable to CustomMatchers.contains() 78 | interface ArrayLike { 79 | length: number; 80 | [n: number]: T; 81 | } 82 | 83 | interface ArrayContaining { 84 | new (sample: any[]): any; 85 | 86 | asymmetricMatch(other: any): boolean; 87 | jasmineToString(): string; 88 | } 89 | 90 | interface ObjectContaining { 91 | new (sample: any): any; 92 | 93 | jasmineMatches(other: any, mismatchKeys: any[], mismatchValues: any[]): boolean; 94 | jasmineToString(): string; 95 | } 96 | 97 | interface Block { 98 | 99 | new (env: Env, func: SpecFunction, spec: Spec): any; 100 | 101 | execute(onComplete: () => void): void; 102 | } 103 | 104 | interface WaitsBlock extends Block { 105 | new (env: Env, timeout: number, spec: Spec): any; 106 | } 107 | 108 | interface WaitsForBlock extends Block { 109 | new (env: Env, timeout: number, latchFunction: SpecFunction, message: string, spec: Spec): any; 110 | } 111 | 112 | interface Clock { 113 | install(): void; 114 | uninstall(): void; 115 | /** Calls to any registered callback are triggered when the clock is ticked forward via the jasmine.clock().tick function, which takes a number of milliseconds. */ 116 | tick(ms: number): void; 117 | mockDate(date?: Date): void; 118 | } 119 | 120 | interface CustomEqualityTester { 121 | (first: any, second: any): boolean; 122 | } 123 | 124 | interface CustomMatcher { 125 | compare(actual: T, expected: T): CustomMatcherResult; 126 | compare(actual: any, expected: any): CustomMatcherResult; 127 | } 128 | 129 | interface CustomMatcherFactory { 130 | (util: MatchersUtil, customEqualityTesters: Array): CustomMatcher; 131 | } 132 | 133 | interface CustomMatcherFactories { 134 | [index: string]: CustomMatcherFactory; 135 | } 136 | 137 | interface CustomMatcherResult { 138 | pass: boolean; 139 | message?: string; 140 | } 141 | 142 | interface MatchersUtil { 143 | equals(a: any, b: any, customTesters?: Array): boolean; 144 | contains(haystack: ArrayLike | string, needle: any, customTesters?: Array): boolean; 145 | buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: Array): string; 146 | } 147 | 148 | interface Env { 149 | setTimeout: any; 150 | clearTimeout: void; 151 | setInterval: any; 152 | clearInterval: void; 153 | updateInterval: number; 154 | 155 | currentSpec: Spec; 156 | 157 | matchersClass: Matchers; 158 | 159 | version(): any; 160 | versionString(): string; 161 | nextSpecId(): number; 162 | addReporter(reporter: Reporter): void; 163 | execute(): void; 164 | describe(description: string, specDefinitions: () => void): Suite; 165 | // ddescribe(description: string, specDefinitions: () => void): Suite; Not a part of jasmine. Angular team adds these 166 | beforeEach(beforeEachFunction: () => void): void; 167 | beforeAll(beforeAllFunction: () => void): void; 168 | currentRunner(): Runner; 169 | afterEach(afterEachFunction: () => void): void; 170 | afterAll(afterAllFunction: () => void): void; 171 | xdescribe(desc: string, specDefinitions: () => void): XSuite; 172 | it(description: string, func: () => void): Spec; 173 | // iit(description: string, func: () => void): Spec; Not a part of jasmine. Angular team adds these 174 | xit(desc: string, func: () => void): XSpec; 175 | compareRegExps_(a: RegExp, b: RegExp, mismatchKeys: string[], mismatchValues: string[]): boolean; 176 | compareObjects_(a: any, b: any, mismatchKeys: string[], mismatchValues: string[]): boolean; 177 | equals_(a: any, b: any, mismatchKeys: string[], mismatchValues: string[]): boolean; 178 | contains_(haystack: any, needle: any): boolean; 179 | addCustomEqualityTester(equalityTester: CustomEqualityTester): void; 180 | addMatchers(matchers: CustomMatcherFactories): void; 181 | specFilter(spec: Spec): boolean; 182 | } 183 | 184 | interface FakeTimer { 185 | 186 | new (): any; 187 | 188 | reset(): void; 189 | tick(millis: number): void; 190 | runFunctionsWithinRange(oldMillis: number, nowMillis: number): void; 191 | scheduleFunction(timeoutKey: any, funcToCall: () => void, millis: number, recurring: boolean): void; 192 | } 193 | 194 | interface HtmlReporter { 195 | new (): any; 196 | } 197 | 198 | interface HtmlSpecFilter { 199 | new (): any; 200 | } 201 | 202 | interface Result { 203 | type: string; 204 | } 205 | 206 | interface NestedResults extends Result { 207 | description: string; 208 | 209 | totalCount: number; 210 | passedCount: number; 211 | failedCount: number; 212 | 213 | skipped: boolean; 214 | 215 | rollupCounts(result: NestedResults): void; 216 | log(values: any): void; 217 | getItems(): Result[]; 218 | addResult(result: Result): void; 219 | passed(): boolean; 220 | } 221 | 222 | interface MessageResult extends Result { 223 | values: any; 224 | trace: Trace; 225 | } 226 | 227 | interface ExpectationResult extends Result { 228 | matcherName: string; 229 | passed(): boolean; 230 | expected: any; 231 | actual: any; 232 | message: string; 233 | trace: Trace; 234 | } 235 | 236 | interface Trace { 237 | name: string; 238 | message: string; 239 | stack: any; 240 | } 241 | 242 | interface PrettyPrinter { 243 | 244 | new (): any; 245 | 246 | format(value: any): void; 247 | iterateObject(obj: any, fn: (property: string, isGetter: boolean) => void): void; 248 | emitScalar(value: any): void; 249 | emitString(value: string): void; 250 | emitArray(array: any[]): void; 251 | emitObject(obj: any): void; 252 | append(value: any): void; 253 | } 254 | 255 | interface StringPrettyPrinter extends PrettyPrinter { 256 | } 257 | 258 | interface Queue { 259 | 260 | new (env: any): any; 261 | 262 | env: Env; 263 | ensured: boolean[]; 264 | blocks: Block[]; 265 | running: boolean; 266 | index: number; 267 | offset: number; 268 | abort: boolean; 269 | 270 | addBefore(block: Block, ensure?: boolean): void; 271 | add(block: any, ensure?: boolean): void; 272 | insertNext(block: any, ensure?: boolean): void; 273 | start(onComplete?: () => void): void; 274 | isRunning(): boolean; 275 | next_(): void; 276 | results(): NestedResults; 277 | } 278 | 279 | interface Matchers { 280 | 281 | new (env: Env, actual: any, spec: Env, isNot?: boolean): any; 282 | 283 | env: Env; 284 | actual: any; 285 | spec: Env; 286 | isNot?: boolean; 287 | message(): any; 288 | 289 | toBe(expected: any, expectationFailOutput?: any): boolean; 290 | toEqual(expected: any, expectationFailOutput?: any): boolean; 291 | toMatch(expected: string | RegExp, expectationFailOutput?: any): boolean; 292 | toBeDefined(expectationFailOutput?: any): boolean; 293 | toBeUndefined(expectationFailOutput?: any): boolean; 294 | toBeNull(expectationFailOutput?: any): boolean; 295 | toBeNaN(): boolean; 296 | toBeTruthy(expectationFailOutput?: any): boolean; 297 | toBeFalsy(expectationFailOutput?: any): boolean; 298 | toHaveBeenCalled(): boolean; 299 | toHaveBeenCalledWith(...params: any[]): boolean; 300 | toHaveBeenCalledTimes(expected: number): boolean; 301 | toContain(expected: any, expectationFailOutput?: any): boolean; 302 | toBeLessThan(expected: number, expectationFailOutput?: any): boolean; 303 | toBeGreaterThan(expected: number, expectationFailOutput?: any): boolean; 304 | toBeCloseTo(expected: number, precision: any, expectationFailOutput?: any): boolean; 305 | toThrow(expected?: any): boolean; 306 | toThrowError(message?: string | RegExp): boolean; 307 | toThrowError(expected?: new (...args: any[]) => Error, message?: string | RegExp): boolean; 308 | not: Matchers; 309 | 310 | Any: Any; 311 | } 312 | 313 | interface Reporter { 314 | reportRunnerStarting(runner: Runner): void; 315 | reportRunnerResults(runner: Runner): void; 316 | reportSuiteResults(suite: Suite): void; 317 | reportSpecStarting(spec: Spec): void; 318 | reportSpecResults(spec: Spec): void; 319 | log(str: string): void; 320 | } 321 | 322 | interface MultiReporter extends Reporter { 323 | addReporter(reporter: Reporter): void; 324 | } 325 | 326 | interface Runner { 327 | 328 | new (env: Env): any; 329 | 330 | execute(): void; 331 | beforeEach(beforeEachFunction: SpecFunction): void; 332 | afterEach(afterEachFunction: SpecFunction): void; 333 | beforeAll(beforeAllFunction: SpecFunction): void; 334 | afterAll(afterAllFunction: SpecFunction): void; 335 | finishCallback(): void; 336 | addSuite(suite: Suite): void; 337 | add(block: Block): void; 338 | specs(): Spec[]; 339 | suites(): Suite[]; 340 | topLevelSuites(): Suite[]; 341 | results(): NestedResults; 342 | } 343 | 344 | interface SpecFunction { 345 | (spec?: Spec): void; 346 | } 347 | 348 | interface SuiteOrSpec { 349 | id: number; 350 | env: Env; 351 | description: string; 352 | queue: Queue; 353 | } 354 | 355 | interface Spec extends SuiteOrSpec { 356 | 357 | new (env: Env, suite: Suite, description: string): any; 358 | 359 | suite: Suite; 360 | 361 | afterCallbacks: SpecFunction[]; 362 | spies_: Spy[]; 363 | 364 | results_: NestedResults; 365 | matchersClass: Matchers; 366 | 367 | getFullName(): string; 368 | results(): NestedResults; 369 | log(arguments: any): any; 370 | runs(func: SpecFunction): Spec; 371 | addToQueue(block: Block): void; 372 | addMatcherResult(result: Result): void; 373 | expect(actual: any): any; 374 | waits(timeout: number): Spec; 375 | waitsFor(latchFunction: SpecFunction, timeoutMessage?: string, timeout?: number): Spec; 376 | fail(e?: any): void; 377 | getMatchersClass_(): Matchers; 378 | addMatchers(matchersPrototype: CustomMatcherFactories): void; 379 | finishCallback(): void; 380 | finish(onComplete?: () => void): void; 381 | after(doAfter: SpecFunction): void; 382 | execute(onComplete?: () => void): any; 383 | addBeforesAndAftersToQueue(): void; 384 | explodes(): void; 385 | spyOn(obj: any, methodName: string, ignoreMethodDoesntExist: boolean): Spy; 386 | removeAllSpies(): void; 387 | } 388 | 389 | interface XSpec { 390 | id: number; 391 | runs(): void; 392 | } 393 | 394 | interface Suite extends SuiteOrSpec { 395 | 396 | new (env: Env, description: string, specDefinitions: () => void, parentSuite: Suite): any; 397 | 398 | parentSuite: Suite; 399 | 400 | getFullName(): string; 401 | finish(onComplete?: () => void): void; 402 | beforeEach(beforeEachFunction: SpecFunction): void; 403 | afterEach(afterEachFunction: SpecFunction): void; 404 | beforeAll(beforeAllFunction: SpecFunction): void; 405 | afterAll(afterAllFunction: SpecFunction): void; 406 | results(): NestedResults; 407 | add(suiteOrSpec: SuiteOrSpec): void; 408 | specs(): Spec[]; 409 | suites(): Suite[]; 410 | children(): any[]; 411 | execute(onComplete?: () => void): void; 412 | } 413 | 414 | interface XSuite { 415 | execute(): void; 416 | } 417 | 418 | interface Spy { 419 | (...params: any[]): any; 420 | 421 | identity: string; 422 | and: SpyAnd; 423 | calls: Calls; 424 | mostRecentCall: { args: any[]; }; 425 | argsForCall: any[]; 426 | wasCalled: boolean; 427 | } 428 | 429 | interface SpyAnd { 430 | /** By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation. */ 431 | callThrough(): Spy; 432 | /** By chaining the spy with and.returnValue, all calls to the function will return a specific value. */ 433 | returnValue(val: any): Spy; 434 | /** By chaining the spy with and.returnValues, all calls to the function will return specific values in order until it reaches the end of the return values list. */ 435 | returnValues(...values: any[]): Spy; 436 | /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied function. */ 437 | callFake(fn: Function): Spy; 438 | /** By chaining the spy with and.throwError, all calls to the spy will throw the specified value. */ 439 | throwError(msg: string): Spy; 440 | /** When a calling strategy is used for a spy, the original stubbing behavior can be returned at any time with and.stub. */ 441 | stub(): Spy; 442 | } 443 | 444 | interface Calls { 445 | /** By chaining the spy with calls.any(), will return false if the spy has not been called at all, and then true once at least one call happens. **/ 446 | any(): boolean; 447 | /** By chaining the spy with calls.count(), will return the number of times the spy was called **/ 448 | count(): number; 449 | /** By chaining the spy with calls.argsFor(), will return the arguments passed to call number index **/ 450 | argsFor(index: number): any[]; 451 | /** By chaining the spy with calls.allArgs(), will return the arguments to all calls **/ 452 | allArgs(): any[]; 453 | /** By chaining the spy with calls.all(), will return the context (the this) and arguments passed all calls **/ 454 | all(): CallInfo[]; 455 | /** By chaining the spy with calls.mostRecent(), will return the context (the this) and arguments for the most recent call **/ 456 | mostRecent(): CallInfo; 457 | /** By chaining the spy with calls.first(), will return the context (the this) and arguments for the first call **/ 458 | first(): CallInfo; 459 | /** By chaining the spy with calls.reset(), will clears all tracking for a spy **/ 460 | reset(): void; 461 | } 462 | 463 | interface CallInfo { 464 | /** The context (the this) for the call */ 465 | object: any; 466 | /** All arguments passed to the call */ 467 | args: any[]; 468 | /** The return value of the call */ 469 | returnValue: any; 470 | } 471 | 472 | interface Util { 473 | inherit(childClass: Function, parentClass: Function): any; 474 | formatException(e: any): any; 475 | htmlEscape(str: string): string; 476 | argsToArray(args: any): any; 477 | extend(destination: any, source: any): any; 478 | } 479 | 480 | interface JsApiReporter extends Reporter { 481 | 482 | started: boolean; 483 | finished: boolean; 484 | result: any; 485 | messages: any; 486 | 487 | new (): any; 488 | 489 | suites(): Suite[]; 490 | summarize_(suiteOrSpec: SuiteOrSpec): any; 491 | results(): any; 492 | resultsForSpec(specId: any): any; 493 | log(str: any): any; 494 | resultsForSpecs(specIds: any): any; 495 | summarizeResult_(result: any): any; 496 | } 497 | 498 | interface Jasmine { 499 | Spec: Spec; 500 | clock: Clock; 501 | util: Util; 502 | } 503 | 504 | export var HtmlReporter: HtmlReporter; 505 | export var HtmlSpecFilter: HtmlSpecFilter; 506 | export var DEFAULT_TIMEOUT_INTERVAL: number; 507 | } 508 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # immutable-assign (iassign.js) 2 | 3 | Lightweight immutable helper that allows you to continue working with POJO (Plain Old JavaScript Object), and supports full TypeScript type checking for nested objects. 4 | 5 | [![NPM version][3]][4] [![Build Status][1]][2] [![coverage status][5]][6] [![size][9]][10] 6 | 7 | [![Sauce Test Status][7]][8] 8 | 9 | 10 |

11 | 12 | Demo 13 | 14 |

15 | 16 | This library is trying to solve the following problems: 17 | 18 | * Most immutable JavaScript libraries try to encapsulate the data and provide proprietary APIs to work with the data. They are more verbose than normal JavaScript syntax. E.g., `map1.get('b')` vs `map1.b`, `nested2.getIn(['a', 'b', 'd'])` vs `nested2.a.b.d`, etc. 19 | * Encapsulated data is no more POJO, therefore cannot be easily used with other libraries, e.g., lodash, underscore, etc. 20 | * Most immutable libraries leak themselves throughout your entire application (including view components), however, they should have been encapsulated at the place where updates happen (e.g., Redux reducers). This is also a pain when you need to change to another immutable library that has its own APIs. 21 | * [seamless-immutable](https://github.com/rtfeldman/seamless-immutable) address some of the above issues when reading the properties, but still use verbose APIs to write properties. 22 | * [Immutability Helpers](https://facebook.github.io/react/docs/update.html) allows us to work with POJO, but it has still introduced some magic keywords, such as $set, $push, etc. 23 | * In addition, we lost TypeScript type checking. E.g., when calling `nested2.getIn(["a", "b", "c"])`, TypeScript won't be able to warn me if I changed property "c" to "d". 24 | 25 | This library is an alternative to [Immutable.js](https://facebook.github.io/immutable-js/), it has only one method **iassign()**, which accept a POJO object and return you a new POJO object with specific property updated. However, since it works with other libraries such as lodash (refer to [example 4](#example-4-work-with-3rd-party-libraries-eg-lodash)), it provides all the functionalities you need plus immutability. 26 | 27 | * I have added some options to freeze input and output using [deep-freeze](https://github.com/substack/deep-freeze), which can be used in development to make sure they don't change unintentionally by us or the 3rd party libraries. 28 | 29 | This library will leave your POJO objects completely untouched (except the optional deep-freeze), it does not wrap around nor add any methods/properties to your POJO objects. 30 | 31 | This library works in JavaScript and it works really well with TypeScript, because of its [generic type argument inference](https://www.typescriptlang.org/docs/handbook/generics.html); and since you are working with POJO (not the wrapper objects), you can utilize the full power of TypeScript: IntelliSense, type checking and refactoring, etc. 32 | 33 | ## Performance 34 | 35 | Performance of this library should be comparable to [Immutable.js](https://facebook.github.io/immutable-js/), because read operations will always occur more than write operations. When using this library, all your react components can read object properties directly. E.g., you can use `` in your components, instead of ``. In addition, `shouldComponentUpdate()` can compare POJO objects without knowing about the immutable libraries, e.g., `return this.props.userInfo.orders !== nextProps.userInfos.orders`. I.e., the more read operations you have, the more it will outperform [Immutable.js](https://facebook.github.io/immutable-js/). Following are the benchmarks for multiple immutable libraries (assuming the read to write ratio is 5 to 1): 36 | 37 | ``` 38 | $ npm run benchmarks 39 | 40 | Mutable 41 | Total elapsed = 57 ms (read) + 41 ms (write) = 98 ms. 42 | 43 | Immutable (immutable.js) 44 | Total elapsed = 254 ms (read) + 847 ms (write) = 1101 ms. 45 | 46 | Immutable (Object.assign) 47 | Total elapsed = 66 ms (read) + 1339 ms (write) = 1405 ms. 48 | 49 | Immutable (immutable-assign) 50 | Total elapsed = 77 ms (read) + 2265 ms (write) = 2342 ms. 51 | 52 | Immutable (immer setAutoFreeze(false)) 53 | Total elapsed = 65 ms (read) + 4706 ms (write) = 4771 ms. 54 | 55 | Immutable (seamless-immutable production) 56 | Total elapsed = 73 ms (read) + 29688 ms (write) = 29761 ms. 57 | ``` 58 | 59 | Full performance test results and test script can be found at benchmarks. 60 | 61 | ## Install with npm 62 | 63 | ```sh 64 | npm install immutable-assign 65 | # or 66 | yarn add immutable-assign 67 | ``` 68 | 69 |
70 | 71 | ### Example 1: Update 1st level object properties, it has skipped the getProp parameter 72 | 73 | ```javascript 74 | const iassign = require("immutable-assign"); 75 | 76 | // Deep freeze both input and output, can be used in development to make sure they don't change. 77 | iassign.setOption({ freeze: true }); 78 | 79 | const map1 = { a:1, b:2, c:3 }; 80 | 81 | // 1: Calling iassign() to update map1.b, using overload 1 82 | const map2 = iassign( 83 | map1, 84 | function (m) { m.b = 50; return m; } 85 | ); 86 | 87 | // map2 = { a:1, b: 50, c:3 } 88 | // map2 !== map1 89 | 90 | ``` 91 | 92 |
93 | 94 | ### Example 2: Update 1st level list/array elements, it has skipped the getProp parameter 95 | 96 | ```javascript 97 | const iassign = require("immutable-assign"); 98 | 99 | const list1 = [1, 2]; 100 | 101 | 102 | // 2.1: Calling iassign() to push items to list1, using overload 1 103 | const list2 = iassign( 104 | list1, 105 | function (l) { l.push(3, 4, 5); return l; } 106 | ); 107 | 108 | // list2 = [1, 2, 3, 4, 5] 109 | // list2 !== list1 110 | 111 | 112 | // 2.2: Calling iassign() to unshift item to list2, using overload 1 113 | const list3 = iassign( 114 | list2, 115 | function (l) { l.unshift(0); return l; } 116 | ); 117 | 118 | // list3 = [0, 1, 2, 3, 4, 5] 119 | // list3 !== list2 120 | 121 | 122 | // 2.3, Calling iassign() to concat list1, list2 and list3, using overload 1 123 | const list4 = iassign( 124 | list1, 125 | function (l) { return l.concat(list2, list3); } 126 | ); 127 | 128 | // list4 = [1, 2, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5] 129 | // list4 !== list1 130 | 131 | 132 | // 2.4, Calling iassign() to concat sort list4, using overload 1 133 | const list5 = iassign( 134 | list4, 135 | function (l) { return l.sort(); } 136 | ); 137 | 138 | // list5 = [0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5] 139 | // list5 !== list4 140 | 141 | ``` 142 | 143 |
144 | 145 | ### Example 3: Update nested level object properties 146 | 147 | ```javascript 148 | const iassign = require("immutable-assign"); 149 | 150 | const nested1 = { a:{ b:{ c:[3, 4, 5] } } }; 151 | 152 | 153 | // 3.1: Calling iassign() to assign d to nested1.a.b 154 | const nested2 = iassign( 155 | nested1, 156 | function (n) { return n.a.b; }, 157 | function (b) { b.d = 6; return b; } 158 | ); 159 | 160 | // nested2 = { a:{ b:{ c:[3, 4, 5], d: 6 } } } 161 | // nested2 !== nested1 162 | 163 | 164 | // 3.2: Calling iassign() to increment nested2.a.b.d 165 | const nested3 = iassign( 166 | nested2, 167 | function (n) { return n.a.b.d; }, 168 | function (d) { return d + 1; } 169 | ); 170 | 171 | // nested3 = { a:{ b:{ c:[3, 4, 5], d: 7 } } } 172 | // nested3 !== nested2 173 | 174 | 175 | // 3.3: Calling iassign() to push item to nested3.a.b.c 176 | const nested4 = iassign( 177 | nested3, 178 | function (n) { return n.a.b.c; }, 179 | function (c) { c.push(6); return c; } 180 | ); 181 | 182 | // nested4 = { a:{ b:{ c:[3, 4, 5, 6], d: 7 } } } 183 | // nested4 !== nested3 184 | 185 | ``` 186 | 187 |
188 | 189 | ### Example 4: Work with 3rd party libraries, e.g., lodash 190 | 191 | ```javascript 192 | const iassign = require("immutable-assign"); 193 | const _ = require("lodash"); 194 | 195 | const nested1 = { a: { b: { c: [1, 2, 3] } } }; 196 | 197 | 198 | // 4.1: Calling iassign() and _.map() to increment to every item in "c" array 199 | const nested2 = iassign( 200 | nested1, 201 | function (n) { return n.a.b.c; }, 202 | function (c) { 203 | return _.map(c, function (i) { return i + 1; }); 204 | } 205 | ); 206 | 207 | // nested2 = { a: { b: { c: [2, 3, 4] } } }; 208 | // nested2 !== nested1 209 | 210 | 211 | // 4.2: Calling iassign() and _.flatMap() 212 | const nested3 = iassign( 213 | nested2, 214 | function (n) { return n.a.b.c; }, 215 | function (c) { 216 | return _.flatMap(c, function (i) { return [i, i]; }); 217 | } 218 | ); 219 | 220 | // nested3 = { a: { b: { c: [2, 2, 3, 3, 4, 4] } } }; 221 | // nested3 !== nested2 222 | 223 | ``` 224 | 225 |
226 | 227 | ### Example 5: Update nested object 228 | 229 | ```javascript 230 | const iassign = require("immutable-assign"); 231 | 232 | const o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }]], c2: {} }, b2: {} }, a2: {} }; 233 | 234 | // 5: Calling iassign() to increment o1.a.b.c[0][0].d 235 | const o2 = iassign( 236 | o1, 237 | function (o) { return o.a.b.c[0][0]; }, 238 | function (ci) { ci.d++; return ci; } 239 | ); 240 | ``` 241 | 242 |
243 | 244 | ### Example 6: Update nested array 245 | 246 | ```javascript 247 | const iassign = require("immutable-assign"); 248 | 249 | const o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }]], c2: {} }, b2: {} }, a2: {} }; 250 | 251 | // 6: Calling iassign() to push new item to o1.a.b.c[1] 252 | const o2 = iassign( 253 | o1, 254 | function (o) { return o.a.b.c[1]; }, 255 | function (c) { c.push(101); return c; } 256 | ); 257 | ``` 258 | 259 |
260 | 261 | ### Example 7: Update nested object, referring to external context. 262 | 263 | ```javascript 264 | const iassign = require("immutable-assign"); 265 | 266 | const o1 = { a: { b: { c: [{ d: 11, e: 12 }, { d: 21, e: 22 }] } } }; 267 | 268 | // 7: Calling iassign() to push increment to o1.a.b.c[0].d 269 | const external = { a: 0 }; 270 | 271 | const o2 = iassign( 272 | o1, 273 | function (o, ctx) { return o.a.b.c[ctx.external.a]; }, 274 | function (ci) { ci.d++; return ci; }, 275 | { external: external } 276 | ); 277 | ``` 278 | 279 |
280 | 281 | ### Example 8: Update nested object using iassign.fp() and currying 282 | 283 | ```javascript 284 | const iassign = require("immutable-assign"); 285 | 286 | const nested1 = { a: { b: { c: [3, 4, 5] } } }; 287 | 288 | 289 | // 8.1: Calling iassign() to assign d to nested1.a.b 290 | const iassignFp = iassign.fp(undefined) 291 | (function (n) { return n.a.b; }) 292 | (function (b) { b.d = 6; return b; }) 293 | (undefined); 294 | 295 | const nested2 = iassignFp(nested1); 296 | 297 | // nested2 = { a: { b: { c: [3, 4, 5], d: 6 } } }; 298 | // nested2 !== nested1 299 | 300 | // 8.2: Calling iassign() to increment nested2.a.b.d 301 | iassignFp = iassign.fp(undefined) 302 | (function (n) { return n.a.b.d; }) 303 | (function (d) { return d + 1; }) 304 | (undefined); 305 | const nested3 = iassignFp(nested2); 306 | 307 | // nested3 = { a: { b: { c: [3, 4, 5], d: 7 } } }; 308 | // nested3 !== nested2 309 | 310 | // 8.3: Calling iassign() to push item to nested3.a.b.c 311 | iassignFp = iassign.fp(undefined) 312 | (function (n) { return n.a.b.c; }) 313 | (function (c) { c.push(6); return c; }) 314 | (undefined); 315 | const nested4 = iassignFp(nested3); 316 | 317 | // nested4 = { a: { b: { c: [3, 4, 5, 6], d: 7 } } }; 318 | // nested4 !== nested3 319 | 320 | // 8.4: Calling iassign() to push item to nested3.a.b.c[1] 321 | iassignFp = iassign.fp(undefined) 322 | (function (n, ctx) { return n.a.b.c[ctx.i]; }) 323 | (function (ci) { return ci + 100; }) 324 | ({i: 1}); 325 | const nested5 = iassignFp(nested4); 326 | 327 | // nested5 = { a: { b: { c: [3, 104, 5, 6], d: 7 } } }; 328 | // nested5 !== nested4 329 | 330 | ``` 331 | 332 | ### Example 9: Support the ES6 Map 333 | 334 | ```javascript 335 | const iassign = require("immutable-assign"); 336 | 337 | const map1 = new Map(); 338 | map1.set("a", "value a"); 339 | 340 | iassign.setOption({ 341 | copyFunc: function (value, propName) { 342 | if (value instanceof Map) { 343 | // In IE11, Map constructor arguments are not supported, 344 | // you need to provide ES6 shim, e.g., use core-js 345 | return new Map(value); 346 | } 347 | } 348 | }); 349 | 350 | const map2 = iassign( 351 | map1, 352 | m => { m.set(1, 'first'); return m; } 353 | ); 354 | 355 | // map2 !== map1 356 | // map1 = Map({ "a": "value a" }) 357 | // map1 = Map({ "a": "value a", 1: "first" }) 358 | 359 | ``` 360 | 361 | ### Example 10: Update nested level object properties using property paths (overload 3) 362 | 363 | ```javascript 364 | const iassign = require("immutable-assign"); 365 | 366 | const o1 = { a: { b: { c: [[{ d: 11, e: 12 }], [{ d: 21, e: 22 }]], c2: {} } } }; 367 | 368 | const o2 = iassign( 369 | o1, 370 | ['a', 'b', 'c', 0, '0'], 371 | (ci) => { 372 | ci.d++; 373 | return ci; 374 | }); 375 | 376 | // o2 = { a: { b: { c: [[{ d: 12, e: 12 }], [{ d: 21, e: 22 }]], c2: {} } } }; 377 | // o2 !== o1 378 | ``` 379 | 380 |
381 | 382 | ### Function Signature (TypeScript syntax) 383 | 384 | ```typescript 385 | 386 | // Return a new POJO object with property updated. 387 | 388 | // function overload 1: you can skip getProp() if you trying to update the 1st level object properties, refer to example 1 and 2 389 | iassign = function( 390 | obj: TObj, // POJO object to be getting the property from, it will not be modified. 391 | setProp: setPropFunc, // Function to set the property. 392 | option?: IIassignOption): TObj; // (Optional) Options 393 | 394 | // function overload 2: use getProp() to get prop paths 395 | iassign = function( 396 | obj: TObj, // POJO object to be getting the property from, it will not be modified. 397 | getProp: (obj: TObj, context: TContext) => TProp, // Function to get the property that needs to be updated. 398 | setProp: (prop: TProp) => TProp, // Function to set the property. 399 | context?: TContext, // (Optional) Context to be used in getProp(). 400 | option?: IIassignOption): TObj; // (Optional) Options 401 | 402 | // function overload 3: pass in known property paths (array) 403 | iassign = function( 404 | obj: TObj, // POJO object to be getting the property from, it will not be modified. 405 | propPaths: (string | number)[], // paths to the property that needs to be updated. 406 | setProp: setPropFunc, // Function to set the property. 407 | option?: IIassignOption): TObj; // (Optional) Options 408 | 409 | // functional programming friendly style, moved obj to the last parameter and supports currying, refer to example 8 410 | iassign.fp = function ( 411 | option: IIassignOption, 412 | getPropOrPropPaths: getPropFunc | (string | number)[], 413 | setProp: setPropFunc, 414 | context?: TContext, 415 | obj?: TObj): TObj; // POJO object to be getting the property from, it will not be modified. 416 | 417 | // In ES6, you cannot set any property on imported modules directly, because they default 418 | // to readonly, in this case, you need to use this method. 419 | iassign.setOption(option: IIassignOption): void; 420 | 421 | // Options: can be applied globally or individually 422 | interface IIassignOption { 423 | freeze?: boolean; // Deep freeze both input and output 424 | freezeInput?: boolean; // Deep freeze input 425 | freezeOutput?: boolean; // Deep freeze output 426 | useConstructor?: boolean; // Uses the constructor to create new instances 427 | 428 | // Custom copy function, can be used to handle special types, e.g., Map, Set; refer to example 9 429 | copyFunc?: (value: T, propName: string): T; 430 | 431 | // Disable validation for extra statements in the getProp() function, 432 | // which is needed when running the coverage, e.g., istanbul.js does add 433 | // instrument statements in our getProp() function, which can be safely ignored. 434 | disableExtraStatementCheck?: boolean; 435 | 436 | // Return the same object if setProp() returns the input with no change. 437 | ignoreIfNoChange?: boolean; 438 | } 439 | ``` 440 | 441 | ## Constraints 442 | 443 | * getProp() must be a pure function; I.e., it cannot access anything other than the input parameters. e.g., it must not access "this" or "window" objects. In addition, it must not modify the input parameters. It should only return a property that needs to be updated. 444 | 445 | 446 | ## History 447 | 448 | * 2.1.5 - Migrated to github actions 449 | * 2.1.4 - Reduce the package size to 11.9 kB 450 | * 2.1.1 - Added function overload 3 to pass in known property paths (array). Refer to [example 10](#example-10-update-nested-level-object-properties-using-property-paths-overload-3) 451 | * 2.0.8 - Fixed bug for undefined properties. 452 | * 2.0.4 - Replaced the proxy-polyfill with Object.defineProperty(), which has a much better browser support. 453 | * 2.0.1 - Minor bug fixes. 454 | * 2.0.0 - 455 | * Used [ES6 Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) instead of [eval()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) to process getProp(), which is more secure. 456 | * Also works on platforms that don't support Proxy and Map, such as IE 10 and IE 11 using the [proxy-polyfill](https://github.com/GoogleChrome/proxy-polyfill) and [ES6 Map polyfill](https://github.com/zloirock/core-js) 457 | 458 | * 1.0.36 - [Supports ES6 Map and Set](https://github.com/engineforce/ImmutableAssign/issues/12). Refer to [example 9](#example-9-support-the-es6-map) 459 | * 1.0.35 - Supports ES6 default export. 460 | * 1.0.31 - 461 | * Added ignoreIfNoChange option, which causes iassign to return the same object if setProp() returns its parameter (i.e., reference pointer not changed). 462 | * Added setOption() function to allow you to set the iassign options globally in ES6. 463 | 464 | * 1.0.30 - [Support classes](https://github.com/engineforce/ImmutableAssign/issues/4) 465 | * 1.0.29 - Supported ES6 [Arrow Functions](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions) 466 | * 1.0.27 - Added iassign.fp() that support [currying](https://www.sitepoint.com/currying-in-functional-javascript), refer to [example 8](#example-8-update-nested-object-using-iassignfp-and-currying) 467 | * 1.0.26 - Works with webpack, please refer to [ImmutableAssignTest](https://github.com/engineforce/ImmutableAssignTest) 468 | * 1.0.23 - Greatly improved performance. 469 | * 1.0.21 - Added function overload 1 to skip getProp() if you trying to update the 1st level object properties, refer to [example 1](#example-1-update-1st-level-object-properties) and [example 2](#example-2-update-1st-level-listarray-elements) 470 | * 1.0.20 - Added Travis-CI, Coveralls (coverage) and SauceLabs (browsers' tests) 471 | * 1.0.19 - Added TypeScript types to package.json 472 | * 1.0.18 - Tested on Mac (Safari 10 and Chrome 54) 473 | * 1.0.16 - Tested in Node.js and major browsers (IE 11, Chrome 52, Firefox 47, Edge 13, PhantomJS 2) 474 | 475 | 476 | [1]: https://github.com/engineforce/ImmutableAssign/actions/workflows/npm-publish.yml/badge.svg?branch=master 477 | [2]: https://github.com/engineforce/ImmutableAssign/actions 478 | [3]: https://badge.fury.io/js/immutable-assign.svg 479 | [4]: https://badge.fury.io/js/immutable-assign 480 | [5]: https://coveralls.io/repos/github/engineforce/ImmutableAssign/badge.svg?branch=master 481 | [6]: https://coveralls.io/github/engineforce/ImmutableAssign?branch=master 482 | [7]: https://saucelabs.com/browser-matrix/iassign.svg 483 | [8]: https://saucelabs.com/u/iassign 484 | [9]: https://flat.badgen.net/bundlephobia/minzip/immutable-assign 485 | [10]: https://bundlephobia.com/result?p=immutable-assign 486 | -------------------------------------------------------------------------------- /src/iassign.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __spreadArray = (this && this.__spreadArray) || function (to, from) { 3 | for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) 4 | to[j] = from[i]; 5 | return to; 6 | }; 7 | (function (root, factory) { 8 | if (typeof module === 'object' && typeof module.exports === 'object') { 9 | try { 10 | var deepFreeze = require('deep-freeze-strict'); 11 | } 12 | catch (ex) { 13 | console.warn('Cannot load deep-freeze-strict module, however you can still use iassign() function.'); 14 | } 15 | var v = factory(deepFreeze, exports); 16 | if (v !== undefined) 17 | module.exports = v; 18 | } 19 | else if (typeof define === 'function' && define.amd) { 20 | define(['deep-freeze-strict', 'exports'], factory); 21 | } 22 | else { 23 | // Browser globals (root is window) 24 | root.iassign = factory(root.deepFreeze, {}); 25 | } 26 | })(this, function (deepFreeze, exports) { 27 | var autoCurry = (function () { 28 | var toArray = function toArray(arr, from) { 29 | return Array.prototype.slice.call(arr, from || 0); 30 | }; 31 | var curry = function curry(fn /* variadic number of args */) { 32 | var args = toArray(arguments, 1); 33 | return function curried() { 34 | return fn.apply(undefined, args.concat(toArray(arguments))); 35 | }; 36 | }; 37 | return function autoCurry(fn, numArgs) { 38 | numArgs = numArgs || fn.length; 39 | return function autoCurried() { 40 | if (arguments.length < numArgs) { 41 | return numArgs - arguments.length > 0 42 | ? autoCurry(curry.apply(undefined, [fn].concat(toArray(arguments))), numArgs - arguments.length) 43 | : curry.apply(undefined, [fn].concat(toArray(arguments))); 44 | } 45 | else { 46 | return fn.apply(undefined, arguments); 47 | } 48 | }; 49 | }; 50 | })(); 51 | var iassign = _iassign; 52 | iassign.fp = autoCurry(_iassignFp); 53 | iassign.maxGetPropCacheSize = 100; 54 | iassign.freeze = 55 | typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'; 56 | iassign.setOption = function (option) { 57 | copyOption(iassign, option); 58 | }; 59 | // Immutable Assign 60 | function _iassign(obj, // Object to set property, it will not be modified. 61 | getPropOrSetPropOrPaths, // Function to get property to be updated. Must be pure function. 62 | setPropOrOption, // Function to set property. 63 | contextOrUndefined, // (Optional) Context to be used in getProp(). 64 | optionOrUndefined) { 65 | var getProp = getPropOrSetPropOrPaths; 66 | var propPaths = undefined; 67 | var setProp = setPropOrOption; 68 | var context = contextOrUndefined; 69 | var option = optionOrUndefined; 70 | if (typeof setPropOrOption !== 'function') { 71 | getProp = undefined; 72 | setProp = getPropOrSetPropOrPaths; 73 | context = undefined; 74 | option = setPropOrOption; 75 | } 76 | else { 77 | if (getProp instanceof Array) { 78 | propPaths = getProp; 79 | } 80 | } 81 | option = copyOption(undefined, option, iassign); 82 | if (deepFreeze && (option.freeze || option.freezeInput)) { 83 | deepFreeze(obj); 84 | } 85 | if (!getProp) { 86 | var newValue = undefined; 87 | if (option.ignoreIfNoChange) { 88 | newValue = setProp(obj); 89 | if (newValue === obj) { 90 | return obj; 91 | } 92 | } 93 | obj = quickCopy(obj, undefined, option.useConstructor, option.copyFunc); 94 | obj = option.ignoreIfNoChange ? newValue : setProp(obj); 95 | } 96 | else { 97 | var newValue = undefined; 98 | if (!propPaths) { 99 | if (option.ignoreIfNoChange) { 100 | // Check if getProp() is valid 101 | var value = getProp(obj, context); 102 | newValue = setProp(value); 103 | if (newValue === value) { 104 | return obj; 105 | } 106 | } 107 | var funcText = getProp.toString(); 108 | var arrowIndex = funcText.indexOf('=>'); 109 | if (arrowIndex <= -1) { 110 | var returnIndex = funcText.indexOf('return '); 111 | if (returnIndex <= -1) { 112 | throw new Error('getProp() function does not return a part of obj'); 113 | } 114 | } 115 | propPaths = getPropPath(getProp, obj, context, option); 116 | } 117 | else { 118 | if (option.ignoreIfNoChange) { 119 | // Check if getProp() is valid 120 | var value = getPropByPaths(obj, propPaths); 121 | newValue = setProp(value); 122 | if (newValue === value) { 123 | return obj; 124 | } 125 | } 126 | } 127 | if (!propPaths) { 128 | throw new Error('getProp() function does not return a part of obj'); 129 | } 130 | obj = updateProperty(obj, setProp, newValue, context, propPaths, option); 131 | } 132 | if (deepFreeze && (option.freeze || option.freezeOutput)) { 133 | deepFreeze(obj); 134 | } 135 | return obj; 136 | } 137 | function _iassignFp(option, getProp, setProp, context, obj) { 138 | return _iassign(obj, getProp, setProp, context, option); 139 | } 140 | function getPropPath(getProp, obj, context, option) { 141 | var paths = []; 142 | var objCopy; 143 | var propValue; 144 | if (typeof Proxy === 'undefined') { 145 | propValue = getProp(obj, context); 146 | objCopy = _getPropPathViaProperty(obj, paths); 147 | } 148 | else { 149 | objCopy = _getPropPathViaProxy(obj, paths); 150 | } 151 | getProp(objCopy, context); 152 | // Check propValue === undefined for performance 153 | if (typeof Proxy === 'undefined' && propValue === undefined) { 154 | var functionInfo = parseGetPropFuncInfo(getProp, option); 155 | if (paths.length != functionInfo.funcTokens.length - 1) { 156 | var remainingFunctionTokens = functionInfo.funcTokens.slice(paths.length + 1); 157 | for (var _i = 0, remainingFunctionTokens_1 = remainingFunctionTokens; _i < remainingFunctionTokens_1.length; _i++) { 158 | var token = remainingFunctionTokens_1[_i]; 159 | if (token.propNameSource == ePropNameSource.inBracket && 160 | isNaN(token.propName)) { 161 | throw new Error("Cannot handle " + token.propName + " when the property it point to is undefined."); 162 | } 163 | } 164 | paths = __spreadArray(__spreadArray([], paths), remainingFunctionTokens.map(function (s) { return s.propName; })); 165 | } 166 | } 167 | return paths; 168 | } 169 | function _getPropPathViaProperty(obj, paths, level) { 170 | if (level === void 0) { level = 0; } 171 | var objCopy = quickCopy(obj, paths[level - 1]); 172 | var propertyNames = getOwnPropertyNames(obj); 173 | propertyNames.forEach(function (propKey) { 174 | var descriptor = Object.getOwnPropertyDescriptor(obj, propKey); 175 | if (descriptor && (!(obj instanceof Array) || propKey != 'length')) { 176 | var copyDescriptor = { 177 | enumerable: descriptor.enumerable, 178 | configurable: false, 179 | get: function () { 180 | if (level == paths.length) { 181 | paths.push(propKey); 182 | var propValue = obj[propKey]; 183 | if (propValue != undefined) { 184 | return _getPropPathViaProperty(propValue, paths, level + 1); 185 | } 186 | } 187 | return obj[propKey]; 188 | } 189 | }; 190 | Object.defineProperty(objCopy, propKey, copyDescriptor); 191 | } 192 | }); 193 | return objCopy; 194 | } 195 | function _getPropPathViaProxy(obj, paths, level) { 196 | if (level === void 0) { level = 0; } 197 | var handlers = { 198 | get: function (target, propKey) { 199 | var propValue = obj[propKey]; 200 | if (level == paths.length) { 201 | paths.push(propKey); 202 | if (typeof propValue === 'object' && propValue != null) { 203 | return _getPropPathViaProxy(propValue, paths, level + 1); 204 | } 205 | } 206 | return propValue; 207 | } 208 | }; 209 | return new Proxy(quickCopy(obj, paths[level - 1]), handlers); 210 | } 211 | // For performance 212 | function copyOption(target, option, defaultOption) { 213 | if (target === void 0) { target = {}; } 214 | if (defaultOption) { 215 | target.freeze = defaultOption.freeze; 216 | target.freezeInput = defaultOption.freezeInput; 217 | target.freezeOutput = defaultOption.freezeOutput; 218 | target.useConstructor = defaultOption.useConstructor; 219 | target.copyFunc = defaultOption.copyFunc; 220 | target.disableAllCheck = defaultOption.disableAllCheck; 221 | target.disableHasReturnCheck = defaultOption.disableHasReturnCheck; 222 | target.disableExtraStatementCheck = 223 | defaultOption.disableExtraStatementCheck; 224 | target.maxGetPropCacheSize = defaultOption.maxGetPropCacheSize; 225 | target.ignoreIfNoChange = defaultOption.ignoreIfNoChange; 226 | } 227 | if (option) { 228 | if (option.freeze != undefined) { 229 | target.freeze = option.freeze; 230 | } 231 | if (option.freezeInput != undefined) { 232 | target.freezeInput = option.freezeInput; 233 | } 234 | if (option.freezeOutput != undefined) { 235 | target.freezeOutput = option.freezeOutput; 236 | } 237 | if (option.useConstructor != undefined) { 238 | target.useConstructor = option.useConstructor; 239 | } 240 | if (option.copyFunc != undefined) { 241 | target.copyFunc = option.copyFunc; 242 | } 243 | if (option.disableAllCheck != undefined) { 244 | target.disableAllCheck = option.disableAllCheck; 245 | } 246 | if (option.disableHasReturnCheck != undefined) { 247 | target.disableHasReturnCheck = option.disableHasReturnCheck; 248 | } 249 | if (option.disableExtraStatementCheck != undefined) { 250 | target.disableExtraStatementCheck = option.disableExtraStatementCheck; 251 | } 252 | if (option.maxGetPropCacheSize != undefined) { 253 | target.maxGetPropCacheSize = option.maxGetPropCacheSize; 254 | } 255 | if (option.ignoreIfNoChange != undefined) { 256 | target.ignoreIfNoChange = option.ignoreIfNoChange; 257 | } 258 | } 259 | return target; 260 | } 261 | function updateProperty(obj, setProp, newValue, context, propPaths, option) { 262 | var propValue = quickCopy(obj, undefined, option.useConstructor, option.copyFunc); 263 | obj = propValue; 264 | if (!propPaths.length) { 265 | return option.ignoreIfNoChange ? newValue : setProp(propValue); 266 | } 267 | for (var propIndex = 0; propIndex < propPaths.length; ++propIndex) { 268 | var propName = propPaths[propIndex]; 269 | var isLast = propIndex + 1 === propPaths.length; 270 | var prevPropValue = propValue; 271 | propValue = propValue[propName]; 272 | propValue = quickCopy(propValue, propName, option.useConstructor, option.copyFunc); 273 | if (isLast) { 274 | propValue = option.ignoreIfNoChange ? newValue : setProp(propValue); 275 | } 276 | prevPropValue[propName] = propValue; 277 | } 278 | return obj; 279 | } 280 | function quickCopy(value, propName, useConstructor, copyFunc) { 281 | if (value != undefined && !(value instanceof Date)) { 282 | if (value instanceof Array) { 283 | return value.slice(); 284 | } 285 | else if (typeof value === 'object') { 286 | if (useConstructor) { 287 | var target = new value.constructor(); 288 | return extend(target, value); 289 | } 290 | else if (copyFunc) { 291 | var newValue = copyFunc(value, propName); 292 | if (newValue != undefined) 293 | return newValue; 294 | } 295 | return extend({}, value); 296 | } 297 | } 298 | return value; 299 | } 300 | function extend(destination, source) { 301 | for (var key in source) { 302 | if (!Object.prototype.hasOwnProperty.call(source, key)) { 303 | continue; 304 | } 305 | var value = source[key]; 306 | destination[key] = value; 307 | } 308 | return destination; 309 | } 310 | var ePropNameSource; 311 | (function (ePropNameSource) { 312 | ePropNameSource[ePropNameSource["none"] = 0] = "none"; 313 | ePropNameSource[ePropNameSource["beforeDot"] = 1] = "beforeDot"; 314 | ePropNameSource[ePropNameSource["beforeBracket"] = 2] = "beforeBracket"; 315 | ePropNameSource[ePropNameSource["inBracket"] = 3] = "inBracket"; 316 | ePropNameSource[ePropNameSource["last"] = 4] = "last"; 317 | })(ePropNameSource || (ePropNameSource = {})); 318 | var getPropCaches = {}; 319 | var getPropCacheKeys = []; 320 | function parseGetPropFuncInfo(func, option) { 321 | var funcText = func.toString(); 322 | var cacheKey = funcText + JSON.stringify(option); 323 | var info = getPropCaches[cacheKey]; 324 | if (getPropCaches[cacheKey]) { 325 | return info; 326 | } 327 | var matches = /\(([^\)]*)\)/.exec(funcText); 328 | var objParameterName = undefined; 329 | var cxtParameterName = undefined; 330 | if (matches) { 331 | var parametersText = matches[1]; 332 | var parameters = parametersText.split(','); 333 | objParameterName = parameters[0]; 334 | cxtParameterName = parameters[1]; 335 | } 336 | if (objParameterName) { 337 | objParameterName = objParameterName.trim(); 338 | } 339 | if (cxtParameterName) { 340 | cxtParameterName = cxtParameterName.trim(); 341 | } 342 | var bodyStartIndex = funcText.indexOf('{'); 343 | var bodyEndIndex = funcText.lastIndexOf('}'); 344 | var bodyText = ''; 345 | if (bodyStartIndex > -1 && bodyEndIndex > -1) { 346 | bodyText = funcText.substring(bodyStartIndex + 1, bodyEndIndex); 347 | } 348 | else { 349 | var arrowIndex = funcText.indexOf('=>'); 350 | if (arrowIndex > -1) { 351 | //console.log("Handle arrow function."); 352 | bodyText = 'return ' + funcText.substring(arrowIndex + 3); 353 | } 354 | else { 355 | throw new Error("Cannot parse function: " + funcText); 356 | } 357 | } 358 | var accessorTextInfo = getAccessorTextInfo(bodyText, option); 359 | info = { 360 | objParameterName: objParameterName, 361 | cxtParameterName: cxtParameterName, 362 | bodyText: bodyText, 363 | accessorText: accessorTextInfo.accessorText, 364 | quotedTextInfos: accessorTextInfo.quotedTextInfos, 365 | funcTokens: parseGetPropFuncTokens(accessorTextInfo.accessorText) 366 | }; 367 | if (option.maxGetPropCacheSize > 0) { 368 | getPropCaches[cacheKey] = info; 369 | getPropCacheKeys.push(cacheKey); 370 | if (getPropCacheKeys.length > option.maxGetPropCacheSize) { 371 | var cacheKeyToRemove = getPropCacheKeys.shift(); 372 | delete getPropCaches[cacheKeyToRemove]; 373 | } 374 | } 375 | return info; 376 | } 377 | function parseGetPropFuncTokens(accessorText) { 378 | var tokens = []; 379 | while (accessorText) { 380 | var openBracketIndex = accessorText.indexOf('['); 381 | var closeBracketIndex = accessorText.indexOf(']'); 382 | var dotIndex = accessorText.indexOf('.'); 383 | var propName = ''; 384 | var propNameSource = ePropNameSource.none; 385 | // if (dotIndex == 0) { 386 | // accessorText = accessorText.substr(dotIndex + 1); 387 | // continue; 388 | // } 389 | if (openBracketIndex > -1 && closeBracketIndex <= -1) { 390 | throw new Error('Found open bracket but not close bracket.'); 391 | } 392 | if (openBracketIndex <= -1 && closeBracketIndex > -1) { 393 | throw new Error('Found close bracket but not open bracket.'); 394 | } 395 | if (dotIndex > -1 && 396 | (dotIndex < openBracketIndex || openBracketIndex <= -1)) { 397 | propName = accessorText.substr(0, dotIndex); 398 | accessorText = accessorText.substr(dotIndex + 1); 399 | propNameSource = ePropNameSource.beforeDot; 400 | } 401 | else if (openBracketIndex > -1 && 402 | (openBracketIndex < dotIndex || dotIndex <= -1)) { 403 | if (openBracketIndex > 0) { 404 | propName = accessorText.substr(0, openBracketIndex); 405 | accessorText = accessorText.substr(openBracketIndex); 406 | propNameSource = ePropNameSource.beforeBracket; 407 | } 408 | else { 409 | propName = accessorText.substr(openBracketIndex + 1, closeBracketIndex - 1); 410 | accessorText = accessorText.substr(closeBracketIndex + 1); 411 | propNameSource = ePropNameSource.inBracket; 412 | } 413 | } 414 | else { 415 | propName = accessorText; 416 | accessorText = ''; 417 | propNameSource = ePropNameSource.last; 418 | } 419 | propName = propName.trim(); 420 | if (propName == '') { 421 | continue; 422 | } 423 | //console.log(propName); 424 | tokens.push({ 425 | propName: propName, 426 | propNameSource: propNameSource, 427 | subAccessorText: accessorText 428 | }); 429 | } 430 | return tokens; 431 | } 432 | function getAccessorTextInfo(bodyText, option) { 433 | var returnIndex = bodyText.indexOf('return '); 434 | if (!option.disableAllCheck && !option.disableHasReturnCheck) { 435 | if (returnIndex <= -1) { 436 | throw new Error("getProp() function has no 'return' keyword."); 437 | } 438 | } 439 | if (!option.disableAllCheck && !option.disableExtraStatementCheck) { 440 | var otherBodyText = bodyText.substr(0, returnIndex); 441 | otherBodyText = otherBodyText.replace(/['"]use strict['"];*/g, ''); 442 | otherBodyText = otherBodyText.trim(); 443 | if (otherBodyText != '') { 444 | throw new Error("getProp() function has statements other than 'return': " + 445 | otherBodyText); 446 | } 447 | } 448 | var accessorText = bodyText.substr(returnIndex + 7).trim(); 449 | if (accessorText[accessorText.length - 1] == ';') { 450 | accessorText = accessorText.substring(0, accessorText.length - 1); 451 | } 452 | accessorText = accessorText.trim(); 453 | return parseTextInQuotes(accessorText, option); 454 | } 455 | function parseTextInQuotes(accessorText, option) { 456 | var quotedTextInfos = {}; 457 | var index = 0; 458 | while (true) { 459 | var singleQuoteIndex = accessorText.indexOf("'"); 460 | var doubleQuoteIndex = accessorText.indexOf('"'); 461 | var varName = '#' + index++; 462 | if (singleQuoteIndex <= -1 && doubleQuoteIndex <= -1) 463 | break; 464 | var matches = undefined; 465 | var quoteIndex = void 0; 466 | if (doubleQuoteIndex > -1 && 467 | (doubleQuoteIndex < singleQuoteIndex || singleQuoteIndex <= -1)) { 468 | matches = /("[^"\\]*(?:\\.[^"\\]*)*")/.exec(accessorText); 469 | quoteIndex = doubleQuoteIndex; 470 | } 471 | else if (singleQuoteIndex > -1 && 472 | (singleQuoteIndex < doubleQuoteIndex || doubleQuoteIndex <= -1)) { 473 | matches = /('[^'\\]*(?:\\.[^'\\]*)*')/.exec(accessorText); 474 | quoteIndex = singleQuoteIndex; 475 | } 476 | if (matches) { 477 | quotedTextInfos[varName] = matches[1]; 478 | accessorText = 479 | accessorText.substr(0, quoteIndex) + 480 | varName + 481 | accessorText.substr(matches.index + matches[1].length); 482 | } 483 | else { 484 | throw new Error('Invalid text in quotes: ' + accessorText); 485 | } 486 | } 487 | return { 488 | accessorText: accessorText, 489 | quotedTextInfos: quotedTextInfos 490 | }; 491 | } 492 | iassign.default = iassign; 493 | iassign.deepFreeze = function (obj) { return (iassign.freeze ? deepFreeze(obj) : obj); }; 494 | return iassign; 495 | }); 496 | function getPropByPaths(obj, paths) { 497 | paths = paths.slice(); 498 | var value = obj; 499 | while (paths.length > 0) { 500 | var path = paths.shift(); 501 | value = value[path]; 502 | } 503 | return value; 504 | } 505 | // Android 5: Object.getOwnPropertyNames does not support primitive values gracefully. 506 | function getOwnPropertyNames(obj) { 507 | if (typeof obj !== 'object') { 508 | return []; 509 | } 510 | return Object.getOwnPropertyNames(obj); 511 | } 512 | -------------------------------------------------------------------------------- /deploy/iassign.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __spreadArray = (this && this.__spreadArray) || function (to, from) { 3 | for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) 4 | to[j] = from[i]; 5 | return to; 6 | }; 7 | (function (root, factory) { 8 | if (typeof module === 'object' && typeof module.exports === 'object') { 9 | try { 10 | var deepFreeze = require('deep-freeze-strict'); 11 | } 12 | catch (ex) { 13 | console.warn('Cannot load deep-freeze-strict module, however you can still use iassign() function.'); 14 | } 15 | var v = factory(deepFreeze, exports); 16 | if (v !== undefined) 17 | module.exports = v; 18 | } 19 | else if (typeof define === 'function' && define.amd) { 20 | define(['deep-freeze-strict', 'exports'], factory); 21 | } 22 | else { 23 | // Browser globals (root is window) 24 | root.iassign = factory(root.deepFreeze, {}); 25 | } 26 | })(this, function (deepFreeze, exports) { 27 | var autoCurry = (function () { 28 | var toArray = function toArray(arr, from) { 29 | return Array.prototype.slice.call(arr, from || 0); 30 | }; 31 | var curry = function curry(fn /* variadic number of args */) { 32 | var args = toArray(arguments, 1); 33 | return function curried() { 34 | return fn.apply(undefined, args.concat(toArray(arguments))); 35 | }; 36 | }; 37 | return function autoCurry(fn, numArgs) { 38 | numArgs = numArgs || fn.length; 39 | return function autoCurried() { 40 | if (arguments.length < numArgs) { 41 | return numArgs - arguments.length > 0 42 | ? autoCurry(curry.apply(undefined, [fn].concat(toArray(arguments))), numArgs - arguments.length) 43 | : curry.apply(undefined, [fn].concat(toArray(arguments))); 44 | } 45 | else { 46 | return fn.apply(undefined, arguments); 47 | } 48 | }; 49 | }; 50 | })(); 51 | var iassign = _iassign; 52 | iassign.fp = autoCurry(_iassignFp); 53 | iassign.maxGetPropCacheSize = 100; 54 | iassign.freeze = 55 | typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'; 56 | iassign.setOption = function (option) { 57 | copyOption(iassign, option); 58 | }; 59 | // Immutable Assign 60 | function _iassign(obj, // Object to set property, it will not be modified. 61 | getPropOrSetPropOrPaths, // Function to get property to be updated. Must be pure function. 62 | setPropOrOption, // Function to set property. 63 | contextOrUndefined, // (Optional) Context to be used in getProp(). 64 | optionOrUndefined) { 65 | var getProp = getPropOrSetPropOrPaths; 66 | var propPaths = undefined; 67 | var setProp = setPropOrOption; 68 | var context = contextOrUndefined; 69 | var option = optionOrUndefined; 70 | if (typeof setPropOrOption !== 'function') { 71 | getProp = undefined; 72 | setProp = getPropOrSetPropOrPaths; 73 | context = undefined; 74 | option = setPropOrOption; 75 | } 76 | else { 77 | if (getProp instanceof Array) { 78 | propPaths = getProp; 79 | } 80 | } 81 | option = copyOption(undefined, option, iassign); 82 | if (deepFreeze && (option.freeze || option.freezeInput)) { 83 | deepFreeze(obj); 84 | } 85 | if (!getProp) { 86 | var newValue = undefined; 87 | if (option.ignoreIfNoChange) { 88 | newValue = setProp(obj); 89 | if (newValue === obj) { 90 | return obj; 91 | } 92 | } 93 | obj = quickCopy(obj, undefined, option.useConstructor, option.copyFunc); 94 | obj = option.ignoreIfNoChange ? newValue : setProp(obj); 95 | } 96 | else { 97 | var newValue = undefined; 98 | if (!propPaths) { 99 | if (option.ignoreIfNoChange) { 100 | // Check if getProp() is valid 101 | var value = getProp(obj, context); 102 | newValue = setProp(value); 103 | if (newValue === value) { 104 | return obj; 105 | } 106 | } 107 | var funcText = getProp.toString(); 108 | var arrowIndex = funcText.indexOf('=>'); 109 | if (arrowIndex <= -1) { 110 | var returnIndex = funcText.indexOf('return '); 111 | if (returnIndex <= -1) { 112 | throw new Error('getProp() function does not return a part of obj'); 113 | } 114 | } 115 | propPaths = getPropPath(getProp, obj, context, option); 116 | } 117 | else { 118 | if (option.ignoreIfNoChange) { 119 | // Check if getProp() is valid 120 | var value = getPropByPaths(obj, propPaths); 121 | newValue = setProp(value); 122 | if (newValue === value) { 123 | return obj; 124 | } 125 | } 126 | } 127 | if (!propPaths) { 128 | throw new Error('getProp() function does not return a part of obj'); 129 | } 130 | obj = updateProperty(obj, setProp, newValue, context, propPaths, option); 131 | } 132 | if (deepFreeze && (option.freeze || option.freezeOutput)) { 133 | deepFreeze(obj); 134 | } 135 | return obj; 136 | } 137 | function _iassignFp(option, getProp, setProp, context, obj) { 138 | return _iassign(obj, getProp, setProp, context, option); 139 | } 140 | function getPropPath(getProp, obj, context, option) { 141 | var paths = []; 142 | var objCopy; 143 | var propValue; 144 | if (typeof Proxy === 'undefined') { 145 | propValue = getProp(obj, context); 146 | objCopy = _getPropPathViaProperty(obj, paths); 147 | } 148 | else { 149 | objCopy = _getPropPathViaProxy(obj, paths); 150 | } 151 | getProp(objCopy, context); 152 | // Check propValue === undefined for performance 153 | if (typeof Proxy === 'undefined' && propValue === undefined) { 154 | var functionInfo = parseGetPropFuncInfo(getProp, option); 155 | if (paths.length != functionInfo.funcTokens.length - 1) { 156 | var remainingFunctionTokens = functionInfo.funcTokens.slice(paths.length + 1); 157 | for (var _i = 0, remainingFunctionTokens_1 = remainingFunctionTokens; _i < remainingFunctionTokens_1.length; _i++) { 158 | var token = remainingFunctionTokens_1[_i]; 159 | if (token.propNameSource == ePropNameSource.inBracket && 160 | isNaN(token.propName)) { 161 | throw new Error("Cannot handle " + token.propName + " when the property it point to is undefined."); 162 | } 163 | } 164 | paths = __spreadArray(__spreadArray([], paths), remainingFunctionTokens.map(function (s) { return s.propName; })); 165 | } 166 | } 167 | return paths; 168 | } 169 | function _getPropPathViaProperty(obj, paths, level) { 170 | if (level === void 0) { level = 0; } 171 | var objCopy = quickCopy(obj, paths[level - 1]); 172 | var propertyNames = getOwnPropertyNames(obj); 173 | propertyNames.forEach(function (propKey) { 174 | var descriptor = Object.getOwnPropertyDescriptor(obj, propKey); 175 | if (descriptor && (!(obj instanceof Array) || propKey != 'length')) { 176 | var copyDescriptor = { 177 | enumerable: descriptor.enumerable, 178 | configurable: false, 179 | get: function () { 180 | if (level == paths.length) { 181 | paths.push(propKey); 182 | var propValue = obj[propKey]; 183 | if (propValue != undefined) { 184 | return _getPropPathViaProperty(propValue, paths, level + 1); 185 | } 186 | } 187 | return obj[propKey]; 188 | } 189 | }; 190 | Object.defineProperty(objCopy, propKey, copyDescriptor); 191 | } 192 | }); 193 | return objCopy; 194 | } 195 | function _getPropPathViaProxy(obj, paths, level) { 196 | if (level === void 0) { level = 0; } 197 | var handlers = { 198 | get: function (target, propKey) { 199 | var propValue = obj[propKey]; 200 | if (level == paths.length) { 201 | paths.push(propKey); 202 | if (typeof propValue === 'object' && propValue != null) { 203 | return _getPropPathViaProxy(propValue, paths, level + 1); 204 | } 205 | } 206 | return propValue; 207 | } 208 | }; 209 | return new Proxy(quickCopy(obj, paths[level - 1]), handlers); 210 | } 211 | // For performance 212 | function copyOption(target, option, defaultOption) { 213 | if (target === void 0) { target = {}; } 214 | if (defaultOption) { 215 | target.freeze = defaultOption.freeze; 216 | target.freezeInput = defaultOption.freezeInput; 217 | target.freezeOutput = defaultOption.freezeOutput; 218 | target.useConstructor = defaultOption.useConstructor; 219 | target.copyFunc = defaultOption.copyFunc; 220 | target.disableAllCheck = defaultOption.disableAllCheck; 221 | target.disableHasReturnCheck = defaultOption.disableHasReturnCheck; 222 | target.disableExtraStatementCheck = 223 | defaultOption.disableExtraStatementCheck; 224 | target.maxGetPropCacheSize = defaultOption.maxGetPropCacheSize; 225 | target.ignoreIfNoChange = defaultOption.ignoreIfNoChange; 226 | } 227 | if (option) { 228 | if (option.freeze != undefined) { 229 | target.freeze = option.freeze; 230 | } 231 | if (option.freezeInput != undefined) { 232 | target.freezeInput = option.freezeInput; 233 | } 234 | if (option.freezeOutput != undefined) { 235 | target.freezeOutput = option.freezeOutput; 236 | } 237 | if (option.useConstructor != undefined) { 238 | target.useConstructor = option.useConstructor; 239 | } 240 | if (option.copyFunc != undefined) { 241 | target.copyFunc = option.copyFunc; 242 | } 243 | if (option.disableAllCheck != undefined) { 244 | target.disableAllCheck = option.disableAllCheck; 245 | } 246 | if (option.disableHasReturnCheck != undefined) { 247 | target.disableHasReturnCheck = option.disableHasReturnCheck; 248 | } 249 | if (option.disableExtraStatementCheck != undefined) { 250 | target.disableExtraStatementCheck = option.disableExtraStatementCheck; 251 | } 252 | if (option.maxGetPropCacheSize != undefined) { 253 | target.maxGetPropCacheSize = option.maxGetPropCacheSize; 254 | } 255 | if (option.ignoreIfNoChange != undefined) { 256 | target.ignoreIfNoChange = option.ignoreIfNoChange; 257 | } 258 | } 259 | return target; 260 | } 261 | function updateProperty(obj, setProp, newValue, context, propPaths, option) { 262 | var propValue = quickCopy(obj, undefined, option.useConstructor, option.copyFunc); 263 | obj = propValue; 264 | if (!propPaths.length) { 265 | return option.ignoreIfNoChange ? newValue : setProp(propValue); 266 | } 267 | for (var propIndex = 0; propIndex < propPaths.length; ++propIndex) { 268 | var propName = propPaths[propIndex]; 269 | var isLast = propIndex + 1 === propPaths.length; 270 | var prevPropValue = propValue; 271 | propValue = propValue[propName]; 272 | propValue = quickCopy(propValue, propName, option.useConstructor, option.copyFunc); 273 | if (isLast) { 274 | propValue = option.ignoreIfNoChange ? newValue : setProp(propValue); 275 | } 276 | prevPropValue[propName] = propValue; 277 | } 278 | return obj; 279 | } 280 | function quickCopy(value, propName, useConstructor, copyFunc) { 281 | if (value != undefined && !(value instanceof Date)) { 282 | if (value instanceof Array) { 283 | return value.slice(); 284 | } 285 | else if (typeof value === 'object') { 286 | if (useConstructor) { 287 | var target = new value.constructor(); 288 | return extend(target, value); 289 | } 290 | else if (copyFunc) { 291 | var newValue = copyFunc(value, propName); 292 | if (newValue != undefined) 293 | return newValue; 294 | } 295 | return extend({}, value); 296 | } 297 | } 298 | return value; 299 | } 300 | function extend(destination, source) { 301 | for (var key in source) { 302 | if (!Object.prototype.hasOwnProperty.call(source, key)) { 303 | continue; 304 | } 305 | var value = source[key]; 306 | destination[key] = value; 307 | } 308 | return destination; 309 | } 310 | var ePropNameSource; 311 | (function (ePropNameSource) { 312 | ePropNameSource[ePropNameSource["none"] = 0] = "none"; 313 | ePropNameSource[ePropNameSource["beforeDot"] = 1] = "beforeDot"; 314 | ePropNameSource[ePropNameSource["beforeBracket"] = 2] = "beforeBracket"; 315 | ePropNameSource[ePropNameSource["inBracket"] = 3] = "inBracket"; 316 | ePropNameSource[ePropNameSource["last"] = 4] = "last"; 317 | })(ePropNameSource || (ePropNameSource = {})); 318 | var getPropCaches = {}; 319 | var getPropCacheKeys = []; 320 | function parseGetPropFuncInfo(func, option) { 321 | var funcText = func.toString(); 322 | var cacheKey = funcText + JSON.stringify(option); 323 | var info = getPropCaches[cacheKey]; 324 | if (getPropCaches[cacheKey]) { 325 | return info; 326 | } 327 | var matches = /\(([^\)]*)\)/.exec(funcText); 328 | var objParameterName = undefined; 329 | var cxtParameterName = undefined; 330 | if (matches) { 331 | var parametersText = matches[1]; 332 | var parameters = parametersText.split(','); 333 | objParameterName = parameters[0]; 334 | cxtParameterName = parameters[1]; 335 | } 336 | if (objParameterName) { 337 | objParameterName = objParameterName.trim(); 338 | } 339 | if (cxtParameterName) { 340 | cxtParameterName = cxtParameterName.trim(); 341 | } 342 | var bodyStartIndex = funcText.indexOf('{'); 343 | var bodyEndIndex = funcText.lastIndexOf('}'); 344 | var bodyText = ''; 345 | if (bodyStartIndex > -1 && bodyEndIndex > -1) { 346 | bodyText = funcText.substring(bodyStartIndex + 1, bodyEndIndex); 347 | } 348 | else { 349 | var arrowIndex = funcText.indexOf('=>'); 350 | if (arrowIndex > -1) { 351 | //console.log("Handle arrow function."); 352 | bodyText = 'return ' + funcText.substring(arrowIndex + 3); 353 | } 354 | else { 355 | throw new Error("Cannot parse function: " + funcText); 356 | } 357 | } 358 | var accessorTextInfo = getAccessorTextInfo(bodyText, option); 359 | info = { 360 | objParameterName: objParameterName, 361 | cxtParameterName: cxtParameterName, 362 | bodyText: bodyText, 363 | accessorText: accessorTextInfo.accessorText, 364 | quotedTextInfos: accessorTextInfo.quotedTextInfos, 365 | funcTokens: parseGetPropFuncTokens(accessorTextInfo.accessorText) 366 | }; 367 | if (option.maxGetPropCacheSize > 0) { 368 | getPropCaches[cacheKey] = info; 369 | getPropCacheKeys.push(cacheKey); 370 | if (getPropCacheKeys.length > option.maxGetPropCacheSize) { 371 | var cacheKeyToRemove = getPropCacheKeys.shift(); 372 | delete getPropCaches[cacheKeyToRemove]; 373 | } 374 | } 375 | return info; 376 | } 377 | function parseGetPropFuncTokens(accessorText) { 378 | var tokens = []; 379 | while (accessorText) { 380 | var openBracketIndex = accessorText.indexOf('['); 381 | var closeBracketIndex = accessorText.indexOf(']'); 382 | var dotIndex = accessorText.indexOf('.'); 383 | var propName = ''; 384 | var propNameSource = ePropNameSource.none; 385 | // if (dotIndex == 0) { 386 | // accessorText = accessorText.substr(dotIndex + 1); 387 | // continue; 388 | // } 389 | if (openBracketIndex > -1 && closeBracketIndex <= -1) { 390 | throw new Error('Found open bracket but not close bracket.'); 391 | } 392 | if (openBracketIndex <= -1 && closeBracketIndex > -1) { 393 | throw new Error('Found close bracket but not open bracket.'); 394 | } 395 | if (dotIndex > -1 && 396 | (dotIndex < openBracketIndex || openBracketIndex <= -1)) { 397 | propName = accessorText.substr(0, dotIndex); 398 | accessorText = accessorText.substr(dotIndex + 1); 399 | propNameSource = ePropNameSource.beforeDot; 400 | } 401 | else if (openBracketIndex > -1 && 402 | (openBracketIndex < dotIndex || dotIndex <= -1)) { 403 | if (openBracketIndex > 0) { 404 | propName = accessorText.substr(0, openBracketIndex); 405 | accessorText = accessorText.substr(openBracketIndex); 406 | propNameSource = ePropNameSource.beforeBracket; 407 | } 408 | else { 409 | propName = accessorText.substr(openBracketIndex + 1, closeBracketIndex - 1); 410 | accessorText = accessorText.substr(closeBracketIndex + 1); 411 | propNameSource = ePropNameSource.inBracket; 412 | } 413 | } 414 | else { 415 | propName = accessorText; 416 | accessorText = ''; 417 | propNameSource = ePropNameSource.last; 418 | } 419 | propName = propName.trim(); 420 | if (propName == '') { 421 | continue; 422 | } 423 | //console.log(propName); 424 | tokens.push({ 425 | propName: propName, 426 | propNameSource: propNameSource, 427 | subAccessorText: accessorText 428 | }); 429 | } 430 | return tokens; 431 | } 432 | function getAccessorTextInfo(bodyText, option) { 433 | var returnIndex = bodyText.indexOf('return '); 434 | if (!option.disableAllCheck && !option.disableHasReturnCheck) { 435 | if (returnIndex <= -1) { 436 | throw new Error("getProp() function has no 'return' keyword."); 437 | } 438 | } 439 | if (!option.disableAllCheck && !option.disableExtraStatementCheck) { 440 | var otherBodyText = bodyText.substr(0, returnIndex); 441 | otherBodyText = otherBodyText.replace(/['"]use strict['"];*/g, ''); 442 | otherBodyText = otherBodyText.trim(); 443 | if (otherBodyText != '') { 444 | throw new Error("getProp() function has statements other than 'return': " + 445 | otherBodyText); 446 | } 447 | } 448 | var accessorText = bodyText.substr(returnIndex + 7).trim(); 449 | if (accessorText[accessorText.length - 1] == ';') { 450 | accessorText = accessorText.substring(0, accessorText.length - 1); 451 | } 452 | accessorText = accessorText.trim(); 453 | return parseTextInQuotes(accessorText, option); 454 | } 455 | function parseTextInQuotes(accessorText, option) { 456 | var quotedTextInfos = {}; 457 | var index = 0; 458 | while (true) { 459 | var singleQuoteIndex = accessorText.indexOf("'"); 460 | var doubleQuoteIndex = accessorText.indexOf('"'); 461 | var varName = '#' + index++; 462 | if (singleQuoteIndex <= -1 && doubleQuoteIndex <= -1) 463 | break; 464 | var matches = undefined; 465 | var quoteIndex = void 0; 466 | if (doubleQuoteIndex > -1 && 467 | (doubleQuoteIndex < singleQuoteIndex || singleQuoteIndex <= -1)) { 468 | matches = /("[^"\\]*(?:\\.[^"\\]*)*")/.exec(accessorText); 469 | quoteIndex = doubleQuoteIndex; 470 | } 471 | else if (singleQuoteIndex > -1 && 472 | (singleQuoteIndex < doubleQuoteIndex || doubleQuoteIndex <= -1)) { 473 | matches = /('[^'\\]*(?:\\.[^'\\]*)*')/.exec(accessorText); 474 | quoteIndex = singleQuoteIndex; 475 | } 476 | if (matches) { 477 | quotedTextInfos[varName] = matches[1]; 478 | accessorText = 479 | accessorText.substr(0, quoteIndex) + 480 | varName + 481 | accessorText.substr(matches.index + matches[1].length); 482 | } 483 | else { 484 | throw new Error('Invalid text in quotes: ' + accessorText); 485 | } 486 | } 487 | return { 488 | accessorText: accessorText, 489 | quotedTextInfos: quotedTextInfos 490 | }; 491 | } 492 | iassign.default = iassign; 493 | iassign.deepFreeze = function (obj) { return (iassign.freeze ? deepFreeze(obj) : obj); }; 494 | return iassign; 495 | }); 496 | function getPropByPaths(obj, paths) { 497 | paths = paths.slice(); 498 | var value = obj; 499 | while (paths.length > 0) { 500 | var path = paths.shift(); 501 | value = value[path]; 502 | } 503 | return value; 504 | } 505 | // Android 5: Object.getOwnPropertyNames does not support primitive values gracefully. 506 | function getOwnPropertyNames(obj) { 507 | if (typeof obj !== 'object') { 508 | return []; 509 | } 510 | return Object.getOwnPropertyNames(obj); 511 | } 512 | -------------------------------------------------------------------------------- /src/iassign.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | declare var define; 4 | 5 | interface ICopyFunc { 6 | (value: T, propName: string): T; 7 | } 8 | 9 | interface IIassignOption { 10 | freeze?: boolean; // Deep freeze both input and output 11 | freezeInput?: boolean; // Deep freeze input 12 | freezeOutput?: boolean; // Deep freeze output 13 | useConstructor?: boolean; // Uses the constructor to create new instances 14 | copyFunc?: ICopyFunc; // Custom copy function, can be used to handle special types, e.g., Map, Set 15 | 16 | disableAllCheck?: boolean; 17 | disableHasReturnCheck?: boolean; 18 | // Disable validation for extra statements in the getProp() function, 19 | // which is needed when running the coverage, e.g., istanbul.js does add 20 | // instrument statements in our getProp() function, which can be safely ignored. 21 | disableExtraStatementCheck?: boolean; 22 | 23 | // Default: 100 24 | maxGetPropCacheSize?: number; 25 | 26 | // Return the same object if setProp() returns its parameter (i.e., reference pointer not changed). 27 | ignoreIfNoChange?: boolean; 28 | } 29 | 30 | type getPropFunc = ( 31 | obj: TObj, 32 | context: TContext 33 | ) => TProp; 34 | type setPropFunc = (prop: TProp) => TProp; 35 | 36 | interface IIassign extends IIassignOption { 37 | // Intellisense for the TObj parameter in getProp will only work if we remove the auto added closing bracket of iassign, 38 | // and manually add the closing bracket at last. i.e., 39 | // 40 | // 1. Type iassign( in the editor 41 | // 2. Most editor will auto complete with closing bracket, e.g., iassign() 42 | // 3. If we continue to type without removing the closing bracket, e.g., iassign(nested, (n) => n.), 43 | // editor such as VS Code will not show any intellisense for "n" 44 | // 4. We must remove the closing bracket of iassign(), and intellisense will be shown for "n" 45 | (obj: TObj, setProp: setPropFunc, option?: IIassignOption): TObj; 46 | 47 | ( 48 | obj: TObj, 49 | getProp: getPropFunc, 50 | setProp: setPropFunc, 51 | context?: TContext, 52 | option?: IIassignOption 53 | ): TObj; 54 | 55 | ( 56 | obj: TObj, 57 | propPaths: (string | number)[], 58 | setProp: setPropFunc, 59 | context?: TContext, 60 | option?: IIassignOption 61 | ): TObj; 62 | 63 | // functional programming friendly style, moved obj to the last parameter and supports currying 64 | fp( 65 | option: IIassignOption, 66 | getPropOrPropPath: getPropFunc | (string | number)[], 67 | setProp: setPropFunc, 68 | context?: TContext, 69 | obj?: TObj 70 | ): TObj; 71 | 72 | // In ES6, you cannot set property on imported module directly, because they are default 73 | // to readonly, in this case you need to use this method. 74 | setOption(option: IIassignOption); 75 | 76 | deepFreeze(obj: T): T; 77 | 78 | // ES6 default export 79 | default: IIassign; 80 | } 81 | 82 | (function(root: any, factory) { 83 | if (typeof module === 'object' && typeof module.exports === 'object') { 84 | try { 85 | var deepFreeze: DeepFreeze.DeepFreezeInterface = require('deep-freeze-strict'); 86 | } catch (ex) { 87 | console.warn( 88 | 'Cannot load deep-freeze-strict module, however you can still use iassign() function.' 89 | ); 90 | } 91 | 92 | const v = factory(deepFreeze, exports); 93 | if (v !== undefined) module.exports = v; 94 | } else if (typeof define === 'function' && define.amd) { 95 | define(['deep-freeze-strict', 'exports'], factory); 96 | } else { 97 | // Browser globals (root is window) 98 | root.iassign = factory(root.deepFreeze, {}); 99 | } 100 | })(this, function(deepFreeze, exports) { 101 | const autoCurry = (function() { 102 | const toArray = function toArray(arr, from?) { 103 | return Array.prototype.slice.call(arr, from || 0); 104 | }; 105 | 106 | const curry = function curry(fn /* variadic number of args */) { 107 | const args = toArray(arguments, 1); 108 | return function curried() { 109 | return fn.apply(undefined, args.concat(toArray(arguments))); 110 | }; 111 | }; 112 | 113 | return function autoCurry(fn, numArgs?) { 114 | numArgs = numArgs || fn.length; 115 | return function autoCurried() { 116 | if (arguments.length < numArgs) { 117 | return numArgs - arguments.length > 0 118 | ? autoCurry( 119 | curry.apply(undefined, [fn].concat(toArray(arguments))), 120 | numArgs - arguments.length 121 | ) 122 | : curry.apply(undefined, [fn].concat(toArray(arguments))); 123 | } else { 124 | return fn.apply(undefined, arguments); 125 | } 126 | }; 127 | }; 128 | })(); 129 | 130 | const iassign: IIassign = _iassign; 131 | iassign.fp = autoCurry(_iassignFp); 132 | iassign.maxGetPropCacheSize = 100; 133 | 134 | iassign.freeze = 135 | typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'; 136 | 137 | iassign.setOption = function(option) { 138 | copyOption(iassign, option); 139 | }; 140 | 141 | // Immutable Assign 142 | function _iassign( 143 | obj: TObj, // Object to set property, it will not be modified. 144 | getPropOrSetPropOrPaths: 145 | | getPropFunc 146 | | setPropFunc 147 | | (string | number)[], // Function to get property to be updated. Must be pure function. 148 | setPropOrOption: setPropFunc | IIassignOption, // Function to set property. 149 | contextOrUndefined?: TContext, // (Optional) Context to be used in getProp(). 150 | optionOrUndefined?: IIassignOption 151 | ): TObj { 152 | let getProp = >getPropOrSetPropOrPaths; 153 | let propPaths = undefined; 154 | let setProp = >setPropOrOption; 155 | let context = contextOrUndefined; 156 | let option = optionOrUndefined; 157 | 158 | if (typeof setPropOrOption !== 'function') { 159 | getProp = undefined; 160 | setProp = >getPropOrSetPropOrPaths; 161 | context = undefined; 162 | option = setPropOrOption; 163 | } else { 164 | if (getProp instanceof Array) { 165 | propPaths = getProp; 166 | } 167 | } 168 | 169 | option = copyOption(undefined, option, iassign); 170 | 171 | if (deepFreeze && (option.freeze || option.freezeInput)) { 172 | deepFreeze(obj); 173 | } 174 | 175 | if (!getProp) { 176 | let newValue = undefined; 177 | if (option.ignoreIfNoChange) { 178 | newValue = setProp(obj); 179 | if (newValue === obj) { 180 | return obj; 181 | } 182 | } 183 | 184 | obj = quickCopy(obj, undefined, option.useConstructor, option.copyFunc); 185 | obj = option.ignoreIfNoChange ? newValue : setProp(obj); 186 | } else { 187 | let newValue = undefined; 188 | if (!propPaths) { 189 | if (option.ignoreIfNoChange) { 190 | // Check if getProp() is valid 191 | let value = getProp(obj, context); 192 | 193 | newValue = setProp(value); 194 | if (newValue === value) { 195 | return obj; 196 | } 197 | } 198 | 199 | let funcText = getProp.toString(); 200 | let arrowIndex = funcText.indexOf('=>'); 201 | if (arrowIndex <= -1) { 202 | let returnIndex = funcText.indexOf('return '); 203 | if (returnIndex <= -1) { 204 | throw new Error('getProp() function does not return a part of obj'); 205 | } 206 | } 207 | 208 | propPaths = getPropPath(getProp, obj, context, option); 209 | } else { 210 | if (option.ignoreIfNoChange) { 211 | // Check if getProp() is valid 212 | let value = getPropByPaths(obj, propPaths); 213 | 214 | newValue = setProp(value); 215 | if (newValue === value) { 216 | return obj; 217 | } 218 | } 219 | } 220 | 221 | if (!propPaths) { 222 | throw new Error('getProp() function does not return a part of obj'); 223 | } 224 | 225 | obj = updateProperty(obj, setProp, newValue, context, propPaths, option); 226 | } 227 | 228 | if (deepFreeze && (option.freeze || option.freezeOutput)) { 229 | deepFreeze(obj); 230 | } 231 | 232 | return obj; 233 | } 234 | 235 | function _iassignFp< 236 | TObj, 237 | TProp, 238 | TContext 239 | >(option: IIassignOption, getProp: getPropFunc, setProp: setPropFunc, context?: TContext, obj?: TObj): TObj { 240 | return _iassign< 241 | TObj, 242 | TProp, 243 | TContext 244 | >(obj, getProp, setProp, context, option); 245 | } 246 | 247 | function getPropPath< 248 | TObj, 249 | TProp, 250 | TContext 251 | >(getProp: getPropFunc, obj: TObj, context: TContext, option: IIassignOption): string[] { 252 | let paths = []; 253 | let objCopy; 254 | let propValue; 255 | if (typeof Proxy === 'undefined') { 256 | propValue = getProp(obj, context); 257 | objCopy = _getPropPathViaProperty(obj, paths); 258 | } else { 259 | objCopy = _getPropPathViaProxy(obj, paths); 260 | } 261 | getProp(objCopy, context); 262 | 263 | // Check propValue === undefined for performance 264 | if (typeof Proxy === 'undefined' && propValue === undefined) { 265 | const functionInfo = parseGetPropFuncInfo(getProp, option); 266 | 267 | if (paths.length != functionInfo.funcTokens.length - 1) { 268 | const remainingFunctionTokens = functionInfo.funcTokens.slice( 269 | paths.length + 1 270 | ); 271 | 272 | for (const token of remainingFunctionTokens) { 273 | if ( 274 | token.propNameSource == ePropNameSource.inBracket && 275 | isNaN(token.propName) 276 | ) { 277 | throw new Error( 278 | `Cannot handle ${ 279 | token.propName 280 | } when the property it point to is undefined.` 281 | ); 282 | } 283 | } 284 | 285 | paths = [...paths, ...remainingFunctionTokens.map((s) => s.propName)]; 286 | } 287 | } 288 | 289 | return paths; 290 | } 291 | 292 | function _getPropPathViaProperty(obj, paths: string[], level = 0): any { 293 | let objCopy = quickCopy(obj, paths[level - 1]); 294 | const propertyNames = getOwnPropertyNames(obj); 295 | propertyNames.forEach(function(propKey) { 296 | const descriptor = Object.getOwnPropertyDescriptor(obj, propKey); 297 | if (descriptor && (!(obj instanceof Array) || propKey != 'length')) { 298 | const copyDescriptor = { 299 | enumerable: descriptor.enumerable, 300 | configurable: false, 301 | get: function() { 302 | if (level == paths.length) { 303 | paths.push(propKey); 304 | let propValue = obj[propKey]; 305 | if (propValue != undefined) { 306 | return _getPropPathViaProperty(propValue, paths, level + 1); 307 | } 308 | } 309 | 310 | return obj[propKey]; 311 | } 312 | }; 313 | Object.defineProperty(objCopy, propKey, copyDescriptor); 314 | } 315 | }); 316 | 317 | return objCopy; 318 | } 319 | 320 | function _getPropPathViaProxy(obj, paths: string[], level = 0): any { 321 | const handlers = { 322 | get: (target: any, propKey: string) => { 323 | let propValue = obj[propKey]; 324 | 325 | if (level == paths.length) { 326 | paths.push(propKey); 327 | 328 | if (typeof propValue === 'object' && propValue != null) { 329 | return _getPropPathViaProxy(propValue, paths, level + 1); 330 | } 331 | } 332 | 333 | return propValue; 334 | } 335 | }; 336 | 337 | return new Proxy(quickCopy(obj, paths[level - 1]), handlers); 338 | } 339 | 340 | // For performance 341 | function copyOption( 342 | target: IIassignOption = {}, 343 | option: IIassignOption, 344 | defaultOption?: IIassignOption 345 | ) { 346 | if (defaultOption) { 347 | target.freeze = defaultOption.freeze; 348 | target.freezeInput = defaultOption.freezeInput; 349 | target.freezeOutput = defaultOption.freezeOutput; 350 | target.useConstructor = defaultOption.useConstructor; 351 | target.copyFunc = defaultOption.copyFunc; 352 | target.disableAllCheck = defaultOption.disableAllCheck; 353 | target.disableHasReturnCheck = defaultOption.disableHasReturnCheck; 354 | target.disableExtraStatementCheck = 355 | defaultOption.disableExtraStatementCheck; 356 | target.maxGetPropCacheSize = defaultOption.maxGetPropCacheSize; 357 | target.ignoreIfNoChange = defaultOption.ignoreIfNoChange; 358 | } 359 | 360 | if (option) { 361 | if (option.freeze != undefined) { 362 | target.freeze = option.freeze; 363 | } 364 | if (option.freezeInput != undefined) { 365 | target.freezeInput = option.freezeInput; 366 | } 367 | if (option.freezeOutput != undefined) { 368 | target.freezeOutput = option.freezeOutput; 369 | } 370 | if (option.useConstructor != undefined) { 371 | target.useConstructor = option.useConstructor; 372 | } 373 | if (option.copyFunc != undefined) { 374 | target.copyFunc = option.copyFunc; 375 | } 376 | if (option.disableAllCheck != undefined) { 377 | target.disableAllCheck = option.disableAllCheck; 378 | } 379 | if (option.disableHasReturnCheck != undefined) { 380 | target.disableHasReturnCheck = option.disableHasReturnCheck; 381 | } 382 | if (option.disableExtraStatementCheck != undefined) { 383 | target.disableExtraStatementCheck = option.disableExtraStatementCheck; 384 | } 385 | if (option.maxGetPropCacheSize != undefined) { 386 | target.maxGetPropCacheSize = option.maxGetPropCacheSize; 387 | } 388 | if (option.ignoreIfNoChange != undefined) { 389 | target.ignoreIfNoChange = option.ignoreIfNoChange; 390 | } 391 | } 392 | 393 | return target; 394 | } 395 | 396 | function updateProperty< 397 | TObj, 398 | TProp, 399 | TContext 400 | >(obj: TObj, setProp: setPropFunc, newValue: TProp, context: TContext, propPaths: string[], option: IIassignOption): TObj { 401 | let propValue: any = quickCopy( 402 | obj, 403 | undefined, 404 | option.useConstructor, 405 | option.copyFunc 406 | ); 407 | obj = propValue; 408 | if (!propPaths.length) { 409 | return option.ignoreIfNoChange ? newValue : (setProp(propValue) as any); 410 | } 411 | 412 | for (let propIndex = 0; propIndex < propPaths.length; ++propIndex) { 413 | const propName = propPaths[propIndex]; 414 | const isLast = propIndex + 1 === propPaths.length; 415 | 416 | const prevPropValue = propValue; 417 | 418 | propValue = propValue[propName]; 419 | propValue = quickCopy( 420 | propValue, 421 | propName, 422 | option.useConstructor, 423 | option.copyFunc 424 | ); 425 | 426 | if (isLast) { 427 | propValue = option.ignoreIfNoChange ? newValue : setProp(propValue); 428 | } 429 | 430 | prevPropValue[propName] = propValue; 431 | } 432 | 433 | return obj; 434 | } 435 | 436 | function quickCopy< 437 | T 438 | >(value: T, propName?: string, useConstructor?: boolean, copyFunc?: ICopyFunc): T { 439 | if (value != undefined && !(value instanceof Date)) { 440 | if (value instanceof Array) { 441 | return (value).slice(); 442 | } else if (typeof value === 'object') { 443 | if (useConstructor) { 444 | const target = new (value as any).constructor(); 445 | return extend(target, value); 446 | } else if (copyFunc) { 447 | let newValue = copyFunc(value, propName); 448 | if (newValue != undefined) return newValue; 449 | } 450 | 451 | return extend({}, value); 452 | } 453 | } 454 | 455 | return value; 456 | } 457 | 458 | function extend(destination: any, source) { 459 | for (let key in source) { 460 | if (!Object.prototype.hasOwnProperty.call(source, key)) { 461 | continue; 462 | } 463 | let value = source[key]; 464 | destination[key] = value; 465 | } 466 | return destination; 467 | } 468 | 469 | interface IGetPropFuncToken { 470 | propName: string; 471 | propNameSource: ePropNameSource; 472 | subAccessorText: string; 473 | getPropName?: (obj, context) => string; 474 | } 475 | 476 | enum ePropNameSource { 477 | none, 478 | beforeDot, 479 | beforeBracket, 480 | inBracket, 481 | last 482 | } 483 | 484 | interface IGetPropFuncInfo extends IParsedTextInQuotes { 485 | objParameterName: string; 486 | cxtParameterName: string; 487 | bodyText: string; 488 | funcTokens: IGetPropFuncToken[]; 489 | } 490 | 491 | interface IParsedTextInQuotes { 492 | accessorText: string; 493 | quotedTextInfos: { [key: string]: string }; 494 | } 495 | 496 | let getPropCaches: { [key: string]: IGetPropFuncInfo } = {}; 497 | let getPropCacheKeys: string[] = []; 498 | 499 | function parseGetPropFuncInfo( 500 | func: Function, 501 | option: IIassignOption 502 | ): IGetPropFuncInfo { 503 | let funcText = func.toString(); 504 | 505 | let cacheKey = funcText + JSON.stringify(option); 506 | let info = getPropCaches[cacheKey]; 507 | if (getPropCaches[cacheKey]) { 508 | return info; 509 | } 510 | 511 | let matches = /\(([^\)]*)\)/.exec(funcText); 512 | let objParameterName = undefined; 513 | let cxtParameterName = undefined; 514 | if (matches) { 515 | let parametersText = matches[1]; 516 | let parameters = parametersText.split(','); 517 | objParameterName = parameters[0]; 518 | cxtParameterName = parameters[1]; 519 | } 520 | 521 | if (objParameterName) { 522 | objParameterName = objParameterName.trim(); 523 | } 524 | 525 | if (cxtParameterName) { 526 | cxtParameterName = cxtParameterName.trim(); 527 | } 528 | 529 | let bodyStartIndex = funcText.indexOf('{'); 530 | let bodyEndIndex = funcText.lastIndexOf('}'); 531 | let bodyText = ''; 532 | if (bodyStartIndex > -1 && bodyEndIndex > -1) { 533 | bodyText = funcText.substring(bodyStartIndex + 1, bodyEndIndex); 534 | } else { 535 | let arrowIndex = funcText.indexOf('=>'); 536 | if (arrowIndex > -1) { 537 | //console.log("Handle arrow function."); 538 | bodyText = 'return ' + funcText.substring(arrowIndex + 3); 539 | } else { 540 | throw new Error(`Cannot parse function: ${funcText}`); 541 | } 542 | } 543 | 544 | let accessorTextInfo = getAccessorTextInfo(bodyText, option); 545 | 546 | info = { 547 | objParameterName: objParameterName, 548 | cxtParameterName: cxtParameterName, 549 | bodyText: bodyText, 550 | accessorText: accessorTextInfo.accessorText, 551 | quotedTextInfos: accessorTextInfo.quotedTextInfos, 552 | funcTokens: parseGetPropFuncTokens(accessorTextInfo.accessorText) 553 | }; 554 | 555 | if (option.maxGetPropCacheSize > 0) { 556 | getPropCaches[cacheKey] = info; 557 | getPropCacheKeys.push(cacheKey); 558 | 559 | if (getPropCacheKeys.length > option.maxGetPropCacheSize) { 560 | let cacheKeyToRemove = getPropCacheKeys.shift(); 561 | delete getPropCaches[cacheKeyToRemove]; 562 | } 563 | } 564 | 565 | return info; 566 | } 567 | 568 | function parseGetPropFuncTokens(accessorText: string): IGetPropFuncToken[] { 569 | let tokens: IGetPropFuncToken[] = []; 570 | 571 | while (accessorText) { 572 | let openBracketIndex = accessorText.indexOf('['); 573 | let closeBracketIndex = accessorText.indexOf(']'); 574 | let dotIndex = accessorText.indexOf('.'); 575 | let propName = ''; 576 | let propNameSource = ePropNameSource.none; 577 | 578 | // if (dotIndex == 0) { 579 | // accessorText = accessorText.substr(dotIndex + 1); 580 | // continue; 581 | // } 582 | if (openBracketIndex > -1 && closeBracketIndex <= -1) { 583 | throw new Error('Found open bracket but not close bracket.'); 584 | } 585 | 586 | if (openBracketIndex <= -1 && closeBracketIndex > -1) { 587 | throw new Error('Found close bracket but not open bracket.'); 588 | } 589 | 590 | if ( 591 | dotIndex > -1 && 592 | (dotIndex < openBracketIndex || openBracketIndex <= -1) 593 | ) { 594 | propName = accessorText.substr(0, dotIndex); 595 | accessorText = accessorText.substr(dotIndex + 1); 596 | propNameSource = ePropNameSource.beforeDot; 597 | } else if ( 598 | openBracketIndex > -1 && 599 | (openBracketIndex < dotIndex || dotIndex <= -1) 600 | ) { 601 | if (openBracketIndex > 0) { 602 | propName = accessorText.substr(0, openBracketIndex); 603 | accessorText = accessorText.substr(openBracketIndex); 604 | propNameSource = ePropNameSource.beforeBracket; 605 | } else { 606 | propName = accessorText.substr( 607 | openBracketIndex + 1, 608 | closeBracketIndex - 1 609 | ); 610 | accessorText = accessorText.substr(closeBracketIndex + 1); 611 | propNameSource = ePropNameSource.inBracket; 612 | } 613 | } else { 614 | propName = accessorText; 615 | accessorText = ''; 616 | propNameSource = ePropNameSource.last; 617 | } 618 | 619 | propName = propName.trim(); 620 | if (propName == '') { 621 | continue; 622 | } 623 | 624 | //console.log(propName); 625 | tokens.push({ 626 | propName, 627 | propNameSource, 628 | subAccessorText: accessorText 629 | }); 630 | } 631 | 632 | return tokens; 633 | } 634 | 635 | function getAccessorTextInfo(bodyText: string, option: IIassignOption) { 636 | let returnIndex = bodyText.indexOf('return '); 637 | 638 | if (!option.disableAllCheck && !option.disableHasReturnCheck) { 639 | if (returnIndex <= -1) { 640 | throw new Error("getProp() function has no 'return' keyword."); 641 | } 642 | } 643 | 644 | if (!option.disableAllCheck && !option.disableExtraStatementCheck) { 645 | let otherBodyText = bodyText.substr(0, returnIndex); 646 | otherBodyText = otherBodyText.replace(/['"]use strict['"];*/g, ''); 647 | otherBodyText = otherBodyText.trim(); 648 | if (otherBodyText != '') { 649 | throw new Error( 650 | "getProp() function has statements other than 'return': " + 651 | otherBodyText 652 | ); 653 | } 654 | } 655 | 656 | let accessorText = bodyText.substr(returnIndex + 7).trim(); 657 | if (accessorText[accessorText.length - 1] == ';') { 658 | accessorText = accessorText.substring(0, accessorText.length - 1); 659 | } 660 | accessorText = accessorText.trim(); 661 | 662 | return parseTextInQuotes(accessorText, option); 663 | } 664 | 665 | function parseTextInQuotes( 666 | accessorText, 667 | option: IIassignOption 668 | ): IParsedTextInQuotes { 669 | let quotedTextInfos: { [key: string]: string } = {}; 670 | 671 | let index = 0; 672 | while (true) { 673 | let singleQuoteIndex = accessorText.indexOf("'"); 674 | let doubleQuoteIndex = accessorText.indexOf('"'); 675 | let varName = '#' + index++; 676 | 677 | if (singleQuoteIndex <= -1 && doubleQuoteIndex <= -1) break; 678 | 679 | let matches: RegExpExecArray = undefined; 680 | let quoteIndex: number; 681 | 682 | if ( 683 | doubleQuoteIndex > -1 && 684 | (doubleQuoteIndex < singleQuoteIndex || singleQuoteIndex <= -1) 685 | ) { 686 | matches = /("[^"\\]*(?:\\.[^"\\]*)*")/.exec(accessorText); 687 | quoteIndex = doubleQuoteIndex; 688 | } else if ( 689 | singleQuoteIndex > -1 && 690 | (singleQuoteIndex < doubleQuoteIndex || doubleQuoteIndex <= -1) 691 | ) { 692 | matches = /('[^'\\]*(?:\\.[^'\\]*)*')/.exec(accessorText); 693 | quoteIndex = singleQuoteIndex; 694 | } 695 | 696 | if (matches) { 697 | quotedTextInfos[varName] = matches[1]; 698 | accessorText = 699 | accessorText.substr(0, quoteIndex) + 700 | varName + 701 | accessorText.substr(matches.index + matches[1].length); 702 | } else { 703 | throw new Error('Invalid text in quotes: ' + accessorText); 704 | } 705 | } 706 | 707 | return { 708 | accessorText, 709 | quotedTextInfos 710 | }; 711 | } 712 | 713 | iassign.default = iassign; 714 | iassign.deepFreeze = (obj) => (iassign.freeze ? deepFreeze(obj) : obj); 715 | return iassign; 716 | }); 717 | 718 | function getPropByPaths(obj, paths: (string | number)[]) { 719 | paths = paths.slice(); 720 | let value = obj; 721 | 722 | while (paths.length > 0) { 723 | let path = paths.shift(); 724 | value = value[path]; 725 | } 726 | 727 | return value; 728 | } 729 | 730 | // Android 5: Object.getOwnPropertyNames does not support primitive values gracefully. 731 | function getOwnPropertyNames(obj: any) { 732 | if (typeof obj !== 'object') { 733 | return []; 734 | } 735 | 736 | return Object.getOwnPropertyNames(obj); 737 | } 738 | -------------------------------------------------------------------------------- /debug/benchmarks.js: -------------------------------------------------------------------------------- 1 | 2 | // Copied and modifed from https://github.com/guigrpa/timm/blob/master/tools/benchmarks.coffee 3 | 4 | (function () { 5 | "use strict"; 6 | 7 | var ARRAY_LENGTH, DEEP_PATH, INITIAL_ARRAY, INITIAL_OBJECT, Immutable, R, Seamless, _, _addResult, _allTests, _getIn, _solImmutableJs, _solImmutableSeamless, _solImmutableTimm, _solMutable, _test, _toggle, _verify, chalk, i, n, ref, timm; 8 | 9 | process.env.NODE_ENV = 'production'; // seamless-immutable will use this 10 | 11 | _ = require('lodash'); 12 | 13 | chalk = require('chalk'); 14 | 15 | var expect = require("expect"); 16 | 17 | Seamless = require('../node_modules/seamless-immutable/seamless-immutable.production.min'); 18 | //Seamless = require('seamless-immutable'); // This will also be production, because process.env.NODE_ENV = "production" 19 | 20 | Immutable = require('immutable'); 21 | 22 | timm = require('timm'); 23 | 24 | var _isDevel = false; 25 | 26 | var deepFreeze = require("deep-freeze-strict"); 27 | 28 | var iassign = require("../src/iassign"); 29 | 30 | var immer = require("immer"); 31 | immer.setAutoFreeze(false); 32 | var produce = immer.default; 33 | 34 | var INITIAL_OBJECT = { 35 | toggle: false, 36 | b: 3, 37 | str: 'foo', 38 | d: { 39 | d1: 6, 40 | d2: 'foo', 41 | toggle: false, 42 | d9: { 43 | b: { 44 | b: { 45 | b: 1 46 | } 47 | } 48 | } 49 | }, 50 | e: { 51 | e1: 18, 52 | e2: 'foo' 53 | } 54 | }; 55 | 56 | var DEEP_PATH = ['d', 'd9', 'b', 'b', 'b']; 57 | 58 | var ARRAY_LENGTH = 1000; 59 | 60 | var INITIAL_ARRAY = new Array(ARRAY_LENGTH); 61 | 62 | for (n = i = 0, ref = ARRAY_LENGTH; 0 <= ref ? i < ref : i > ref; n = 0 <= ref ? ++i : --i) { 63 | INITIAL_ARRAY[n] = { 64 | a: 1, 65 | b: 2 66 | }; 67 | } 68 | 69 | var INITIAL_DEEP_ARRAY = [0, 1, 2, INITIAL_ARRAY, [5, 6, 7]]; 70 | 71 | var R = 5e5; 72 | var W = R / 5; 73 | 74 | var _getIn = function (obj, path) { 75 | var j, key, len, out; 76 | out = obj; 77 | for (j = 0, len = path.length; j < len; j++) { 78 | key = path[j]; 79 | out = out[key]; 80 | } 81 | return out; 82 | }; 83 | 84 | var _solMutable = { 85 | init: function () { 86 | return _.cloneDeep(INITIAL_OBJECT); 87 | }, 88 | get: function (obj, key) { 89 | return obj[key]; 90 | }, 91 | set: function (obj, key, val) { 92 | obj[key] = val; 93 | return obj; 94 | }, 95 | getDeep: function (obj, key1, key2) { 96 | return obj[key1][key2]; 97 | }, 98 | setDeep: function (obj, key1, key2, val) { 99 | obj[key1][key2] = val; 100 | return obj; 101 | }, 102 | getIn: _getIn, 103 | setIn: function (obj, path, val) { 104 | var idx, j, ptr, ref1; 105 | ptr = obj; 106 | for (idx = j = 0, ref1 = path.length - 1; 0 <= ref1 ? j < ref1 : j > ref1; idx = 0 <= ref1 ? ++j : --j) { 107 | ptr = ptr[path[idx]]; 108 | } 109 | ptr[path[path.length - 1]] = val; 110 | return obj; 111 | }, 112 | merge: function (obj1, obj2) { 113 | var j, key, len, ref1, results1; 114 | ref1 = Object.keys(obj2); 115 | results1 = []; 116 | for (j = 0, len = ref1.length; j < len; j++) { 117 | key = ref1[j]; 118 | results1.push(obj1[key] = obj2[key]); 119 | } 120 | return results1; 121 | }, 122 | initArr: function (array) { 123 | if (!array) { 124 | array = INITIAL_ARRAY; 125 | } 126 | return _.cloneDeep(array); 127 | }, 128 | getAt: function (arr, idx) { 129 | return arr[idx]; 130 | }, 131 | setAt: function (arr, idx, val) { 132 | arr[idx] = val; 133 | return arr; 134 | }, 135 | getAtDeep: function (arr, idx1, idx2) { 136 | return arr[idx1][idx2]; 137 | }, 138 | setAtDeep: function (arr, idx1, idx2, val) { 139 | arr[idx1][idx2] = val; 140 | return arr; 141 | } 142 | }; 143 | 144 | var _solObjectAssign = { 145 | init: function () { 146 | var obj = _.cloneDeep(INITIAL_OBJECT); 147 | if (_isDevel) { 148 | obj = deepFreeze(obj); 149 | } 150 | return obj; 151 | }, 152 | get: function (obj, key) { 153 | return obj[key]; 154 | }, 155 | set: function (obj, key, val) { 156 | if (obj[key] === val) 157 | return obj; 158 | 159 | obj = Object.assign({}, obj); 160 | obj[key] = val; 161 | return obj; 162 | }, 163 | getDeep: function (obj, key1, key2) { 164 | return obj[key1][key2]; 165 | }, 166 | setDeep: function (obj, key1, key2, val) { 167 | if (obj[key1][key2] === val) 168 | return obj; 169 | 170 | obj = Object.assign({}, obj); 171 | obj[key1] = Object.assign({}, obj[key1]); 172 | obj[key1][key2] = val; 173 | return obj; 174 | }, 175 | getIn: _getIn, 176 | setIn: function (obj, path, val) { 177 | 178 | obj = Object.assign({}, obj); 179 | var prop = obj; 180 | for (var i = 0; i < path.length; ++i) { 181 | var pathPart = path[i]; 182 | if (i < path.length - 1) { 183 | prop[pathPart] = Object.assign({}, prop[pathPart]); 184 | prop = prop[pathPart]; 185 | } 186 | else { 187 | prop[pathPart] = val; 188 | } 189 | } 190 | 191 | return obj; 192 | }, 193 | merge: function (obj1, obj2) { 194 | return Object.assign({}, obj1, obj2); 195 | }, 196 | initArr: function (array) { 197 | if (!array) { 198 | array = INITIAL_ARRAY; 199 | } 200 | 201 | var obj = _.cloneDeep(array); 202 | if (_isDevel) { 203 | obj = deepFreeze(obj); 204 | } 205 | return obj; 206 | }, 207 | getAt: function (arr, idx) { 208 | return arr[idx]; 209 | }, 210 | setAt: function (arr, idx, val) { 211 | if (arr[idx] === val) 212 | return arr; 213 | 214 | arr = arr.slice(0); 215 | arr[idx] = val; 216 | return arr; 217 | }, 218 | getAtDeep: function (arr, idx1, idx2) { 219 | return arr[idx1][idx2]; 220 | }, 221 | setAtDeep: function (arr, idx1, idx2, val) { 222 | if (arr[idx1][idx2] === val) 223 | return arr; 224 | 225 | arr = arr.slice(0); 226 | arr[idx1] = arr[idx1].slice(0); 227 | arr[idx1][idx2] = val; 228 | return arr; 229 | }, 230 | }; 231 | 232 | var _solIassign = { 233 | init: function () { 234 | var obj = _.cloneDeep(INITIAL_OBJECT); 235 | if (_isDevel) { 236 | obj = deepFreeze(obj); 237 | } 238 | return obj; 239 | }, 240 | get: function (obj, key) { 241 | return obj[key]; 242 | }, 243 | set: function (obj, key, val) { 244 | // Paul Note 245 | if (obj[key] === val) 246 | return obj; 247 | 248 | return iassign( 249 | obj, 250 | function (obj) { obj[key] = val; return obj; } 251 | ); 252 | }, 253 | getDeep: function (obj, key1, key2) { 254 | return obj[key1][key2]; 255 | }, 256 | setDeep: function (obj, key1, key2, val) { 257 | // Paul Note 258 | if (obj[key1][key2] === val) 259 | return obj; 260 | 261 | return iassign( 262 | obj, 263 | function (obj, ctx) { return obj[ctx.key1]; }, 264 | function (oKey1) { oKey1[key2] = val; return oKey1; }, 265 | { key1: key1 } 266 | ); 267 | }, 268 | getIn: _getIn, 269 | setIn: function (obj, path, val) { 270 | // Paul Note 271 | var keyText = ""; 272 | for (var i = 0; i < path.length; i++) { 273 | keyText += "['" + path[i] + "']"; 274 | } 275 | 276 | return iassign( 277 | obj, 278 | new Function('obj', 'ctx', 'return obj' + keyText), 279 | function (prop) { 280 | prop = val; return prop; 281 | } 282 | ) 283 | }, 284 | merge: function (obj1, obj2) { 285 | return iassign( 286 | obj1, 287 | function (obj1) { 288 | for (var key in obj2) { 289 | obj1[key] = obj2[key]; 290 | } 291 | return obj1; 292 | } 293 | ) 294 | }, 295 | initArr: function (array) { 296 | if (!array) { 297 | array = INITIAL_ARRAY; 298 | } 299 | 300 | var obj = _.cloneDeep(array); 301 | if (_isDevel) { 302 | obj = deepFreeze(obj); 303 | } 304 | return obj; 305 | }, 306 | getAt: function (arr, idx) { 307 | return arr[idx]; 308 | }, 309 | setAt: function (arr, idx, val) { 310 | // Paul Note 311 | if (arr[idx] === val) 312 | return arr; 313 | 314 | return iassign( 315 | arr, 316 | function (arr) { arr[idx] = val; return arr; } 317 | ); 318 | }, 319 | getAtDeep: function (arr, idx1, idx2) { 320 | return arr[idx1][idx2]; 321 | }, 322 | setAtDeep: function (arr, idx1, idx2, val) { 323 | if (arr[idx1][idx2] === val) 324 | return arr; 325 | 326 | return iassign( 327 | arr, 328 | function (arr, ctx) { return arr[ctx.idx1][ctx.idx2]; }, 329 | function (prop) { return val; }, 330 | { 331 | idx1: idx1, 332 | idx2: idx2 333 | } 334 | ) 335 | } 336 | }; 337 | 338 | var _solImmer = { 339 | init: function () { 340 | var obj = _.cloneDeep(INITIAL_OBJECT); 341 | if (_isDevel) { 342 | obj = deepFreeze(obj); 343 | } 344 | return obj; 345 | }, 346 | get: function (obj, key) { 347 | return obj[key]; 348 | }, 349 | set: function (obj, key, val) { 350 | // Paul Note 351 | if (obj[key] === val) 352 | return obj; 353 | 354 | return produce( 355 | obj, 356 | function (obj) { obj[key] = val; return obj; } 357 | ); 358 | }, 359 | getDeep: function (obj, key1, key2) { 360 | return obj[key1][key2]; 361 | }, 362 | setDeep: function (obj, key1, key2, val) { 363 | // Paul Note 364 | if (obj[key1][key2] === val) 365 | return obj; 366 | 367 | return produce( 368 | obj, 369 | function (obj) { obj[key1][key2] = val; return obj; } 370 | ); 371 | }, 372 | getIn: _getIn, 373 | setIn: function (obj, path, val) { 374 | return produce(obj, obj => { 375 | var idx, j, ptr, ref1; 376 | ptr = obj; 377 | for (idx = j = 0, ref1 = path.length - 1; 0 <= ref1 ? j < ref1 : j > ref1; idx = 0 <= ref1 ? ++j : --j) { 378 | ptr = ptr[path[idx]]; 379 | } 380 | ptr[path[path.length - 1]] = val; 381 | }); 382 | }, 383 | merge: function (obj1, obj2) { 384 | return produce( 385 | obj1, 386 | function (obj1) { 387 | for (var key in obj2) { 388 | obj1[key] = obj2[key]; 389 | } 390 | return obj1; 391 | } 392 | ) 393 | }, 394 | initArr: function (array) { 395 | if (!array) { 396 | array = INITIAL_ARRAY; 397 | } 398 | 399 | var obj = _.cloneDeep(array); 400 | if (_isDevel) { 401 | obj = deepFreeze(obj); 402 | } 403 | return obj; 404 | }, 405 | getAt: function (arr, idx) { 406 | return arr[idx]; 407 | }, 408 | setAt: function (arr, idx, val) { 409 | // Paul Note 410 | if (arr[idx] === val) 411 | return arr; 412 | 413 | return produce( 414 | arr, 415 | function (arr) { arr[idx] = val; return arr; } 416 | ); 417 | }, 418 | getAtDeep: function (arr, idx1, idx2) { 419 | return arr[idx1][idx2]; 420 | }, 421 | setAtDeep: function (arr, idx1, idx2, val) { 422 | if (arr[idx1][idx2] === val) 423 | return arr; 424 | 425 | return produce( 426 | arr, 427 | function (arr) { arr[idx1][idx2] = val; return arr; } 428 | ) 429 | } 430 | }; 431 | 432 | 433 | var _solImmutableTimm = { 434 | init: function () { 435 | return _.cloneDeep(INITIAL_OBJECT); 436 | }, 437 | get: function (obj, key) { 438 | return obj[key]; 439 | }, 440 | set: function (obj, key, val) { 441 | return timm.set(obj, key, val); 442 | }, 443 | getDeep: function (obj, key1, key2) { 444 | return obj[key1][key2]; 445 | }, 446 | setDeep: function (obj, key1, key2, val) { 447 | return timm.set(obj, key1, timm.set(obj[key1], key2, val)); 448 | }, 449 | getIn: _getIn, 450 | setIn: function (obj, path, val) { 451 | return timm.setIn(obj, path, val); 452 | }, 453 | merge: function (obj1, obj2) { 454 | return timm.merge(obj1, obj2); 455 | }, 456 | initArr: function (array) { 457 | if (!array) { 458 | array = INITIAL_ARRAY; 459 | } 460 | return _.cloneDeep(array); 461 | }, 462 | getAt: function (arr, idx) { 463 | return arr[idx]; 464 | }, 465 | setAt: function (arr, idx, val) { 466 | return timm.replaceAt(arr, idx, val); 467 | } 468 | }; 469 | 470 | var _solImmutableJs = { 471 | init: function () { 472 | return Immutable.fromJS(INITIAL_OBJECT); 473 | }, 474 | get: function (obj, key) { 475 | return obj.get(key); 476 | }, 477 | set: function (obj, key, val) { 478 | return obj.set(key, val); 479 | }, 480 | getDeep: function (obj, key1, key2) { 481 | return obj.getIn([key1, key2]); 482 | }, 483 | setDeep: function (obj, key1, key2, val) { 484 | return obj.setIn([key1, key2], val); 485 | }, 486 | getIn: function (obj, path) { 487 | return obj.getIn(path); 488 | }, 489 | setIn: function (obj, path, val) { 490 | return obj.setIn(path, val); 491 | }, 492 | merge: function (obj1, obj2) { 493 | return obj1.merge(obj2); 494 | }, 495 | initArr: function (array) { 496 | if (!array) { 497 | return Immutable.List(INITIAL_ARRAY); 498 | } 499 | return Immutable.fromJS(array); 500 | }, 501 | getAt: function (arr, idx) { 502 | return arr.get(idx); 503 | }, 504 | setAt: function (arr, idx, val) { 505 | return arr.set(idx, val); 506 | }, 507 | getAtDeep: function (arr, idx1, idx2) { 508 | return arr.getIn([idx1, idx2]); 509 | }, 510 | setAtDeep: function (arr, idx1, idx2, val) { 511 | return arr.setIn([idx1, idx2], val); 512 | } 513 | }; 514 | 515 | var _solImmutableSeamless = { 516 | init: function () { 517 | return Seamless(INITIAL_OBJECT); 518 | }, 519 | get: function (obj, key) { 520 | return obj[key]; 521 | }, 522 | set: function (obj, key, val) { 523 | return obj.set(key, val); 524 | }, 525 | getDeep: function (obj, key1, key2) { 526 | return obj[key1][key2]; 527 | }, 528 | setDeep: function (obj, key1, key2, val) { 529 | return obj.setIn([key1, key2], val); 530 | }, 531 | getIn: _getIn, 532 | setIn: function (obj, path, val) { 533 | return obj.setIn(path, val); 534 | }, 535 | merge: function (obj1, obj2) { 536 | return obj1.merge(obj2); 537 | }, 538 | initArr: function (array) { 539 | if (!array) { 540 | array = INITIAL_ARRAY; 541 | } 542 | return Seamless(array); 543 | }, 544 | getAt: function (arr, idx) { 545 | return arr[idx]; 546 | }, 547 | setAt: function (arr, idx, val) { 548 | return arr.set(idx, val); 549 | }, 550 | getAtDeep: function (arr, idx1, idx2) { 551 | return arr[idx1][idx2]; 552 | }, 553 | setAtDeep: function (arr, idx1, idx2, val) { 554 | return arr.setIn([idx1, idx2], val); 555 | } 556 | }; 557 | 558 | var _toggle = function (solution, obj) { 559 | return solution.set(obj, 'toggle', !(solution.get(obj, 'toggle'))); 560 | }; 561 | 562 | var _addResult = function (results, condition) { 563 | return results.push(condition ? chalk.green.bold('P') : chalk.green.red('F')); 564 | }; 565 | 566 | var _verify = function (solution, ignoreMutationError) { 567 | var arr, arr2, get, getAt, getAtDeep, getIn, init, initArr, merge, obj, obj2, results, set, setAt, setDeep, setIn, setAtDeep; 568 | results = []; 569 | init = solution.init, get = solution.get, set = solution.set, setDeep = solution.setDeep, getIn = solution.getIn, 570 | setIn = solution.setIn, merge = solution.merge, initArr = solution.initArr, getAt = solution.getAt, setAt = solution.setAt 571 | getAtDeep = solution.getAtDeep, setAtDeep = solution.setAtDeep; 572 | obj = init(); 573 | _addResult(results, get(obj, 'toggle') === false); 574 | 575 | results.push('-'); // 1 576 | obj2 = set(obj, 'toggle', true); 577 | if (!ignoreMutationError) { 578 | expect(obj).toEqual(INITIAL_OBJECT); 579 | expect(obj2).toEqual(_.merge(_.cloneDeep(INITIAL_OBJECT), {toggle: true})); 580 | } 581 | 582 | _addResult(results, get(obj, 'toggle') === false); 583 | _addResult(results, get(obj2, 'toggle') === true); 584 | _addResult(results, obj2 !== obj); 585 | _addResult(results, get(obj2, 'd') === get(obj, 'd')); 586 | 587 | results.push('-'); // 2 588 | obj2 = set(obj, 'str', 'foo'); 589 | if (!ignoreMutationError) { 590 | expect(obj).toEqual(INITIAL_OBJECT); 591 | expect(obj2).toEqual(_.merge(_.cloneDeep(INITIAL_OBJECT), {str: 'foo'})); 592 | } 593 | _addResult(results, obj2 === obj); 594 | _addResult(results, get(obj2, 'd') === get(obj, 'd')); 595 | 596 | results.push('-'); // 3 597 | obj2 = setDeep(obj, 'd', 'd1', 3); 598 | if (!ignoreMutationError) { 599 | expect(obj).toEqual(INITIAL_OBJECT); 600 | expect(obj2).toEqual(_.merge(_.cloneDeep(INITIAL_OBJECT), {d: { d1: 3}})); 601 | } 602 | _addResult(results, solution.getDeep(obj, 'd', 'd1') === 6); 603 | _addResult(results, solution.getDeep(obj2, 'd', 'd1') === 3); 604 | _addResult(results, obj2 !== obj); 605 | _addResult(results, get(obj2, 'd') !== get(obj, 'd')); 606 | _addResult(results, get(obj2, 'e') === get(obj, 'e')); 607 | 608 | results.push('-'); // 4 609 | obj2 = set(obj, 'b', get(obj, 'b')); 610 | if (!ignoreMutationError) { 611 | expect(obj).toEqual(INITIAL_OBJECT); 612 | expect(obj2).toEqual(INITIAL_OBJECT); 613 | } 614 | _addResult(results, obj2 === obj); 615 | _addResult(results, get(obj2, 'd') === get(obj, 'd')); 616 | 617 | results.push('-'); // 5 618 | obj2 = set(obj, 'str', 'bar'); 619 | if (!ignoreMutationError) { 620 | expect(obj).toEqual(INITIAL_OBJECT); 621 | expect(obj2).toEqual(_.merge(_.cloneDeep(INITIAL_OBJECT), {str: 'bar'})); 622 | } 623 | _addResult(results, obj2 !== obj); 624 | _addResult(results, get(obj2, 'd') === get(obj, 'd')); 625 | 626 | obj = init(); 627 | obj2 = setDeep(obj, 'd', 'd1', 6); 628 | if (!ignoreMutationError) { 629 | expect(obj).toEqual(INITIAL_OBJECT); 630 | expect(obj2).toEqual(_.merge(_.cloneDeep(INITIAL_OBJECT), {d: { d1: 6}})); 631 | } 632 | _addResult(results, solution.getDeep(obj, 'd', 'd1') === 6); 633 | _addResult(results, solution.getDeep(obj2, 'd', 'd1') === 6); 634 | _addResult(results, obj2 === obj); 635 | _addResult(results, get(obj2, 'd') === get(obj, 'd')); 636 | 637 | results.push('-'); // 6 638 | obj2 = setIn(obj, DEEP_PATH, 3); 639 | if (!ignoreMutationError) { 640 | expect(obj).toEqual(INITIAL_OBJECT); 641 | expect(obj2).toEqual(_.merge(_.cloneDeep(INITIAL_OBJECT), {d: { d9: {b: {b: {b: 3}}}}})); 642 | } 643 | _addResult(results, obj2 !== obj); 644 | _addResult(results, get(obj2, 'd') !== get(obj, 'd')); 645 | _addResult(results, get(obj2, 'e') === get(obj, 'e')); 646 | _addResult(results, getIn(obj, DEEP_PATH) === 1); 647 | _addResult(results, getIn(obj2, DEEP_PATH) === 3); 648 | 649 | results.push('-'); // 7 650 | obj2 = merge(obj, { 651 | c: 5, 652 | f: null 653 | }); 654 | if (!ignoreMutationError) { 655 | expect(obj).toEqual(INITIAL_OBJECT); 656 | expect(obj2).toEqual(_.merge(_.cloneDeep(INITIAL_OBJECT), {c: 5, f: null})); 657 | } 658 | _addResult(results, obj2 !== obj); 659 | _addResult(results, get(obj2, 'd') === get(obj, 'd')); 660 | _addResult(results, get(obj2, 'c') === 5); 661 | _addResult(results, get(obj2, 'f') === null); 662 | 663 | results.push('-'); // 8 664 | arr = initArr(); 665 | arr2 = setAt(arr, 1, { 666 | b: 3 667 | }); 668 | if (!ignoreMutationError) { 669 | expect(arr).toEqual(INITIAL_ARRAY); 670 | const expectedArr2 = _.cloneDeep(INITIAL_ARRAY); 671 | expectedArr2[1] = {b: 3}; 672 | expect(arr2).toEqual(expectedArr2); 673 | } 674 | _addResult(results, arr2 !== arr); 675 | _addResult(results, getAt(arr, 1).b === 2); 676 | _addResult(results, getAt(arr2, 1).b === 3); 677 | arr2 = setAt(arr, 1, getAt(arr, 1)); 678 | if (!ignoreMutationError) { 679 | expect(arr).toEqual(INITIAL_ARRAY); 680 | expect(arr2).toEqual(INITIAL_ARRAY); 681 | } 682 | _addResult(results, arr2 === arr); 683 | 684 | results.push('-'); // 9 685 | arr = initArr(INITIAL_DEEP_ARRAY); 686 | arr2 = setAtDeep(arr, 3, 0, { 687 | b: 3 688 | }); 689 | if (!ignoreMutationError) { 690 | expect(arr).toEqual(INITIAL_DEEP_ARRAY); 691 | const expectedArr2 = _.cloneDeep(INITIAL_DEEP_ARRAY); 692 | expectedArr2[3][0] = {b: 3}; 693 | expect(arr2).toEqual(expectedArr2); 694 | } 695 | _addResult(results, arr2 !== arr); 696 | _addResult(results, get(getAtDeep(arr, 3, 0), "b") === 2); 697 | _addResult(results, getAtDeep(arr2, 3, 0).b === 3); 698 | arr2 = setAtDeep(arr, 3, 1, getAtDeep(arr, 3, 1)); 699 | if (!ignoreMutationError) { 700 | expect(arr).toEqual(INITIAL_DEEP_ARRAY); 701 | expect(arr2).toEqual(INITIAL_DEEP_ARRAY); 702 | } 703 | _addResult(results, arr2 === arr); 704 | 705 | return console.log(" Verification: " + (results.join(''))); 706 | }; 707 | 708 | var _test = function (desc, cb) { 709 | var tac, tic; 710 | tic = new Date().getTime(); 711 | cb(); 712 | tac = new Date().getTime(); 713 | var elapsed = tac - tic; 714 | console.log((" " + desc + ": ") + chalk.bold(elapsed + " ms")); 715 | return elapsed; 716 | }; 717 | 718 | var _allTests = function (desc, solution, ignoreMutationError) { 719 | var MERGE_OBJ, arr, obj; 720 | console.log("\n" + chalk.bold(desc)); 721 | _verify(solution, ignoreMutationError); 722 | obj = solution.init(); 723 | var totalRead = 0; 724 | var totalWrite = 0; 725 | totalRead += _test("Object: read (x" + R + ")", function () { 726 | var j, ref1, val; 727 | for (n = j = 0, ref1 = R; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 728 | val = solution.get(obj, 'toggle'); 729 | } 730 | }); 731 | obj = solution.init(); 732 | totalWrite += _test("Object: write (x" + W + ")", function () { 733 | var j, obj2, ref1; 734 | for (n = j = 0, ref1 = W; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 735 | obj2 = solution.set(obj, 'b', n); 736 | } 737 | }); 738 | obj = solution.init(); 739 | totalRead += _test("Object: deep read (x" + R + ")", function () { 740 | var j, ref1, val; 741 | for (n = j = 0, ref1 = R; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 742 | val = solution.getDeep(obj, 'd', 'd1'); 743 | } 744 | }); 745 | obj = solution.init(); 746 | totalWrite += _test("Object: deep write (x" + W + ")", function () { 747 | var j, obj2, ref1; 748 | for (n = j = 0, ref1 = W; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 749 | obj2 = solution.setDeep(obj, 'd', 'd1', n); 750 | } 751 | }); 752 | obj = solution.init(); 753 | totalRead += _test("Object: very deep read (x" + R + ")", function () { 754 | var j, ref1, val; 755 | for (n = j = 0, ref1 = R; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 756 | val = solution.getIn(obj, DEEP_PATH); 757 | } 758 | }); 759 | obj = solution.init(); 760 | totalWrite += _test("Object: very deep write (x" + W + ")", function () { 761 | var j, obj2, ref1; 762 | for (n = j = 0, ref1 = W; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 763 | obj2 = solution.setIn(obj, DEEP_PATH, n); 764 | } 765 | }); 766 | obj = solution.init(); 767 | MERGE_OBJ = { 768 | c: 5, 769 | f: null 770 | }; 771 | totalWrite += _test("Object: merge (x" + W + ")", function () { 772 | var j, obj2, ref1; 773 | for (n = j = 0, ref1 = W; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 774 | obj2 = solution.merge(obj, MERGE_OBJ); 775 | } 776 | }); 777 | arr = solution.initArr(); 778 | totalRead += _test("Array: read (x" + R + ")", function () { 779 | var j, ref1, val; 780 | for (n = j = 0, ref1 = R; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 781 | val = solution.getAt(arr, 1); 782 | } 783 | }); 784 | arr = solution.initArr(); 785 | totalWrite += _test("Array: write (x" + W + ")", function () { 786 | var arr2, j, ref1; 787 | for (n = j = 0, ref1 = W; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 788 | arr2 = solution.setAt(arr, 1, n); 789 | } 790 | }); 791 | 792 | arr = solution.initArr(INITIAL_DEEP_ARRAY); 793 | totalRead += _test("Array: deep read (x" + R + ")", function () { 794 | var j, ref1, val; 795 | for (n = j = 0, ref1 = R; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 796 | val = solution.getAtDeep(arr, 3, 0); 797 | } 798 | }); 799 | arr = solution.initArr(INITIAL_DEEP_ARRAY); 800 | totalWrite += _test("Array: deep write (x" + W + ")", function () { 801 | var arr2, j, ref1; 802 | for (n = j = 0, ref1 = W; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { 803 | arr2 = solution.setAtDeep(arr, 3, 0, n); 804 | } 805 | }); 806 | 807 | var totalElapsed = totalRead + totalWrite; 808 | console.log("Total elapsed = " + totalRead + " ms (read) + " + totalWrite + " ms (write) = " + totalElapsed + " ms."); 809 | return totalElapsed; 810 | }; 811 | 812 | _allTests("Mutable", _solMutable, true); 813 | _allTests("Immutable (Object.assign)", _solObjectAssign); 814 | _allTests("Immutable (immutable-assign)", _solIassign); 815 | _allTests("Immutable (immer setAutoFreeze(false))", _solImmer); 816 | _allTests("Immutable (immutable.js)", _solImmutableJs, true); 817 | _allTests("Immutable (seamless-immutable production)", _solImmutableSeamless); 818 | // _allTests("Immutable (timm)", _solImmutableTimm); 819 | 820 | // Deep freeze initial object/array 821 | _isDevel = true; 822 | _allTests("Immutable (Object.assign) + deep freeze", _solObjectAssign); 823 | _allTests("Immutable (immutable-assign) + deep freeze", _solIassign); 824 | _allTests("Immutable (immer) + deep freeze", _solImmer); 825 | 826 | }).call(this); 827 | --------------------------------------------------------------------------------