Repository: fwilkerson/clean-set
Branch: master
Commit: 741c142a92cd
Files: 12
Total size: 14.7 KB
Directory structure:
gitextract_bm5mcim6/
├── .gitignore
├── .prettierrc
├── .travis.yml
├── LICENSE
├── README.md
├── dist/
│ ├── clean-set.es.js
│ ├── clean-set.js
│ └── clean-set.modern.js
├── index.d.ts
├── lib/
│ └── index.js
├── package.json
└── tests/
└── index.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# Editor files
.vscode
# Testing area for myself
sandbox
coverage.lcov
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true,
"trailingComma": "es5"
}
================================================
FILE: .travis.yml
================================================
after_success: npm run coverage
language: node_js
node_js:
- "8"
cache:
directories:
- "node_modules"
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Frank Wilkerson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# clean-set
[](https://github.com/prettier/prettier)
[](https://travis-ci.org/fwilkerson/clean-set)
[](https://codecov.io/gh/fwilkerson/clean-set)
> Quickly update a value in a deeply nested object and clone each node touched for simple change tracking `===`.
Check out [dset](https://github.com/lukeed/dset) if you just want to do an in place mutation on a deeply nested value.
## Install
> npm i clean-set
Includes builds for commonjs, umd, and esm and is less than ~~200b~~ 182b gzip (thanks to [@lukeed](https://github.com/lukeed))
## Usage
```javascript
let current = {
a: { b: [], c: true },
d: [],
e: {
f: { g: 'hello' },
h: { i: 0 },
},
};
let next = cleanSet(current, 'e.h.i', 1);
/**
* Alternatively you can provide a function for the final parameter to
* receive the current value of that node.
*
* let next = cleanSet(current, 'e.h.i', i => i + 1);
*/
// The value is assigned
console.log(next.e.h.i !== current.e.h.i); // true
// Each parent node touched is a new reference
console.log(next.e.h !== current.e.h); // true
console.log(next.e !== current.e); // true
console.log(next !== current); // true
// Untouched references remain the same
console.log(next.e.f === current.e.f); // true
console.log(next.a === current.a); // true
console.log(next.a.b === current.a.b); // true
console.log(next.d === current.d); // true
```
Here's what an object spread equivalent would look like.
```javascript
let next = {
...current,
e: {
...current.e,
h: { ...current.e.h, i: 1 },
},
};
```
## Benchmarks
Check out the [es bench link](https://esbench.com/bench/5b16f1cbf2949800a0f61cf2) to run the benchmarks yourself.
> Note: YMMV canary and firefox dev have some impressive improvements for object assign and object spread respectively.
Chrome 67
================================================
FILE: dist/clean-set.es.js
================================================
function r(r){var t=r&&r.pop?[]:{};for(var n in r)t[n]=r[n];return t}export default function(t,n,l){n.split&&(n=n.split("."));for(var o=r(t),a=o,e=0,f=n.length;e(
source: A,
keys: string | string[],
update: (value: B) => B
): A;
function cleanSet(source: A, keys: string | string[], update: any): A;
/**
* Update a value in a deeply nested object and clone each node touched
* @param source The object to be updated
* @param location The dot notation or array key path to the property you wish to update
* @param update Either a function that will receive the current value and return a new value OR the new value for the node
* @returns The new object
*/
export default cleanSet;
}
================================================
FILE: lib/index.js
================================================
export default function(source, keys, update) {
keys.split && (keys = keys.split('.'));
let next = copy(source),
last = next,
i = 0,
l = keys.length;
for (; i < l; i++) {
last = last[keys[i]] =
i === l - 1
? update && !!update.call
? update(last[keys[i]])
: update
: copy(last[keys[i]]);
}
return next;
}
function copy(source) {
let to = source && !!source.pop ? [] : {};
for (let i in source) to[i] = source[i];
return to;
}
================================================
FILE: package.json
================================================
{
"name": "clean-set",
"version": "1.1.2",
"description": "A fast deep assignment alternative to the object spread operator and Object.assign",
"umd:main": "dist/clean-set.min.js",
"module": "dist/clean-set.es.js",
"main": "dist/clean-set.js",
"source": "lib/index.js",
"typings": "index.d.ts",
"scripts": {
"build": "microbundle",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
"tap": "tape -r esm tests/**/*.js | tap-difflet",
"test": "nyc --reporter=text npm run tap"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fwilkerson/clean-set.git"
},
"keywords": [],
"author": "Frank A. Wilkerson",
"license": "MIT",
"bugs": {
"url": "https://github.com/fwilkerson/clean-set/issues"
},
"homepage": "https://github.com/fwilkerson/clean-set#readme",
"devDependencies": {
"codecov": "^3.2.0",
"esm": "^3.2.18",
"microbundle": "^0.12.0",
"nyc": "^15.0.1",
"prettier": "^1.16.4",
"tap-difflet": "^0.7.1",
"tape": "^4.10.1"
}
}
================================================
FILE: tests/index.js
================================================
import test from 'tape';
import cleanSet from '../lib';
let data = {
a: { b: [], c: true },
d: [{ m: [{ n: 2 }] }],
e: {
f: { g: 'hello' },
h: { i: 0, j: [] },
},
};
test('clean-set: basic functionality', tap => {
let next = cleanSet(data, 'e.h.i', 1);
tap.assert(next != null, 'next has a value');
tap.assert(next !== data, 'next has a new reference');
tap.assert(next.e.h.i === 1, 'value was updated');
tap.assert(next.e.h !== data.e.h, 'touched node has a new reference');
tap.assert(next.e !== data.e, 'touched node has a new reference');
tap.assert(next.a === data.a, 'untouched node kept their reference');
tap.assert(next.a.b === data.a.b, 'untouched node kept their reference');
tap.assert(next.d === data.d, 'untouched node kept their reference');
tap.assert(
next.d[0].m === data.d[0].m,
'untouched node kept their reference'
);
tap.assert(
next.d[0].m[0] === data.d[0].m[0],
'untouched node kept their reference'
);
tap.assert(next.e.f === data.e.f, 'untouched node kept their reference');
tap.assert(next.e.h.j === data.e.h.j, 'untouched node kept their reference');
tap.end();
});
test('clean-set: update value as a function', tap => {
let next = cleanSet(data, 'e.h.j', j => j.concat('some-item'));
tap.assert(next != null, 'next has a value');
tap.assert(next !== data, 'next has a new reference');
tap.assert(next.e.h.j[0] === 'some-item', 'value was updated');
tap.assert(next.e.h.j !== data.e.h.j, 'touched node has a new reference');
tap.assert(next.e.h !== data.e.h, 'touched node has a new reference');
tap.assert(next.e !== data.e, 'touched node has a new reference');
tap.assert(next.a === data.a, 'untouched node kept their reference');
tap.assert(next.a.b === data.a.b, 'untouched node kept their reference');
tap.assert(next.d === data.d, 'untouched node kept their reference');
tap.assert(
next.d[0].m === data.d[0].m,
'untouched node kept their reference'
);
tap.assert(
next.d[0].m[0] === data.d[0].m[0],
'untouched node kept their reference'
);
tap.assert(next.e.f === data.e.f, 'untouched node kept their reference');
tap.end();
});
test('clean-set: array key', tap => {
let next = cleanSet(data, ['e', 'h', 'i'], i => i + 1);
tap.assert(next != null, 'next has a value');
tap.assert(next !== data, 'next has a new reference');
tap.assert(next.e.h.i === 1, 'value was updated');
tap.assert(next.e.h !== data.e.h, 'touched node has a new reference');
tap.assert(next.e !== data.e, 'touched node has a new reference');
tap.assert(next.a === data.a, 'untouched node kept their reference');
tap.assert(next.a.b === data.a.b, 'untouched node kept their reference');
tap.assert(next.d === data.d, 'untouched node kept their reference');
tap.assert(
next.d[0].m === data.d[0].m,
'untouched node kept their reference'
);
tap.assert(
next.d[0].m[0] === data.d[0].m[0],
'untouched node kept their reference'
);
tap.assert(next.e.f === data.e.f, 'untouched node kept their reference');
tap.assert(next.e.h.j === data.e.h.j, 'untouched node kept their reference');
tap.end();
});
test('clean-set: creates an object if none exists', tap => {
let next = cleanSet(data, 'e.h.k.l', true);
tap.assert(next != null, 'next has a value');
tap.assert(next !== data, 'next has a new reference');
tap.same(next.e.h.k, { l: true }, 'object was created and value was added');
tap.assert(next.e.h !== data.e.h, 'touched node has a new reference');
tap.assert(next.e !== data.e, 'touched node has a new reference');
tap.assert(next.a === data.a, 'untouched node kept their reference');
tap.assert(next.a.b === data.a.b, 'untouched node kept their reference');
tap.assert(next.d === data.d, 'untouched node kept their reference');
tap.assert(
next.d[0].m === data.d[0].m,
'untouched node kept their reference'
);
tap.assert(
next.d[0].m[0] === data.d[0].m[0],
'untouched node kept their reference'
);
tap.assert(next.e.f === data.e.f, 'untouched node kept their reference');
tap.assert(next.e.h.j === data.e.h.j, 'untouched node kept their reference');
tap.end();
});
test('clean-set: supports accessing an index of an array', tap => {
// let next = cleanSet(data, 'd.0.m.0.n', n => n + 1); // either will work for this scenario
let next = cleanSet(data, ['d', 0, 'm', 0, 'n'], n => n + 1);
tap.assert(next != null, 'next has a value');
tap.assert(next !== data, 'next has a new reference');
tap.assert(next.d[0].m[0].n === 3, 'value was updated');
tap.assert(
next.d[0].m[0] !== data.d[0].m[0],
'touched node has a new reference'
);
tap.assert(next.d[0].m !== data.d[0].m, 'touched node has a new reference');
tap.assert(next.d[0] !== data.d[0], 'touched node has a new reference');
tap.assert(next.d !== data.d, 'touched node has a new reference');
tap.assert(next.a === data.a, 'untouched node kept their reference');
tap.assert(next.a.b === data.a.b, 'untouched node kept their reference');
tap.assert(next.e.f === data.e.f, 'untouched node kept their reference');
tap.assert(next.e.h.i === data.e.h.i, 'untouched node kept their reference');
tap.assert(next.e.h.j === data.e.h.j, 'untouched node kept their reference');
tap.end();
});
test('clean-set: creates a record at the index if none exists', tap => {
let next = cleanSet(data, ['d', 2, 'o'], { p: 'creates at index' });
tap.assert(next != null, 'next has a value');
tap.assert(next !== data, 'next has a new reference');
tap.same(next.d[2].o, { p: 'creates at index' }, 'value was created');
tap.assert(next.d !== data.d, 'touched node has a new reference');
tap.assert(next.a === data.a, 'untouched node kept their reference');
tap.assert(next.a.b === data.a.b, 'untouched node kept their reference');
tap.assert(
next.d[0].m === data.d[0].m,
'untouched node kept their reference'
);
tap.assert(
next.d[0].m[0] === data.d[0].m[0],
'untouched node kept their reference'
);
tap.assert(next.e.f === data.e.f, 'untouched node kept their reference');
tap.assert(next.e.h.i === data.e.h.i, 'untouched node kept their reference');
tap.assert(next.e.h.j === data.e.h.j, 'untouched node kept their reference');
tap.end();
});
test('clean-set: allows setting to null', tap => {
let next = cleanSet(data, 'q.r', null);
tap.assert(next != null, 'next has a value');
tap.assert(next !== data, 'next has a new reference');
tap.same(next.q, { r: null } , 'object was created and value set to null');
tap.end();
});
test('clean-set: performance benchmark', tap => {
let cData = data,
n = 0,
start = +Date.now();
for (; n < 100000; n++) {
cData = cleanSet(cData, 'e.h.i', i => i + 1);
}
let timeTaken = +Date.now() - start;
tap.assert(timeTaken < 300, `cleanSet benchmark ran in ${timeTaken}ms`);
tap.end();
});
test('Object.assign: performance benchmark', tap => {
let cData = data,
n = 0,
start = +Date.now();
for (; n < 100000; n++) {
cData = Object.assign({}, cData, {
e: Object.assign({}, cData.e, {
h: Object.assign({}, cData.e.h, { i: cData.e.h.i + 1 }),
}),
});
}
let timeTaken = +Date.now() - start;
tap.assert(timeTaken < 300, `Object.assign benchmark ran in ${timeTaken}ms`);
tap.end();
});
test('Object spread: performance benchmark', tap => {
let cData = data,
n = 0,
start = +Date.now();
for (; n < 100000; n++) {
cData = {
...cData,
e: { ...cData.e, h: { ...cData.e.h, i: cData.e.h.i + 1 } },
};
}
let timeTaken = +Date.now() - start;
tap.assert(timeTaken < 300, `Object spread benchmark ran in ${timeTaken}ms`);
tap.end();
});