diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..e76bde7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,14 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore index 944c283..5d9fbdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -.idea/ +coverage +.idea +.vscode diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1a02b9e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - '0.10' -script: "npm test" -after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" \ No newline at end of file diff --git a/.xo-config.json b/.xo-config.json new file mode 100644 index 0000000..c13ef48 --- /dev/null +++ b/.xo-config.json @@ -0,0 +1,3 @@ +{ + "prettier": true +} diff --git a/README.md b/README.md index bf52f94..dd391d6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -# deep-keys +# deep-keys + [![NPM version][npm-image]][npm-url] -[![Build status][travis-image]][travis-url] -[![Test coverage][coveralls-image]][coveralls-url] -[![Dependency Status][david-image]][david-url] +[![Build status][github-actions-image]][github-actions-url] [![License][license-image]][license-url] [![Downloads][downloads-image]][downloads-url] @@ -12,61 +11,59 @@ ## Install -```sh -$ npm install --save deep-keys +```shell +npm install deep-keys ``` ## Usage + ##### `deepKeys(obj, intermediate[optional])` + ```js -var deepKeys = require('deep-keys'); +import deepKeys from "deep-keys"; -var obj1 = { +const obj1 = { a: 1, b: { c: 1 }, c: { d: { e: 1 }, f: 1 }, d: { e: { f: { g: 1, h: 2 } } }, e: 2, - f: { g: [] } + f: { g: [] }, }; deepKeys(obj1); //=> ['a', 'b.c', 'c.d.e', 'c.f', 'd.e.f.g', 'd.e.f.h', 'e', 'f.g'] -var obj2 = { - type: 'customer', +const obj2 = { + type: "customer", details: { - name: 'Ariel', age: 26, address: { city: 'Tel Aviv', country: 'Israel' } + name: "Ariel", + age: 26, + address: { city: "Tel Aviv", country: "Israel" }, }, - isActive: true + isActive: true, }; deepKeys(obj2); //=> ['type', 'details.name', 'details.age', 'details.address.city', 'details.address.country', 'isActive'] // intermediate example -var obj3 = {a:{b:{c:1}}}; -deepKeys(obj3); //=> [ 'a.b.c' ] +const obj3 = { a: { b: { c: 1 } } }; +deepKeys(obj3); //=> [ 'a.b.c' ] deepKeys(obj3, true); //=> [ 'a', 'a.b', 'a.b.c' ] // Dots in key names get escaped -var obj4 = { 'a.': { b: 1} }; -deepKeys(obj4) //=> [ 'a\\..b' ] +const obj4 = { "a.": { b: 1 } }; +deepKeys(obj4); //=> [ 'a\\..b' ] ``` - ## License MIT © [Ariel Mashraki](https://github.com/a8m) [npm-image]: https://img.shields.io/npm/v/deep-keys.svg?style=flat-square [npm-url]: https://npmjs.org/package/deep-keys -[travis-image]: https://img.shields.io/travis/a8m/deep-keys.svg?style=flat-square -[travis-url]: https://travis-ci.org/a8m/deep-keys -[coveralls-image]: https://img.shields.io/coveralls/a8m/deep-keys.svg?style=flat-square -[coveralls-url]: https://coveralls.io/r/a8m/deep-keys -[david-image]: http://img.shields.io/david/a8m/deep-keys.svg?style=flat-square -[david-url]: https://david-dm.org/a8m/deep-keys -[license-image]: http://img.shields.io/npm/l/deep-keys.svg?style=flat-square +[github-actions-image]: https://github.com/a8m/deep-keys/actions +[github-actions-url]: https://img.shields.io/github/actions/workflow/status/a8m/deep-keys/main.yml?branch=master&logo=github&label=CI&style=flat-square +[license-image]: https://img.shields.io/npm/l/deep-keys.svg?style=flat-square [license-url]: LICENSE -[downloads-image]: http://img.shields.io/npm/dm/deep-keys.svg?style=flat-square +[downloads-image]: https://img.shields.io/npm/dm/deep-keys.svg?style=flat-square [downloads-url]: https://npmjs.org/package/deep-keys - diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..beb47d8 --- /dev/null +++ b/index.d.ts @@ -0,0 +1 @@ +export default function deepKeys(object: any): string[]; diff --git a/index.js b/index.js index c7437a0..1a2cddd 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,3 @@ -'use strict'; - /** * @description * returns {boolean} True if `value` is an `Object` but not `null` @@ -7,7 +5,9 @@ * @returns {boolean} */ function isObject(value) { - return value !== null && typeof value === 'object' && !(value instanceof Date); + return ( + value !== null && typeof value === 'object' && !(value instanceof Date) + ); } /** @@ -16,26 +16,30 @@ function isObject(value) { * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ -var isArray = Array.isArray; +const {isArray} = Array; + +function deepKeys(object, stack, parent, intermediate) { + for (const element of Object.keys(object)) { + // Escape . in the element name + const escaped = element.replaceAll('.', '\\.'); + // If it's a nested object + if (isObject(object[element]) && !isArray(object[element])) { + // Concatenate the new parent if exist + const p = parent ? parent + '.' + escaped : parent; + // Push intermediate parent key if flag is true + if (intermediate) { + stack.push(parent ? p : escaped); + } -function deepKeys(obj, stack, parent, intermediate) { - Object.keys(obj).forEach(function(el) { - // Escape . in the element name - var escaped = el.replace(/\./g, '\\\.'); - // If it's a nested object - if(isObject(obj[el]) && !isArray(obj[el])) { - // Concatenate the new parent if exist - var p = parent ? parent + '.' + escaped : parent; - // Push intermediate parent key if flag is true - if (intermediate) stack.push(parent ? p : escaped); - deepKeys(obj[el], stack, p || escaped, intermediate); - } else { - // Create and save the key - var key = parent ? parent + '.' + escaped : escaped; - stack.push(key) - } - }); - return stack + deepKeys(object[element], stack, p || escaped, intermediate); + } else { + // Create and save the key + const key = parent ? parent + '.' + escaped : escaped; + stack.push(key); + } + } + + return stack; } /** @@ -52,6 +56,6 @@ function deepKeys(obj, stack, parent, intermediate) { * @example * deepKeys({ 'a.': { b: 1 }) ==> ["a\..b"] */ -module.exports = function (obj, intermediate) { - return deepKeys(obj, [], null, intermediate); -}; +export default function _deepKeys(object, intermediate) { + return deepKeys(object, [], null, intermediate); +} diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000..03b8808 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,4 @@ +import {expectType} from 'tsd'; +import deepKeys from './index.js'; + +expectType(deepKeys({foo: {bar: {baz: 'qux'}}})); diff --git a/package.json b/package.json index 1336567..0a29bfa 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "deep-keys", "version": "0.5.0", "description": "Creates an array composed of the own enumerable property names(including nested) of an object.", + "type": "module", + "exports": "./index.js", + "types": "./index.d.ts", "license": "MIT", "repository": "a8m/deep-keys", "author": { @@ -10,13 +13,14 @@ "url": "https://github.com/a8m/deep-keys" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" }, "scripts": { - "test": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- -u exports test.js" + "test": "xo && tsd && c8 ava" }, "files": [ - "index.js" + "index.js", + "index.d.ts" ], "keywords": [ "map", @@ -28,7 +32,9 @@ "nested-keys" ], "devDependencies": { - "istanbul": "^0.3.2", - "mocha": "^3.5.3" + "ava": "^6.2.0", + "c8": "^10.1.3", + "tsd": "^0.31.2", + "xo": "^0.60.0" } } diff --git a/test.js b/test.js index 93e7291..5837a01 100644 --- a/test.js +++ b/test.js @@ -1,77 +1,99 @@ -'use strict'; +import test from 'ava'; +import keys from './index.js'; -var assert = require('assert'); -var keys = require('./'); - -//o1, o2, msg -var expectEqual = assert.deepEqual; - -describe('deep-keys', function() { - - it('should return array composed of it\'s properties names', function() { - expectEqual(keys({ a:1, b:2, c:3, d:4 }), ['a', 'b', 'c', 'd']); - expectEqual(keys({}), []); - }); - - it('should return owned properties', function() { - var obj = { - a: { b: 1, c: 2 } - }; - expectEqual(keys(obj.a), ['b', 'c']); - }); +test("should return array composed of it's properties names", (t) => { + t.deepEqual( + keys({ + a: 1, + b: 2, + c: 3, + d: 4, + }), + ['a', 'b', 'c', 'd'], + ); +}); - it('should return deep keys', function() { - var obj1 = { - a: 1, - b: { c: 1 }, - c: { d: { e: 1 }, f: 1 }, - d: { e: { f: { g: 1, h: 2 } } }, - e: 2, - f: { g: [] } - }; - expectEqual(keys(obj1), ['a', 'b.c', 'c.d.e', 'c.f', 'd.e.f.g', 'd.e.f.h', 'e', 'f.g']); +test('should return owned properties', (t) => { + const object = {a: {b: 1, c: 2}}; + t.deepEqual(keys(object.a), ['b', 'c']); +}); - var obj2 = { - type: 'customer', - details: { - name: 'Ariel', age: 26, address: { city: 'Tel Aviv', country: 'Israel' } - }, - isActive: true, - createdAt: new Date() - }; - expectEqual(keys(obj2), [ - 'type', - 'details.name', - 'details.age', - 'details.address.city', - 'details.address.country', - 'isActive', - 'createdAt' - ]); - }); +test('should return deep keys', (t) => { + const object1 = { + a: 1, + b: {c: 1}, + c: {d: {e: 1}, f: 1}, + d: {e: {f: {g: 1, h: 2}}}, + e: 2, + f: {g: []}, + }; + t.deepEqual(keys(object1), [ + 'a', + 'b.c', + 'c.d.e', + 'c.f', + 'd.e.f.g', + 'd.e.f.h', + 'e', + 'f.g', + ]); - it('should return deep keys including intermediate parent keys', function() { - var obj1 = { - a: 1, - b: { c: 1 }, - c: { d: { e: 1 }, f: 1 }, - d: { e: { f: { g: 1, h: 2 } } }, - e: 2, - f: { g: [] } - }; - expectEqual(keys(obj1, true), ['a', 'b', 'b.c', 'c', 'c.d', 'c.d.e', 'c.f', - 'd', 'd.e', 'd.e.f', 'd.e.f.g', 'd.e.f.h', 'e', 'f', 'f.g']); - }); + const object2 = { + type: 'customer', + details: { + name: 'Ariel', + age: 26, + address: {city: 'Tel Aviv', country: 'Israel'}, + }, + isActive: true, + createdAt: new Date(), + }; + t.deepEqual(keys(object2), [ + 'type', + 'details.name', + 'details.age', + 'details.address.city', + 'details.address.country', + 'isActive', + 'createdAt', + ]); +}); - it('should escape . in key names', function() { - var obj1 = { a: { '.b': 1 } }; - expectEqual(keys(obj1), ['a.\\.b']); - var obj2 = { 'a.': { b: 1 } }; - expectEqual(keys(obj2, true), ['a\\.', 'a\\..b']); - var obj3 = { a: { 'b.d': { c: 1 } } }; - expectEqual(keys(obj3), ['a.b\\.d.c']); - var obj4 = { a: { 'b.d': { c: 1 } } }; - expectEqual(keys(obj4, true), ['a','a.b\\.d', 'a.b\\.d.c']); - }); +test('should return deep keys including intermediate parent keys', (t) => { + const object1 = { + a: 1, + b: {c: 1}, + c: {d: {e: 1}, f: 1}, + d: {e: {f: {g: 1, h: 2}}}, + e: 2, + f: {g: []}, + }; + t.deepEqual(keys(object1, true), [ + 'a', + 'b', + 'b.c', + 'c', + 'c.d', + 'c.d.e', + 'c.f', + 'd', + 'd.e', + 'd.e.f', + 'd.e.f.g', + 'd.e.f.h', + 'e', + 'f', + 'f.g', + ]); +}); +test('should escape . in key names', (t) => { + const object1 = {a: {'.b': 1}}; + t.deepEqual(keys(object1), ['a.\\.b']); + const object2 = {'a.': {b: 1}}; + t.deepEqual(keys(object2, true), ['a\\.', 'a\\..b']); + const object3 = {a: {'b.d': {c: 1}}}; + t.deepEqual(keys(object3), ['a.b\\.d.c']); + const object4 = {a: {'b.d': {c: 1}}}; + t.deepEqual(keys(object4, true), ['a', 'a.b\\.d', 'a.b\\.d.c']); });