diff --git a/src/deepFreeze.ts b/src/deepFreeze.ts new file mode 100644 index 0000000..e3106f3 --- /dev/null +++ b/src/deepFreeze.ts @@ -0,0 +1,59 @@ +import type { AnyFunction } from './types/common.js'; + +export type DeepFreezeOptions = { + filterProperties: Set; + doNotFreeze: WeakSet; + frozen: WeakSet; +}; + +/** + * Deep freeze an object or array. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze + */ +export function deepFreeze( + input: Input, + partialOptions: Partial = {}, +): Readonly { + const { + filterProperties = new Set(['prototype', '__proto__']), + doNotFreeze = new WeakSet(typeof globalThis !== 'undefined' ? [globalThis] : []), + frozen = new WeakSet(), + } = partialOptions; + + if (doNotFreeze.has(input)) { + return input; + } + + const shouldFreeze = (value: unknown): value is object | AnyFunction => { + return ((value && typeof value === 'object') || typeof value === 'function') && !doNotFreeze.has(value); + }; + + const freeze = (inputObject: InputObject): Readonly => { + if (Array.isArray(inputObject)) { + inputObject.forEach((item) => { + if (shouldFreeze(item)) { + freeze(item); + } + }); + } else { + const properties = Reflect.ownKeys(inputObject).filter((prop) => !filterProperties.has(prop)); + + for (const propertyKey of properties) { + const propertyValue = Reflect.get(inputObject, propertyKey); + + if (shouldFreeze(propertyValue)) { + freeze(propertyValue); + } + } + } + + if (!frozen.has(inputObject)) { + frozen.add(Object.freeze(inputObject)); + } + + return inputObject; + }; + + return freeze(input); +} diff --git a/src/index.ts b/src/index.ts index ea8c570..3f6fa17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ export * from './compose.js'; export * from './convert.js'; export * from './counter.js'; export * from './deepDecodeURI.js'; +export * from './deepFreeze.js'; export * from './delay.js'; export * from './delayAnimationFrame.js'; export * from './delayAnimationFrames.js'; diff --git a/test/deepFreeze.test.ts b/test/deepFreeze.test.ts new file mode 100644 index 0000000..264a828 --- /dev/null +++ b/test/deepFreeze.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest'; + +import { deepFreeze } from '../src/deepFreeze.js'; + +describe('deepFreeze()', () => { + it('deep freezes an object', () => { + const data = deepFreeze({ + name: 'Test Testerson', + job: { + title: 'Fake User', + }, + }); + + expect(Object.isFrozen(data)).toBeTruthy(); + expect(Object.isFrozen(data.job)).toBeTruthy(); + expect(Object.isFrozen(data.job.title)).toBeTruthy(); + }); + + it('deep freezes an array', () => { + const data = deepFreeze([ + { + name: 'Name', + }, + ]); + + expect(Object.isFrozen(data)).toBeTruthy(); + expect(data.every((item) => Object.isFrozen(item))).toBeTruthy(); + }); + + it('does not freeze globalThis', () => { + const data = deepFreeze(globalThis); + + expect(Object.isFrozen(data)).toBeFalsy(); + }); +});