From ec796354de4d02865abe876add35451dc621c62a Mon Sep 17 00:00:00 2001 From: Stephen Isienyi Date: Sat, 1 Jun 2024 12:47:04 -0700 Subject: [PATCH 1/2] secure changes --- docs-dev | 2 +- src/model/accessor-cache/index.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs-dev b/docs-dev index 8ab3efa..1d5a633 160000 --- a/docs-dev +++ b/docs-dev @@ -1 +1 @@ -Subproject commit 8ab3efaf3ed568c3ce154592c3640c031b6d74c3 +Subproject commit 1d5a6337b145d95d2f3872d75d2be6ab12da77db diff --git a/src/model/accessor-cache/index.ts b/src/model/accessor-cache/index.ts index 6b15906..1145588 100644 --- a/src/model/accessor-cache/index.ts +++ b/src/model/accessor-cache/index.ts @@ -16,7 +16,8 @@ interface PropertyOriginInfo { value: any }; -import { isEmpty, isEqual } from 'lodash'; +import isEmpty from 'lodash.isempty'; +import isEqual from 'lodash.isequal'; import { GLOBAL_SELECTOR } from '../../constants'; From 0f1a79f0f75462f52096b7fdeebdc6b8d5889f15 Mon Sep 17 00:00:00 2001 From: Stephen Isienyi Date: Sun, 2 Jun 2024 01:37:19 -0700 Subject: [PATCH 2/2] utils migration and lodash to peer dependencies conversion --- package-lock.json | 63 +++++-- package.json | 16 +- post-build.js | 14 +- src/connection.test.ts | 12 +- src/main.test.ts | 8 + src/main.ts | 2 +- src/model/accessor-cache/index.test.ts | 18 +- src/model/accessor-cache/index.ts | 4 +- src/model/accessor/test/index.test.ts | 22 ++- src/model/atom/index.test.ts | 7 + src/model/atom/index.ts | 4 +- src/set/index.test.ts | 23 ++- src/set/index.ts | 7 +- src/set/tag-functions/index.test.ts | 11 +- src/set/tag-functions/index.ts | 17 +- src/test-artifacts/data/create-data-obj.ts | 2 +- src/test-artifacts/utils.ts | 2 +- src/utils/clonedeep-eligibility-check.ts | 57 ------- .../clonedeep-eligibility-checks.test.ts | 37 ----- src/utils/index.test.ts | 157 ++---------------- src/utils/index.ts | 80 +-------- tsconfig.json | 2 +- 22 files changed, 188 insertions(+), 377 deletions(-) delete mode 100644 src/utils/clonedeep-eligibility-check.ts delete mode 100644 src/utils/clonedeep-eligibility-checks.test.ts diff --git a/package-lock.json b/package-lock.json index 1f33370..191cc2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@webkrafters/auto-immutable", - "version": "0.0.1", + "version": "1.0.1-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@webkrafters/auto-immutable", - "version": "0.0.1", + "version": "1.0.1-rc.0", "license": "MIT", "dependencies": { - "@webkrafters/get-property": "^1.1.2", - "lodash": "^4.17.21" + "@webkrafters/clone-total": "^1.0.1", + "@webkrafters/get-property": "^1.1.2" }, "devDependencies": { "@babel/cli": "^7.17.0", @@ -21,12 +21,20 @@ "@babel/preset-env": "^7.20.2", "@types/jest": "^29.5.12", "@types/lodash": "^4.17.0", - "@types/node": "^20.11.28", + "@types/node": "^20.13.0", "babel-loader": "^8.2.5", "jest": "^29.7.0", + "lodash.isempty": "^4.4.0", + "lodash.isequal": "^4.5.0", + "lodash.isplainobject": "^4.0.6", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.4.2" + }, + "peerDependencies": { + "lodash.isempty": ">= 0.1.0", + "lodash.isequal": ">= 0.1.0", + "lodash.isplainobject": ">= 0.8.0" } }, "node_modules/@ampproject/remapping": { @@ -2908,13 +2916,12 @@ "node_modules/@types/lodash": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", - "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", - "dev": true + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" }, "node_modules/@types/node": { - "version": "20.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz", - "integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==", + "version": "20.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.13.0.tgz", + "integrity": "sha512-FM6AOb3khNkNIXPnHFDYaHerSv8uN22C91z098AnGccVu+Pcdhi+pNUFDi0iLmPIsVE0JBD0KVS7mzUYt4nRzQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -3102,6 +3109,15 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webkrafters/clone-total": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@webkrafters/clone-total/-/clone-total-1.0.1.tgz", + "integrity": "sha512-wCOcGorgkUmbUeU8DApJ40uAcxbeikniN89xuQy9jOuKvHxGh45ruoMhRfLef7IMu2zrCLZzkjZht0dGMlEpKA==", + "peerDependencies": { + "@types/lodash": ">= 4.x", + "lodash.clonedeepwith": ">= 4.x" + } + }, "node_modules/@webkrafters/get-property": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@webkrafters/get-property/-/get-property-1.1.2.tgz", @@ -7440,10 +7456,11 @@ "node": ">=6" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "node_modules/lodash.clonedeepwith": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeepwith/-/lodash.clonedeepwith-4.5.0.tgz", + "integrity": "sha512-QRBRSxhbtsX1nc0baxSkkK5WlVTTm/s48DSukcGcWZwIyI8Zz+lB+kFiELJXtzfH4Aj6kMWQ1VWW4U5uUDgZMA==", + "peer": true }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -7451,6 +7468,24 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", diff --git a/package.json b/package.json index 669416e..e21acda 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "steveswork (https://github.com/steveswork)" ], "dependencies": { - "@webkrafters/get-property": "^1.1.2", - "lodash": "^4.17.21" + "@webkrafters/clone-total": "^1.0.1", + "@webkrafters/get-property": "^1.1.2" }, "description": "Auto Immutable - self enforcing immutable data structure", "devDependencies": { @@ -24,9 +24,12 @@ "@babel/preset-env": "^7.20.2", "@types/jest": "^29.5.12", "@types/lodash": "^4.17.0", - "@types/node": "^20.11.28", + "@types/node": "^20.13.0", "babel-loader": "^8.2.5", "jest": "^29.7.0", + "lodash.isempty": "^4.4.0", + "lodash.isequal": "^4.5.0", + "lodash.isplainobject": "^4.0.6", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.4.2" @@ -44,6 +47,11 @@ "license": "MIT", "main": "dist/index.js", "name": "@webkrafters/auto-immutable", + "peerDependencies": { + "lodash.isempty": ">= 0.1.0", + "lodash.isequal": ">= 0.1.0", + "lodash.isplainobject": ">= 0.8.0" + }, "publishConfig": { "access": "public" }, @@ -60,5 +68,5 @@ "test:watch": "jest --updateSnapshot --watchAll" }, "types": "dist/index.d.ts", - "version": "1.0.0" + "version": "1.0.1-rc.0" } diff --git a/post-build.js b/post-build.js index a81d414..54caf84 100644 --- a/post-build.js +++ b/post-build.js @@ -2,18 +2,20 @@ var fs = require( 'fs' ); var path = require( 'path' ); const { promisify } = require( 'util' ); +const copyFile = promisify( fs.copyFile ); const read = promisify( fs.readFile ); const write = promisify( fs.writeFile ); +const LOGO_FILENAME = 'logo.svg'; + +const LOGO_SOURCEPATH = path.join( 'docs-dev', 'src', 'images', LOGO_FILENAME ); + const fOpts = { encoding: 'utf8' }; Promise .allSettled([ - read( - path.join( 'docs-dev', 'src', 'images', 'logo.svg' ), - fOpts - ), - read( 'logo.svg', fOpts ) + read( LOGO_SOURCEPATH, fOpts ), + read( LOGO_FILENAME, fOpts ) ]) .then(([ officialLogo, appLogo ]) => { if( officialLogo.reason ) { @@ -21,7 +23,7 @@ Promise } if( appLogo.reason ) { appLogo.value = '' } if( appLogo.value === officialLogo.value ) { return } - write( 'logo.svg', officialLogo.value, fOpts ); + return copyFile( LOGO_SOURCEPATH, LOGO_FILENAME ); }) .catch( e => { console.log( 'FAILED TO PROCESS LOGO TRANSFER\n', e ); diff --git a/src/connection.test.ts b/src/connection.test.ts index 00b3054..f1cd7d3 100644 --- a/src/connection.test.ts +++ b/src/connection.test.ts @@ -1,3 +1,11 @@ +import { + beforeAll, + describe, + expect, + jest, + test +} from '@jest/globals'; + import AccessorCache from './model/accessor-cache'; import { Immutable } from './main'; import { deps, Connection } from './connection'; @@ -100,7 +108,7 @@ describe( 'Connection class', () => { .spyOn( deps, 'setValue' ) .mockReturnValue( undefined ); - connection.get( expect.any( Array ) ); + connection.get( expect.any( Array ) as unknown as string ); expect( cacheGetSpy ).toHaveBeenCalledTimes( 1 ); connection.set( {} ); expect( setSpy ).toHaveBeenCalledTimes( 1 ); @@ -111,7 +119,7 @@ describe( 'Connection class', () => { connection.disconnect(); expect( connection.disconnected ).toBe( true ); - connection.get( expect.any( Array ) ); + connection.get( expect.any( Array ) as unknown as string ); expect( cacheGetSpy ).not.toHaveBeenCalled(); connection.set( {} ); expect( setSpy ).not.toHaveBeenCalled(); diff --git a/src/main.test.ts b/src/main.test.ts index 4b29fb5..54074a6 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -1,3 +1,11 @@ +import { + beforeAll, + describe, + expect, + jest, + test +} from '@jest/globals'; + import { Connection } from './connection'; import { Closable, Immutable } from './main'; diff --git a/src/main.ts b/src/main.ts index 28d6410..1fa3900 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ import type { Value } from '.'; import * as _constants from './constants'; -import { clonedeep } from './utils'; +import clonedeep from '@webkrafters/clone-total'; import AccessorCache from './model/accessor-cache'; diff --git a/src/model/accessor-cache/index.test.ts b/src/model/accessor-cache/index.test.ts index a9cca20..e3e419b 100644 --- a/src/model/accessor-cache/index.test.ts +++ b/src/model/accessor-cache/index.test.ts @@ -1,9 +1,20 @@ +import { + afterAll, + beforeAll, + describe, + expect, + jest, + test +} from '@jest/globals'; + import type { SourceData } from '../../test-artifacts/data/create-data-obj'; type Value = Partial; +import clonedeep from '@webkrafters/clone-total'; + import * as constants from '../../constants'; -import { clonedeep } from '../../utils'; + import AccessorCache from '.'; import _createSourceData from '../../test-artifacts/data/create-data-obj'; @@ -177,7 +188,10 @@ describe( 'AccessorCache class', () => { }, 'tags[4]': 'ullamco' }; - const retVal = cache.get( expect.any( String ), ...accessorPaths[ 0 ] ); + const retVal = cache.get( + expect.any( String ) as unknown as string, + ...accessorPaths[ 0 ] + ); test( 'is a compiled slice of value object as referenced in the propertyPaths', () => { expect( retVal ).toEqual( retValExpected ); } ); diff --git a/src/model/accessor-cache/index.ts b/src/model/accessor-cache/index.ts index 1145588..9573cd4 100644 --- a/src/model/accessor-cache/index.ts +++ b/src/model/accessor-cache/index.ts @@ -19,9 +19,9 @@ interface PropertyOriginInfo { import isEmpty from 'lodash.isempty'; import isEqual from 'lodash.isequal'; -import { GLOBAL_SELECTOR } from '../../constants'; +import getProperty from '@webkrafters/get-property'; -import { getProperty } from '../../utils'; +import { GLOBAL_SELECTOR } from '../../constants'; import Atom from '../atom'; import Accessor from '../accessor'; diff --git a/src/model/accessor/test/index.test.ts b/src/model/accessor/test/index.test.ts index 8205c3a..8fe63b4 100644 --- a/src/model/accessor/test/index.test.ts +++ b/src/model/accessor/test/index.test.ts @@ -1,4 +1,12 @@ -import { getProperty } from '../../../utils'; +import { + beforeAll, + describe, + expect, + test +} from '@jest/globals'; + +import getProperty from '@webkrafters/get-property'; + import Atom from '../../atom'; import Accessor from '..'; @@ -66,13 +74,13 @@ describe( 'Accessor class', () => { describe( 'addClient(...)', () => { test( 'adds new client id to `clients`', () => { const numClients = accessor.numClients; - const id = expect.any( String ); + const id = expect.any( String ) as unknown as string; accessor.addClient( id ); expect( accessor.numClients ).toBe( numClients + 1 ); accessor.removeClient( id ); } ); test( 'ignores requests to add existing clients', () => { - const id = expect.any( String ); + const id = expect.any( String ) as unknown as string; accessor.addClient( id ); const numClients = accessor.numClients; accessor.addClient( id ); @@ -83,12 +91,12 @@ describe( 'Accessor class', () => { } ); describe( 'hasClient(...)', () => { test( 'returns `false` if client not found in `clients`', () => { - const id = expect.any( String ); + const id = expect.any( String ) as unknown as string; accessor.removeClient( id ); expect( accessor.hasClient( id ) ).toBe( false ); } ); test( 'returns `true` if client found in `clients`', () => { - const id = expect.any( String ); + const id = expect.any( String ) as unknown as string; accessor.addClient( id ); expect( accessor.hasClient( id ) ).toBe( true ); accessor.removeClient( id ); @@ -96,14 +104,14 @@ describe( 'Accessor class', () => { } ); describe( 'removeClient(...)', () => { test( 'removes client id from `clients`', () => { - const id = expect.any( String ); + const id = expect.any( String ) as unknown as string; accessor.addClient( id ); const numClients = accessor.numClients; accessor.removeClient( id ); expect( accessor.numClients ).toBe( numClients - 1 ); } ); test( 'ignores requests to remove non-existing clients', () => { - const id = expect.any( String ); + const id = expect.any( String ) as unknown as string; accessor.addClient( id ); accessor.removeClient( id ); const numClients = accessor.numClients; diff --git a/src/model/atom/index.test.ts b/src/model/atom/index.test.ts index 106f692..b6494ae 100644 --- a/src/model/atom/index.test.ts +++ b/src/model/atom/index.test.ts @@ -1,3 +1,10 @@ +import { + beforeAll, + describe, + expect, + test +} from '@jest/globals'; + import Atom from '.'; describe( 'Atom class', () => { diff --git a/src/model/atom/index.ts b/src/model/atom/index.ts index fa85cac..90ce502 100644 --- a/src/model/atom/index.ts +++ b/src/model/atom/index.ts @@ -1,4 +1,6 @@ -import { clonedeep, makeReadonly } from '../../utils'; +import clonedeep from '@webkrafters/clone-total'; + +import { makeReadonly } from '../../utils'; const isFunction = (() => { const toString = Function.prototype.toString; diff --git a/src/set/index.test.ts b/src/set/index.test.ts index e42bcec..4c8131b 100644 --- a/src/set/index.test.ts +++ b/src/set/index.test.ts @@ -1,3 +1,14 @@ +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + jest, + test +} from '@jest/globals'; + import type { Changes } from '..'; import type { SourceData } from '../test-artifacts/data/create-data-obj'; @@ -12,7 +23,7 @@ import { SPLICE_TAG } from '../constants'; -import { clonedeep } from '../utils'; +import clonedeep from '@webkrafters/clone-total'; import setValue from '.'; @@ -1145,17 +1156,19 @@ describe( 'setValue(...)', () => { afterEach(() => { delete value.testing }); + interface ExpectVal { value: Array|Record } + describe.each` tag | desc | expected ${ CLEAR_TAG } | ${ 'as a string tag' } | ${ {} } ${ [ CLEAR_TAG ] } | ${ 'in array payload' } | ${ { value: [ undefined ] } } ${ { 1: CLEAR_TAG } } | ${ 'in indexed-object payload' } | ${ { value: [ undefined, undefined ] } } - ${ { places: [ CLEAR_TAG, CLEAR_TAG ] } } | ${ 'in a nested payload' } | ${ { value: { places: [ undefined, undefined ] } } } + ${ { places: [ CLEAR_TAG, CLEAR_TAG ] } } | ${ 'in a nested payload' } | ${ { value: { places: [ undefined, undefined ] } } } ${ { places: CLEAR_TAG } } | ${ 'as a string in a nested payload' } | ${ { value: {} } } - `( `using '${ CLEAR_TAG }' tag property key $desc`, ({ tag, expected }) => { + `( `using '${ CLEAR_TAG }' tag property key $desc`, ({ tag, expected } : Record ) => { test( 'sets shallowly embedded tag', () => { setValue( value, getShallowUpdate( tag ), onChange ); - expect( value.testing ).toEqual( expected.value ); + expect( value.testing ).toEqual( ( expected as ExpectVal ).value ); } ); test( 'sets deeply embedded tag', () => { setValue( value, getUpdate( tag ), onChange ); @@ -1163,7 +1176,7 @@ describe( 'setValue(...)', () => { } ); describe( 'from within an array slice ancestor context', () => { test( 'is supported', () => { - expected = expected.value; + expected = ( expected as ExpectVal ).value; setValue( value, getShallowUpdate([ tag ]) ); expect( value.testing ).toEqual([ expected ]); delete value.testing; diff --git a/src/set/index.ts b/src/set/index.ts index 4883c11..97b0cf4 100644 --- a/src/set/index.ts +++ b/src/set/index.ts @@ -7,9 +7,12 @@ import type { Value } from '..'; -import { isEqual, isPlainObject } from 'lodash'; +import isEqual from 'lodash.isequal'; +import isPlainObject from 'lodash.isplainobject'; -import { clonedeep, isDataContainer } from '../utils'; +import clonedeep from '@webkrafters/clone-total'; + +import { isDataContainer } from '../utils'; import tagFunctions, { isArrayOnlyTag, diff --git a/src/set/tag-functions/index.test.ts b/src/set/tag-functions/index.test.ts index 2db6628..b5c2166 100644 --- a/src/set/tag-functions/index.test.ts +++ b/src/set/tag-functions/index.test.ts @@ -1,3 +1,12 @@ +import { + beforeAll, + beforeEach, + describe, + expect, + jest, + test +} from '@jest/globals'; + import type { SourceData } from '../../test-artifacts/data/create-data-obj'; import { @@ -10,7 +19,7 @@ import { SPLICE_TAG } from '../../constants'; -import { clonedeep } from '../../utils'; +import clonedeep from '@webkrafters/clone-total'; import * as tag from '.'; diff --git a/src/set/tag-functions/index.ts b/src/set/tag-functions/index.ts index 0b180ee..b8b6bd0 100644 --- a/src/set/tag-functions/index.ts +++ b/src/set/tag-functions/index.ts @@ -29,11 +29,9 @@ type TagFunction = < changes? : TaggedChanges ) => void; -import { - isEmpty, - isEqual, - isPlainObject -}from 'lodash'; +import isEmpty from 'lodash.isempty'; +import isEqual from 'lodash.isequal'; +import isPlainObject from 'lodash.isplainobject'; import { CLEAR_TAG, @@ -45,11 +43,10 @@ import { SPLICE_TAG } from '../../constants'; -import { - clonedeep, - getProperty, - isDataContainer -} from '../../utils/index'; +import clonedeep from '@webkrafters/clone-total'; +import getProperty from '@webkrafters/get-property'; + +import { isDataContainer } from '../../utils/index'; /** * Sets a value slice to its empty value equivalent diff --git a/src/test-artifacts/data/create-data-obj.ts b/src/test-artifacts/data/create-data-obj.ts index 5074d67..5874ea5 100644 --- a/src/test-artifacts/data/create-data-obj.ts +++ b/src/test-artifacts/data/create-data-obj.ts @@ -1,4 +1,4 @@ -import { clonedeep } from '../../utils'; +import clonedeep from '@webkrafters/clone-total'; const sourceData = { _id: '639737cc5ac1df69cda79413', diff --git a/src/test-artifacts/utils.ts b/src/test-artifacts/utils.ts index 58ff3f5..487e5c7 100644 --- a/src/test-artifacts/utils.ts +++ b/src/test-artifacts/utils.ts @@ -1,4 +1,4 @@ -import { isPlainObject } from 'lodash'; +import isPlainObject from 'lodash.isplainobject'; export const isReadonly = v => { let isReadonly = true; diff --git a/src/utils/clonedeep-eligibility-check.ts b/src/utils/clonedeep-eligibility-check.ts deleted file mode 100644 index f672752..0000000 --- a/src/utils/clonedeep-eligibility-check.ts +++ /dev/null @@ -1,57 +0,0 @@ -export interface VerifyData { - isEligible : boolean; - typeName : string; - value : T; -}; - -export type CloneableType = keyof typeof CloneableMap; - -type ICloneableTags = {[K in CloneableType]: null}; - -export interface CloneableTags extends ICloneableTags {}; - -export default verify; - -export const CloneableMap = { - Arguments: null, - Array: null, - ArrayBuffer: null, - Boolean: null, - DataView: null, - Date: null, - Error: null, - Function: null, - Float32Array: null, - Float64Array: null, - GeneratorFunction: null, - Int8Array: null, - Int16Array: null, - Int32Array: null, - Map: null, - Number: null, - Object: null, - Promise: null, - RegExp: null, - Set: null, - String: null, - Symbol: null, - Uint8Array: null, - Uint8ClampedArray: null, - Uint16Array: null, - Uint32Array: null, - WeakMap: null -}; - -export const CLONEABLE_TAGS = Object.freeze( CloneableMap as CloneableTags ); - -function verify( value : T ) : VerifyData { - let typeName = value?.constructor.name; - let isEligible; - if( typeName !== undefined ) { - isEligible = typeName in CLONEABLE_TAGS; - } else { - typeName = `${ value }`; - isEligible = true; - } - return { isEligible, typeName, value }; -} diff --git a/src/utils/clonedeep-eligibility-checks.test.ts b/src/utils/clonedeep-eligibility-checks.test.ts deleted file mode 100644 index a73cef1..0000000 --- a/src/utils/clonedeep-eligibility-checks.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import verify from './clonedeep-eligibility-check'; - -describe( 'verify(...)', () => { - test( 'is positive for values with known types', () => { - const value = 8; - expect( verify( value ) ).toEqual({ - isEligible: true, - typeName: 'Number', - value - }); - } ); - test( 'is positive for null', () => { - const value = null; - expect( verify( value ) ).toEqual({ - isEligible: true, - typeName: 'null', - value - }); - } ); - test( 'is positive for undefined', () => { - const value = undefined; - expect( verify( value ) ).toEqual({ - isEligible: true, - typeName: 'undefined', - value - }); - } ); - test( 'is negative for values with unknown types', () => { - class Test {} - const value = new Test(); - expect( verify( value ) ).toEqual({ - isEligible: false, - typeName: 'Test', - value - }); - } ); -} ); diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index e8fb372..84e5f86 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -1,7 +1,13 @@ +import { + beforeAll, + describe, + expect, + test +} from '@jest/globals'; + import '../test-artifacts/suppress-render-compat'; import * as utils from '.'; -import createSourceData from '../test-artifacts/data/create-data-obj'; describe( 'utils module', () => { describe( 'arrangePropertyPaths(...)', () => { @@ -82,73 +88,6 @@ describe( 'utils module', () => { } ); } ); } ); - describe( 'clonedeep(...)', () => { - test( 'produces exact clone for commonly used types', () => { - const value = createSourceData(); - const clone = utils.clonedeep( value ); - expect( clone ).not.toBe( value ); - expect( clone ).toStrictEqual( value ); - } ); - test( 'produces exact clone for recognized web api instances ', () => { - const value = { - birth: { - date: new Date( '1952-09-05' ), - place: { city: 'Prague' } - }, - null: null, - regexCount: /[1-9][0-9]*/g, - undefined: undefined - }; - const clone = utils.clonedeep( value ); - expect( clone.birth.date ).not.toBe( value.birth.date ); - expect( clone.birth.place ).not.toBe( value.birth.place ); - expect( clone.regexCount ).not.toBe( value.regexCount ); - expect( clone ).not.toBe( value ); - expect( clone ).toStrictEqual( value ); - } ); - test( 'will not clone but will return unrecognized instances not implementing either `clone` or `cloneNode` methods', () => { - class Test {}; - const value = { testing: { test: new Test() } }; - const clone = utils.clonedeep( value ); - expect( clone.testing.test ).toBe( value.testing.test ); // not cloned: returned as is - expect( clone ).not.toBe( value ); - expect( clone ).toStrictEqual( value ); - } ); - describe( 'cloning unrecognizable instance', () => { - const runWith = ( value, cloneWatcher ) => { - const clone = utils.clonedeep( value ); - expect( clone.testing.test ).not.toBe( value.testing.test ); - expect( cloneWatcher ).toHaveBeenCalled(); - expect( clone ).not.toBe( value ); - expect( clone ).toStrictEqual( value ); - }; - test( 'using its `clone` method', () => { - const cloneWatcher = jest.fn(); - class Test { - clone() { - cloneWatcher(); - return new Test(); - } - }; - runWith({ testing: { test: new Test() } }, cloneWatcher ); - } ); - test( 'using its `cloneNode` method', () => { - const cloneWatcher = jest.fn(); - class Test { - cloneNode() { - cloneWatcher(); - return new Test(); - } - }; - runWith({ testing: { test: new Test() } }, cloneWatcher ); - } ); - } ); - } ); - describe( 'getProperty(...)', () => { - test( 'is a `webkrafters/get-property` default module reference', () => { - expect( utils.getProperty ).toBe( require( '@webkrafters/get-property' ).default ); - } ); - } ); describe( 'isDataContainer(...)', () => { test( 'is true for arrays', () => { expect( utils.isDataContainer( [] ) ).toBe( true ); @@ -172,97 +111,27 @@ describe( 'utils module', () => { } ); describe( 'makeReadonly(...)', () => { const TEST_DATA = { a: { b: { c: [ 1, 2, 3, { testFlag: true } ] } } }; - beforeAll(() => utils.makeReadonly( TEST_DATA )); + beforeAll(() => { utils.makeReadonly( TEST_DATA ) }); test( 'converts composite data to readonly', () => { // @ts-expect-error expect(() => { TEST_DATA.z = expect.anything() }).toThrow( 'Cannot add property z, object is not extensible' ); - expect(() => { TEST_DATA.a = expect.anything() }).toThrow( + expect(() => { TEST_DATA.a = expect.anything() as any }).toThrow( "Cannot assign to read only property 'a' of object '#'" ); - expect(() => { TEST_DATA.a.b = expect.anything() }).toThrow( + expect(() => { TEST_DATA.a.b = expect.anything() as any }).toThrow( "Cannot assign to read only property 'b' of object '#'" ); - expect(() => { TEST_DATA.a.b.c[ 1 ] = expect.anything() }).toThrow( + expect(() => { TEST_DATA.a.b.c[ 1 ] = expect.anything() as any }).toThrow( "Cannot assign to read only property '1' of object '[object Array]'" ); - expect(() => { TEST_DATA.a.b.c[ 3 ] = expect.anything() }).toThrow( + expect(() => { TEST_DATA.a.b.c[ 3 ] = expect.anything() as any }).toThrow( "Cannot assign to read only property '3' of object '[object Array]'" ); - expect(() => { TEST_DATA.a.b.c.push( expect.anything() ) }).toThrow( + expect(() => { TEST_DATA.a.b.c.push( expect.anything() as any ) }).toThrow( 'Cannot add property 4, object is not extensible' ); } ); } ); - describe( 'mapPathsToObject(...)', () => { - let source, propertyPaths; - beforeAll(() => { - source = createSourceData(); - source.matrix = [ - [ 0, 3, 9 ], - [ 4, 1, 1], - [ 8, 7, 3] - ]; - propertyPaths = Object.freeze([ - 'address', - 'friends[1]', - 'history.places.0.city', - 'matrix.0.1', - 'registered.timezone', - 'registered.time', - 'tags[4]', - 'matrix[2][2]', - 'matrix.0.2' - ]); - }); - test( 'returns a subset of the source pbject matching arranged property paths', () => { - expect( utils.mapPathsToObject( source, propertyPaths ) ).toEqual({ - address: source.address, - friends: { 1: source.friends[ 1 ] }, - history: { places: { 0: { city: source.history.places[ 0 ].city } } }, - matrix: { - 0: { 1: source.matrix[ 0 ][ 1 ], 2: source.matrix[ 0 ][ 2 ] }, - 2: { 2: source.matrix[ 2 ][ 2 ] } - }, - registered: { - time: source.registered.time, - timezone: source.registered.timezone - }, - tags: { 4: source.tags[ 4 ] } - }); - } ); - test( - 'returns a subset of the source object following setstate `change` object rules for array/indexed-object mutations', - () => expect( utils.mapPathsToObject( source, [ 'matrix.0.1', 'matrix.0.2' ] ) ).toEqual({ - matrix: { - 0: { - 1: source.matrix[ 0 ][ 1 ], - 2: source.matrix[ 0 ][ 2 ] - } - } - }) - ); - test( 'returns a subset of the source object excluding non-existent property paths', () => { - expect( utils.mapPathsToObject( source, [ 'matrix.0.1', 'matrix.0.44' ] ) ).toEqual({ - matrix: { 0: { 1: source.matrix[ 0 ][ 1 ] } } - }); - }); - test( 'handles multi-dimensional arrays', () => { - source = createSourceData(); - source.matrix = [ - [ [ 0, 3, 1 ], [ 4, 0, 3 ] ], - [ [ 4, 1, 9 ], [ 7, 4, 9 ] ], - [ [ 8, 7, 3 ], [ 0, 3, 1 ] ] - ]; - const matrix11 = { 1: source.matrix[ 1 ][ 1 ] }; - const matrix20 = { 0: source.matrix[ 2 ][ 0 ] }; - expect( utils.mapPathsToObject( source, [ 'matrix.1.1', 'matrix[2].0' ] ) ) - .toEqual({ matrix: { 1: matrix11, 2: matrix20 } }); - expect( utils.mapPathsToObject( source, [ 'matrix[2].0', 'matrix.1.1' ] ) ) - .toEqual({ matrix: { 1: matrix11, 2: matrix20 } }); - expect( utils.mapPathsToObject( source, [ 'matrix.1.1' ] ) ).toEqual({ matrix: { 1: matrix11 } }); - expect( utils.mapPathsToObject( source, [ 'matrix[2].0' ] ) ).toEqual({ matrix: { 2: matrix20 } }); - } ); - } ); } ); diff --git a/src/utils/index.ts b/src/utils/index.ts index 89d8d70..310404a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,13 +1,4 @@ -import type { CloneDeepWithCustomizer } from 'lodash'; -import type { PropertyInfo } from '@webkrafters/get-property'; - -import { cloneDeepWith, isPlainObject } from 'lodash'; - -import checkEligibility from './clonedeep-eligibility-check'; - -import get from '@webkrafters/get-property'; - -export type Transform = (property : PropertyInfo) => T; +import isPlainObject from 'lodash.isplainobject'; /** * Curates the most inclusive propertyPaths from a list of property paths. @@ -47,30 +38,6 @@ export function arrangePropertyPaths( propertyPaths : Array ) : Array { - const defaultCustomizer : CloneDeepWithCustomizer = v => { - if( v === null ) { return } - if( typeof v === 'object' ) { - if( 'clone' in v && typeof v.clone === 'function' ) { return v.clone() } - if( 'cloneNode' in v && typeof v.cloneNode === 'function' ) { return v.cloneNode( true ) } - } - if( !checkEligibility( v ).isEligible ) { return v } - } - const clone = ( - value : T , - customizer : CloneDeepWithCustomizer = defaultCustomizer - ) : R => cloneDeepWith( value, customizer ); - const clonedeep = (value: T) : R => clone( value ); - return clonedeep; -})(); - -export const getProperty = get; - /** Checks if value is either a plain object or an array */ export function isDataContainer( v ) : boolean { return isPlainObject( v ) || Array.isArray( v ) } @@ -92,48 +59,3 @@ export function makeReadonly( v : T ) : Readonly { !frozen && Object.freeze( v ); return v; }; - -const defaultFormatValue = ({ value } : PropertyInfo) : T => value; - -export const stringToDotPath = (() => { - const BRACKET_OPEN = /\.?\[/g; - const BRACKET_CLOSE = /^\.|\]/g; - const fn = ( path : string ) : string => path - .replace( BRACKET_OPEN, '.' ) - .replace( BRACKET_CLOSE, '' ); - return fn; -})(); - -/** - * Pulls propertyPath values from state and - * compiles them into a partial state object. - */ -export function mapPathsToObject( - source : T, - propertyPaths : Array, - transform : Transform = defaultFormatValue -) : Partial { - const paths = []; - for( const path of propertyPaths ) { paths.push( stringToDotPath( path ) ) } - const dest = {}; - let object = dest; - for( const path of arrangePropertyPaths( paths ) ) { - const property = getProperty( source, path ); - if( !property.exists ) { continue } - for( - let tokens = path.split( '.' ), tLen = tokens.length, t = 0; - t < tLen; - t++ - ) { - const token = tokens[ t ]; - if( t + 1 === tLen ) { - object[ token ] = transform( property ); - object = dest; - break; - } - if( !( token in object ) ) { object[ token ] = {} } - object = object[ token ]; - } - } - return dest; -} diff --git a/tsconfig.json b/tsconfig.json index 83d9ed0..13042bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ // "emitDeclarationOnly": true, "esModuleInterop": true, "module": "CommonJS", - // "moduleResolution": "node", + "moduleResolution": "node", // Types should go into this directory. // Removing this would place the .d.ts files // next to the .js files