diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9cf683..f628921 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,16 +22,15 @@ jobs: if: matrix.nodejs >= 14 run: npm install -g c8 - # - name: Build - # run: npm run build + - name: Build + run: npm run build - name: Test run: npm test if: matrix.nodejs < 14 - name: (coverage) Test - # run: c8 --include=src npm test - run: c8 npm test + run: c8 --include=src npm test if: matrix.nodejs >= 14 - name: (coverage) Report diff --git a/bench/immutable.js b/bench/immutable.js index 3ed9498..1d132ae 100644 --- a/bench/immutable.js +++ b/bench/immutable.js @@ -1,7 +1,7 @@ const assert = require('uvu/assert'); const { Suite } = require('benchmark'); const { klona } = require('klona/json'); -const dset = require('../dist/dset'); +const { dset } = require('../dist'); const contenders = { 'clean-set': require('clean-set'), diff --git a/bench/mutable.js b/bench/mutable.js index a986098..0e56334 100644 --- a/bench/mutable.js +++ b/bench/mutable.js @@ -5,7 +5,7 @@ const contenders = { 'deep-set': require('deep-set'), 'set-value': require('set-value'), 'lodash/set': require('lodash/set'), - 'dset': require('../dist/dset'), + 'dset': require('../dist').dset, }; console.log('Validation: '); diff --git a/bench/readme.md b/bench/readme.md index 9884645..d5bfe0f 100644 --- a/bench/readme.md +++ b/bench/readme.md @@ -14,10 +14,10 @@ Validation: ✔ dset Benchmark: - deep-set x 1,894,926 ops/sec ±2.51% (88 runs sampled) - set-value x 2,208,207 ops/sec ±2.79% (92 runs sampled) - lodash/set x 1,271,022 ops/sec ±1.34% (90 runs sampled) - dset x 2,217,614 ops/sec ±0.55% (96 runs sampled) + deep-set x 1,545,526 ops/sec ±1.49% (89 runs sampled) + set-value x 1,704,871 ops/sec ±2.81% (92 runs sampled) + lodash/set x 995,789 ops/sec ±1.66% (91 runs sampled) + dset x 1,757,022 ops/sec ±0.12% (97 runs sampled) ``` #### Immutable diff --git a/index.d.ts b/index.d.ts index 04a43fd..9c7d2c3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1 +1 @@ -export default function (obj: T, keys: string | ArrayLike, value: V): void; +export function dset(obj: T, keys: string | ArrayLike, value: V): void; diff --git a/package.json b/package.json index 6498c3c..47219ea 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,20 @@ "name": "dset", "version": "2.1.0", "repository": "lukeed/dset", - "description": "A tiny (190B) utility for safely writing deep Object values~!", - "unpkg": "dist/dset.min.js", - "umd:main": "dist/dset.min.js", - "module": "dist/dset.es.js", - "main": "dist/dset.js", + "description": "A tiny (196B) utility for safely writing deep Object values~!", + "unpkg": "dist/index.min.js", + "umd:main": "dist/index.min.js", + "module": "dist/index.mjs", + "main": "dist/index.js", "types": "index.d.ts", "license": "MIT", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./package.json": "./package.json" + }, "author": { "name": "Luke Edwards", "email": "luke.edwards05@gmail.com", @@ -19,8 +26,7 @@ }, "scripts": { "build": "bundt", - "pretest": "npm run build", - "test": "uvu test" + "test": "uvu -r esm test" }, "files": [ "*.d.ts", @@ -37,6 +43,7 @@ ], "devDependencies": { "bundt": "1.1.2", + "esm": "3.2.25", "uvu": "0.5.1" } } diff --git a/readme.md b/readme.md index a023504..1f8b063 100644 --- a/readme.md +++ b/readme.md @@ -1,18 +1,12 @@ # dset [![CI](https://github.com/lukeed/dset/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/lukeed/dset/actions) -> A tiny (190B) utility for safely writing deep Object values~! - -This module exposes two module definitions: - -* **ES Module**: `dist/dset.es.js` -* **CommonJS**: `dist/dset.js` -* **UMD**: `dist/dset.min.js` +> A tiny (196B) utility for safely writing deep Object values~! For _accessing_ deep object properties, please see [`dlv`](https://github.com/developit/dlv). ## Install -``` +```sh $ npm install --save dset ``` @@ -20,35 +14,57 @@ $ npm install --save dset ## Usage ```js -const dset = require('dset'); +import { dset } from 'dset'; -let foo = { a:1, b:2 }; -let bar = { foo:123, bar:[4, 5, 6], baz:{} }; -let baz = { a:1, b:{ x:{ y:{ z:999 } } }, c:3 }; -let qux = { }; +let foo = { abc: 123 }; +dset(foo, 'foo.bar', 'hello'); +// or: dset(foo, ['foo', 'bar'], 'hello'); +console.log(foo); +//=> { +//=> abc: 123, +//=> foo: { bar: 'hello' }, +//=> } -dset(foo, 'd.e.f', 'hello'); -// or ~> dset(foo, ['d', 'e', 'f'], 'hello'); +dset(foo, 'abc.hello', 'world'); +// or: dset(foo, ['abc', 'hello'], 'world'); console.log(foo); -//=> { a:1, b:2, d:{ e:{ f:'hello' } } }; +//=> { +//=> abc: { hello: 'world' }, +//=> foo: { bar: 'hello' }, +//=> } +let bar = { a: { x: 7 }, b:[1, 2, 3] }; dset(bar, 'bar.1', 999); -// or ~> dset(bar, ['bar', 1], 999); +// or: dset(bar, ['bar', 1], 999); +// or: dset(bar, ['bar', '1'], 999); console.log(bar); -//=> { foo:123, bar:[4, 999, 6], baz:{} }; - -dset(baz, 'b.x.j.k', 'mundo'); -dset(baz, 'b.x.y.z', 'hola'); +//=> { +//=> a: { x: 7 }, +//=> bar: [1, 999, 3], +//=> } + +dset(bar, 'a.y.0', 8); +// or: dset(bar, ['a', 'y', 0], 8); +// or: dset(bar, ['a', 'y', '0'], 8); +console.log(bar); +//=> { +//=> a: { +//=> x: 7, +//=> y: [8], +//=> }, +//=> bar: [1, 999, 3], +//=> } + +let baz = {}; +dset(baz, 'a.0.b.0', 1); +dset(baz, 'a.0.b.1', 2); console.log(baz); -//=> { a:1, b:{ x:{ y:{ z:'hola' }, j:{ k:'mundo' } } }, c:3 } - -dset(qux, 'a.0.b.0', 1); -dset(qux, 'a.0.b.1', 2); -console.log(qux); -//=> { a: [{ b: [1, 2] }] } +//=> { +//=> a: [{ b: [1, 2] }] +//=> } ``` -## Mutability +## Immutability As shown in the examples above, all `dset` interactions mutate the source object. @@ -56,8 +72,8 @@ If you need immutable writes, please visit [`clean-set`](https://github.com/fwil Alternatively, you may pair `dset` with [`klona`](https://github.com/lukeed/klona), a 366B utility to clone your source(s). Here's an example pairing: ```js -import klona from 'klona'; -import dset from 'dset'; +import { klona } from 'klona'; +import { dset } from 'dset'; export function deepset(obj, path, val) { let copy = klona(obj); @@ -87,7 +103,7 @@ The key path that should receive the value. May be in `x.y.z` or `['x', 'y', 'z' > **Note:** Please be aware that only the _last_ key actually receives the value! -> **Important:** New Objects are created at each segment if there is not an existing structure.
When numerical-types are encounted, Arrays are created instead! +> **Important:** New Objects are created at each segment if there is not an existing structure.
However, when integers are encounted, Arrays are created instead! #### value @@ -98,7 +114,21 @@ The value that you want to set. Can be of any type! ## Benchmarks -For benchmark results, check out the [`bench`](/bench) directory! +For benchmarks and full results, check out the [`bench`](/bench) directory! + +``` +# Node 10.13.0 + +Validation: + ✔ set-value + ✔ lodash/set + ✔ dset + +Benchmark: + set-value x 1,701,821 ops/sec ±1.81% (93 runs sampled) + lodash/set x 975,530 ops/sec ±0.96% (91 runs sampled) + dset x 1,797,922 ops/sec ±0.32% (94 runs sampled) +``` ## Related diff --git a/src/index.js b/src/index.js index 0582cee..f5628ec 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,9 @@ -export default function (obj, keys, val) { +export function dset(obj, keys, val) { keys.split && (keys=keys.split('.')); var i=0, l=keys.length, t=obj, x, k; for (; i < l;) { k = keys[i++]; - if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue; - t = t[k] = (i === l ? val : ((x=t[k]) != null ? x : (keys[i]*0 !== 0 || !!~keys[i].indexOf('.')) ? {} : [])); + if (k === '__proto__' || k === 'constructor' || k === 'prototype') break; + t = t[k] = (i === l) ? val : (typeof(x=t[k])===typeof(keys)) ? x : (keys[i]*0 !== 0 || !!~(''+keys[i]).indexOf('.')) ? {} : []; } } diff --git a/test/index.js b/test/index.js index 2fab553..fbecbc8 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,6 @@ -const { suite } = require('uvu'); -const assert = require('uvu/assert'); -const dset = require('../dist/dset'); +import { suite } from 'uvu'; +import * as assert from 'uvu/assert'; +import { dset } from '../src'; const API = suite('API'); @@ -71,6 +71,24 @@ keys('should add value to key path :: nested :: array', () => { }); }); +keys('should create Array via integer key :: string', () => { + let input = {}; + dset(input, ['foo', '0'], 123); + assert.instance(input.foo, Array); + assert.equal(input, { + foo: [123] + }) +}); + +keys('should create Array via integer key :: number', () => { + let input = {}; + dset(input, ['foo', 0], 123); + assert.instance(input.foo, Array); + assert.equal(input, { + foo: [123] + }) +}); + keys.run(); // --- @@ -117,7 +135,7 @@ arrays('should create arrays with hole(s) if needed', () => { }); }); -arrays('should create object from decimal-like key :: array :: zero', () => { +arrays('should create object from decimal-like key :: array :: zero :: string', () => { let input = {}; dset(input, ['x', '10.0', 'z'], 123); assert.not.instance(input.x, Array); @@ -130,6 +148,16 @@ arrays('should create object from decimal-like key :: array :: zero', () => { }); }); +arrays('should create array from decimal-like key :: array :: zero :: number', () => { + let input = {}; + dset(input, ['x', 10.0, 'z'], 123); + assert.instance(input.x, Array); + + let x = Array(10); + x.push({ z: 123 }); + assert.equal(input, { x }); +}); + arrays('should create object from decimal-like key :: array :: nonzero', () => { let input = {}; dset(input, ['x', '10.2', 'z'], 123); @@ -172,20 +200,22 @@ preserves('should preserve existing object structure', () => { }); }); -preserves('should not convert existing non-object values into object', () => { +preserves('should overwrite existing non-object values as object', () => { let input = { a: { b: 123 } }; - let before = JSON.stringify(input); dset(input, 'a.b.c', 'hello'); - assert.is( - JSON.stringify(input), - before - ); + assert.equal(input, { + a: { + b: { + c: 'hello' + } + } + }); }); preserves('should preserve existing object tree w/ array value', () => { @@ -208,26 +238,6 @@ preserves('should preserve existing object tree w/ array value', () => { }); }); -preserves('should not throw when refusing to convert non-object into object', () => { - try { - let input = { b:123 }; - dset(input, 'b.c.d.e', 123); - assert.is(input.b, 123); - } catch (err) { - assert.unreachable('should not have thrown'); - } -}); - -preserves('should not throw when refusing to convert `0` into object', () => { - try { - let input = { b:0 }; - dset(input, 'b.a.s.d', 123); - assert.equal(input, { b: 0 }); - } catch (err) { - assert.unreachable('should not have thrown'); - } -}); - preserves.run(); // --- @@ -241,8 +251,7 @@ pollution('should protect against "__proto__" assignment', () => { assert.equal(input.__proto__, before); assert.equal(input, { - abc: 123, - hello: 123 + abc: 123 }); assert.is.not({}.hello, 123); @@ -259,7 +268,7 @@ pollution('should protect against "__proto__" assignment :: nested', () => { assert.equal(input, { abc: 123, xyz: { - hello: 123 + // empty } }); @@ -274,18 +283,46 @@ pollution('should ignore "prototype" assignment', () => { dset(input, 'a.prototype.hello', 'world'); assert.is(input.a.prototype, undefined); - assert.is.not(input.a.hello, 'world'); - assert.equal(input, { a: 123 }); + assert.is(input.a.hello, undefined); + + assert.equal(input, { + a: { + // converted, then aborted + } + }); + + assert.is( + JSON.stringify(input), + '{"a":{}}' + ); }); -pollution('should ignore "constructor" assignment', () => { +pollution('should ignore "constructor" assignment :: direct', () => { let input = { a: 123 }; - let before = input.a.constructor; - dset(input, 'a.constructor', 'world'); - assert.equal(input.a.constructor, before); - assert.equal(input, { a: 123 }); - assert.equal(before, Number); + function Custom() { + // + } + + dset(input, 'a.constructor', Custom); + assert.is.not(input.a.constructor, Custom); + assert.not.instance(input.a, Custom); + + assert.instance(input.a.constructor, Object, '~> 123 -> {}'); + assert.is(input.a.hasOwnProperty('constructor'), false); + assert.equal(input, { a: {} }); +}); + +pollution('should ignore "constructor" assignment :: nested', () => { + let input = {}; + + dset(input, 'constructor.prototype.hello', 'world'); + assert.is(input.hasOwnProperty('constructor'), false); + assert.is(input.hasOwnProperty('hello'), false); + + assert.equal(input, { + // empty + }); }); pollution.run();