Skip to content

Commit

Permalink
chore: add esbuild & improve architecture
Browse files Browse the repository at this point in the history
  • Loading branch information
javier-sierra-sngular committed Nov 16, 2023
1 parent 58cf0af commit 8f97ddf
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 135 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
dist-types/
.DS_Store
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<span class="badge-npmversion"><a href="https://www.npmjs.com/package/replace-circular-object" title="View this project on NPM"><img src="https://img.shields.io/npm/v/replace-circular-object.svg" alt="NPM version" /></a></span>

## Summary

Retrieve a duplicate of the given object, eliminating any circular references.
Expand All @@ -9,6 +11,10 @@ If no depth limit is specified, the function will replace circular references at

> CommonJS & ESM supported
## Requirements

Node >= 12

## Installation

> npm i replace-circular-object
Expand All @@ -26,4 +32,4 @@ const replacedObject = replaceCircularObject(circularObject, 'Default Value!', 3
console.log(replacedObject); // { property: { property: { property: 'Default Value!' } } }
```

More examples at [test/test.js](test/test.js)
More examples at [test/index.test.js](test/index.test.js)
92 changes: 0 additions & 92 deletions index.cjs

This file was deleted.

86 changes: 86 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 26 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
{
"name": "replace-circular-object",
"version": "1.0.0",
"version": "1.0.1",
"description": "Retrieve a duplicate of the given object, eliminating any circular references.",
"main": "index.mjs",
"types": "types/index.d.ts",
"main": "src/index.js",
"type": "module",
"engines": {
"node": ">=12.0.0"
},
"types": "./dist-types/index.d.ts",
"exports": {
"import": "./index.mjs",
"require": "./index.cjs"
".": {
"require": "./dist/index.cjs",
"default": "./src/index.js"
}
},
"files": [
"dist/",
"dist-types/",
"src/",
"LICENSE",
"README.md"
],
"repository": {
"type": "git",
"url": "https://github.com/javier-sierra-sngular/replace-circular-object"
Expand All @@ -19,6 +31,14 @@
"author": "Javi",
"license": "MIT",
"scripts": {
"test": "node test/test.js"
"build": "npm run build:commonjs && npm run build:types",
"build:commonjs": "esbuild --format=cjs --platform=node --target=node10 --outfile=dist/index.cjs src/index.js",
"build:types": "tsc --build --pretty",
"test": "node test/index.test.js && node test/index.test.cjs",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"esbuild": "0.19.5",
"typescript": "^5.2.2"
}
}
44 changes: 23 additions & 21 deletions index.mjs → src/index.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,62 @@
'use strict';

class CircularReferenceReplacer {
#depthLimit;
#defaultValue;
/**
* @param {*} defaultValue - The default value to use for replacing circular references.
* @param {number} depthLimit - The maximum number of circular references allowed before using the default value.
*/
constructor(defaultValue, depthLimit) {
this._depthLimit = depthLimit;
this._defaultValue = defaultValue;
this.#depthLimit = depthLimit;
this.#defaultValue = defaultValue;
}

replace(obj) {
return this._replaceRecursively(obj, []);
return this.#replaceRecursively(obj, []);
}

_replaceRecursively(obj, ancestors) {
if (!this._isJSONObjectOrArray(obj)) {
#replaceRecursively(obj, ancestors) {
if (!this.#isJSONObjectOrArray(obj)) {
return obj;
}
if (this._hasCircularReferences(obj, ancestors) && this._exceedsDepthLimit(obj, ancestors)) {
return this._getDefaultValue(obj);
if (this.#hasCircularReferences(obj, ancestors) && this.#exceedsDepthLimit(obj, ancestors)) {
return this.#getDefaultValue(obj);
}
return this._doReplace(obj, ancestors);
return this.#doReplace(obj, ancestors);
}

_doReplace(obj, ancestors) {
#doReplace(obj, ancestors) {
ancestors.push(obj); // push the original reference
obj = Array.isArray(obj) ? [...obj] : { ...obj }; // copy
for (const key of Object.keys(obj)) {
obj[key] = this._replaceRecursively(
obj[key] = this.#replaceRecursively(
obj[key],
[...ancestors] // copy of ancestors avoiding mutation by siblings
);
}
return obj;
}

_hasCircularReferences(obj, ancestors) {
#hasCircularReferences(obj, ancestors) {
const hasCircularReferences = ancestors.includes(obj);
return hasCircularReferences;
}

_exceedsDepthLimit(obj, ancestors) {
const exceedsCircularReferencesLimit = this._countOccurrences(obj, ancestors) >= this._depthLimit;
#exceedsDepthLimit(obj, ancestors) {
const exceedsCircularReferencesLimit = this.#countOccurrences(obj, ancestors) >= this.#depthLimit;
return exceedsCircularReferencesLimit;
}

_countOccurrences(value, array) {
#countOccurrences(value, array) {
const count = array.reduce(
(accumulator, currentValue) => (currentValue === value ? accumulator + 1 : accumulator),
0
);
return count;
}

_isJSONObjectOrArray(obj) {
#isJSONObjectOrArray(obj) {
return (
typeof obj === 'object' &&
obj !== null &&
Expand All @@ -66,24 +68,24 @@ class CircularReferenceReplacer {
);
}

_getDefaultValue(obj) {
if (this._defaultValue === undefined) {
#getDefaultValue(obj) {
if (this.#defaultValue === undefined) {
return Array.isArray(obj) ? [] : {};
}
return this._defaultValue;
return this.#defaultValue;
}
}

/**
* Retrieve a duplicate of the given object, eliminating any circular references.
* Replaces circular references with a default value if the number of circular references exceeds the depth limit.
* @function
* @param {*} obj - The object to replace circular references in.
* @param {Object} obj - The object to replace circular references in.
* @param {*} [defaultValue] - The default value to use for replacing circular references. If no one is provided, the default value is the empty object or array.
* @param {number} [depthLimit = 0] - The maximum number of circular references allowed before using the default value.
* @returns {*} - The object without circular references.
* @returns {Object} - The object without circular references.
*/
function replaceCircularObject(obj, defaultValue, depthLimit = 0) {
export function replaceCircularObject(obj, defaultValue, depthLimit = 0) {
const circularReferenceReplacer = new CircularReferenceReplacer(defaultValue, depthLimit);
const objWithoutCircularReferences = circularReferenceReplacer.replace(obj);
return objWithoutCircularReferences;
Expand Down
25 changes: 25 additions & 0 deletions test/index.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { replaceCircularObject } = require('../dist/index.cjs');
const assert = require('node:assert');

const circularObject = {};
circularObject.property = circularObject;

const circularArray = [];
circularArray.push(circularArray);

const replacedObjectNoDefault = replaceCircularObject(circularObject);
assert.deepEqual(replacedObjectNoDefault, { property: {} });

const replacedObjectWithDefault = replaceCircularObject(circularObject, 'Default Value!', 3);
assert.deepEqual(replacedObjectWithDefault, { property: { property: { property: 'Default Value!' } } });

const replacedArrayNoDefault = replaceCircularObject(circularArray);
assert.deepEqual(replacedArrayNoDefault, [[]]);

const replacedArrayWithDepth = replaceCircularObject(circularArray, undefined, 5);
assert.deepEqual(replacedArrayWithDepth, [[[[[[]]]]]]);

console.log('---------------------------------------------');
console.log('🚀🚀🚀🚀🚀 CommonJS tests passed! 🚀🚀🚀🚀🚀');
console.log('---------------------------------------------');
console.log('');
Loading

0 comments on commit 8f97ddf

Please sign in to comment.