pax_global_header00006660000000000000000000000064150637236010014515gustar00rootroot0000000000000052 comment=10b32031c6f31be38794bbe0c569e67a5f9182cd sindresorhus-map-obj-6a2faef/000077500000000000000000000000001506372360100163445ustar00rootroot00000000000000sindresorhus-map-obj-6a2faef/.editorconfig000066400000000000000000000002571506372360100210250ustar00rootroot00000000000000root = true [*] indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_style = space indent_size = 2 sindresorhus-map-obj-6a2faef/.gitattributes000066400000000000000000000000231506372360100212320ustar00rootroot00000000000000* text=auto eol=lf sindresorhus-map-obj-6a2faef/.github/000077500000000000000000000000001506372360100177045ustar00rootroot00000000000000sindresorhus-map-obj-6a2faef/.github/security.md000066400000000000000000000002631506372360100220760ustar00rootroot00000000000000# Security Policy To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. sindresorhus-map-obj-6a2faef/.github/workflows/000077500000000000000000000000001506372360100217415ustar00rootroot00000000000000sindresorhus-map-obj-6a2faef/.github/workflows/main.yml000066400000000000000000000006451506372360100234150ustar00rootroot00000000000000name: CI on: - push - pull_request jobs: test: name: Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: - 24 - 20 steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test sindresorhus-map-obj-6a2faef/.gitignore000066400000000000000000000000271506372360100203330ustar00rootroot00000000000000node_modules yarn.lock sindresorhus-map-obj-6a2faef/.npmrc000066400000000000000000000000231506372360100174570ustar00rootroot00000000000000package-lock=false sindresorhus-map-obj-6a2faef/index.d.ts000066400000000000000000000177131506372360100202560ustar00rootroot00000000000000/** Return this value from a `mapper` function to remove a key from an object. @example ``` import mapObject, {mapObjectSkip} from 'map-obj'; const object = {one: 1, two: 2}; const mapper = (key, value) => value === 1 ? [key, value] : mapObjectSkip; const result = mapObject(object, mapper); console.log(result); //=> {one: 1} ``` */ export const mapObjectSkip: unique symbol; /** Mapper function for transforming object keys and values. */ export type Mapper< SourceObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, > = ( sourceKey: Extract, sourceValue: SourceObjectType[keyof SourceObjectType], source: SourceObjectType ) => [ targetKey: MappedObjectKeyType, targetValue: MappedObjectValueType, mapperOptions?: MapperOptions, ] | typeof mapObjectSkip; /** Mapper function when `includeSymbols: true` is enabled. */ export type MapperWithSymbols< SourceObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, > = ( sourceKey: keyof SourceObjectType, sourceValue: SourceObjectType[keyof SourceObjectType], source: SourceObjectType ) => [ targetKey: MappedObjectKeyType, targetValue: MappedObjectValueType, mapperOptions?: MapperOptions, ] | typeof mapObjectSkip; /** Mapper used when `{deep: true}` is enabled. In deep mode we may visit nested objects with keys and values unrelated to the top-level object, so we intentionally widen the key and value types. */ type DeepMapper< SourceObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, > = ( sourceKey: string, sourceValue: unknown, source: SourceObjectType ) => [ targetKey: MappedObjectKeyType, targetValue: MappedObjectValueType, mapperOptions?: MapperOptions, ] | typeof mapObjectSkip; /** Deep mapper when `includeSymbols: true` is enabled. */ type DeepMapperWithSymbols< SourceObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, > = ( sourceKey: string | symbol, sourceValue: unknown, source: SourceObjectType ) => [ targetKey: MappedObjectKeyType, targetValue: MappedObjectValueType, mapperOptions?: MapperOptions, ] | typeof mapObjectSkip; export type Options = { /** Recurse nested objects and objects in arrays. @default false Built-in objects like `RegExp`, `Error`, `Date`, `Map`, `Set`, `WeakMap`, `WeakSet`, `Promise`, `ArrayBuffer`, `DataView`, typed arrays (Uint8Array, etc.), and `Blob` are not recursed into. Special objects like Jest matchers are also automatically excluded. */ readonly deep?: boolean; /** Include symbol keys in the iteration. By default, symbol keys are completely ignored and not passed to the mapper function. When enabled, the mapper will also be called with symbol keys from the source object, allowing them to be transformed or included in the result. Only enumerable symbol properties are included. @default false */ readonly includeSymbols?: boolean; /** The target object to map properties onto. @default {} */ readonly target?: Record; }; export type DeepOptions = { readonly deep: true; } & Options; export type TargetOptions> = { readonly target: TargetObjectType; } & Options; export type SymbolOptions = { readonly includeSymbols: true; } & Options; export type MapperOptions = { /** Whether to recurse into `targetValue`. Requires `deep: true`. @default true */ readonly shouldRecurse?: boolean; }; /** Map object keys and values into a new object. @param source - The source object to copy properties from. @param mapper - A mapping function. @example ``` import mapObject, {mapObjectSkip} from 'map-obj'; // Swap keys and values const newObject = mapObject({foo: 'bar'}, (key, value) => [value, key]); //=> {bar: 'foo'} // Convert keys to lowercase (shallow) const newObject = mapObject({FOO: true, bAr: {bAz: true}}, (key, value) => [key.toLowerCase(), value]); //=> {foo: true, bar: {bAz: true}} // Convert keys to lowercase (deep recursion) const newObject = mapObject({FOO: true, bAr: {bAz: true}}, (key, value) => [key.toLowerCase(), value], {deep: true}); //=> {foo: true, bar: {baz: true}} // Filter out specific values const newObject = mapObject({one: 1, two: 2}, (key, value) => value === 1 ? [key, value] : mapObjectSkip); //=> {one: 1} // Include symbol keys const symbol = Symbol('foo'); const newObject = mapObject({bar: 'baz', [symbol]: 'qux'}, (key, value) => [key, value], {includeSymbols: true}); //=> {bar: 'baz', [Symbol(foo)]: 'qux'} ``` */ // Overloads with includeSymbols: true export default function mapObject< SourceObjectType extends Record, TargetObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, >( source: SourceObjectType, mapper: DeepMapperWithSymbols, options: DeepOptions & SymbolOptions & TargetOptions ): TargetObjectType & Record; export default function mapObject< SourceObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, >( source: SourceObjectType, mapper: DeepMapperWithSymbols, options: DeepOptions & SymbolOptions ): Record; export default function mapObject< SourceObjectType extends Record, TargetObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, >( source: SourceObjectType, mapper: MapperWithSymbols< SourceObjectType, MappedObjectKeyType, MappedObjectValueType >, options: SymbolOptions & TargetOptions ): TargetObjectType & Record; export default function mapObject< SourceObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, >( source: SourceObjectType, mapper: MapperWithSymbols< SourceObjectType, MappedObjectKeyType, MappedObjectValueType >, options: SymbolOptions ): Record; // Overloads without includeSymbols (default) export default function mapObject< SourceObjectType extends Record, TargetObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, >( source: SourceObjectType, mapper: DeepMapper, options: DeepOptions & TargetOptions ): TargetObjectType & Record; export default function mapObject< SourceObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, >( source: SourceObjectType, mapper: DeepMapper, options: DeepOptions ): Record; export default function mapObject< SourceObjectType extends Record, TargetObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, >( source: SourceObjectType, mapper: Mapper< SourceObjectType, MappedObjectKeyType, MappedObjectValueType >, options: TargetOptions ): TargetObjectType & Record; export default function mapObject< SourceObjectType extends Record, MappedObjectKeyType extends string | symbol, MappedObjectValueType, >( source: SourceObjectType, mapper: Mapper< SourceObjectType, MappedObjectKeyType, MappedObjectValueType >, options?: Options ): Record; sindresorhus-map-obj-6a2faef/index.js000066400000000000000000000063121506372360100200130ustar00rootroot00000000000000const isObject = value => typeof value === 'object' && value !== null; // Check if a value is a plain object that should be recursed into const isObjectCustom = value => { if (!isObject(value)) { return false; } // Exclude built-in objects if ( value instanceof RegExp || value instanceof Error || value instanceof Date || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof ArrayBuffer || value instanceof DataView || ArrayBuffer.isView(value) // Typed arrays || (globalThis.Blob && value instanceof globalThis.Blob) ) { return false; } // Exclude Jest matchers if (typeof value.$$typeof === 'symbol' || typeof value.asymmetricMatch === 'function') { return false; } return true; }; export const mapObjectSkip = Symbol('mapObjectSkip'); const getEnumerableKeys = (object, includeSymbols) => { if (includeSymbols) { const stringKeys = Object.keys(object); const symbolKeys = Object.getOwnPropertySymbols(object).filter(symbol => Object.getOwnPropertyDescriptor(object, symbol)?.enumerable); return [...stringKeys, ...symbolKeys]; } return Object.keys(object); }; const _mapObject = (object, mapper, options, isSeen = new WeakMap()) => { const { target = {}, ...processOptions } = { deep: false, includeSymbols: false, ...options, }; if (isSeen.has(object)) { return isSeen.get(object); } isSeen.set(object, target); const mapArray = array => array.map(element => isObjectCustom(element) ? _mapObject(element, mapper, processOptions, isSeen) : element); if (Array.isArray(object)) { return mapArray(object); } for (const key of getEnumerableKeys(object, processOptions.includeSymbols)) { const value = object[key]; const mapResult = mapper(key, value); if (mapResult === mapObjectSkip) { continue; } if (!Array.isArray(mapResult)) { throw new TypeError(`Mapper must return an array or mapObjectSkip, got ${mapResult === null ? 'null' : typeof mapResult}`); } if (mapResult.length < 2) { throw new TypeError(`Mapper must return an array with at least 2 elements [key, value], got ${mapResult.length} elements`); } let [newKey, newValue, {shouldRecurse = true} = {}] = mapResult; // Drop `__proto__` keys. if (newKey === '__proto__') { continue; } if (processOptions.deep && shouldRecurse && isObjectCustom(newValue)) { newValue = Array.isArray(newValue) ? mapArray(newValue) : _mapObject(newValue, mapper, processOptions, isSeen); } try { target[newKey] = newValue; } catch (error) { if (error.name === 'TypeError' && error.message.includes('read only')) { // Skip non-configurable properties continue; } throw error; } } return target; }; export default function mapObject(object, mapper, options) { if (!isObject(object)) { throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`); } if (Array.isArray(object)) { throw new TypeError('Expected an object, got an array'); } // Ensure the third mapper argument is always the original input object const mapperWithRoot = (key, value) => mapper(key, value, object); return _mapObject(object, mapperWithRoot, options); } sindresorhus-map-obj-6a2faef/index.test-d.ts000066400000000000000000000062261506372360100212300ustar00rootroot00000000000000import {expectType, expectAssignable} from 'tsd'; import mapObject, {type Options, mapObjectSkip} from './index.js'; const options: Options = {}; const newObject = mapObject({foo: 'bar'}, (key, value) => [value, key]); expectAssignable>(newObject); expectType<'foo'>(newObject.bar); const object = mapObject({foo: 'bar'}, (key, value) => [value, key], { target: {baz: 'baz'}, }); expectAssignable<{baz: string} & Record>(object); expectType<'foo'>(object.bar); expectType(object.baz); const object1 = mapObject({foo: 'bar'}, (key, value) => [value, key], { target: {baz: 'baz'}, deep: false, }); expectAssignable<{baz: string} & Record>(object1); expectType<'foo'>(object1.bar); expectType(object1.baz); const object2 = mapObject({foo: 'bar'}, (key, value) => [String(value), key], { deep: true, }); expectAssignable>(object2); // Deep mapper parameters should be widened mapObject({fooUpper: true, bAr: {bAz: true}}, (key, value, source) => { expectAssignable(key); // Without includeSymbols, only string keys // In deep mode, source is the original input object expectType<{fooUpper: boolean; bAr: {bAz: boolean}}>(source); return [String(key), value]; }, {deep: true}); // Shallow mode: source should be the original input type mapObject({alpha: 1, beta: 2}, (key, value, source) => { expectType<{alpha: number; beta: number}>(source); return [key, value]; }); const object3 = mapObject({foo: 'bar'}, (key, value) => [String(value), key], { deep: true, target: {bar: 'baz' as const}, }); expectAssignable>(object3); expectType<'baz'>(object3.bar); mapObject({foo: 'bar'}, (key, value) => [value, key, {shouldRecurse: false}]); mapObject({foo: 'bar'}, () => mapObjectSkip); // IncludeSymbols option is available const optionsWithSymbols: Options = {includeSymbols: true}; // Test symbol key support const symbolKey = Symbol('test'); const inputWithSymbol = {foo: 'bar', [symbolKey]: 'value'}; // Test that symbol keys work const resultWithSymbols = mapObject(inputWithSymbol, (key, value) => { expectAssignable(key); // With includeSymbols, both string and symbol return [key, value]; }, {includeSymbols: true}); expectAssignable>(resultWithSymbols); // Test normal usage const resultWithoutSymbols = mapObject(inputWithSymbol, (key, value) => { expectAssignable(key); // Without includeSymbols, only string return [key, value]; }); expectAssignable>(resultWithoutSymbols); // Test deep mode with includeSymbols mapObject({fooUpper: true, [Symbol('test')]: 'symbol'}, (key, value, source) => { expectAssignable(key); // With includeSymbols in deep mode return [String(key), value]; }, {deep: true, includeSymbols: true}); // Verify that without includeSymbols, key type excludes symbols mapObject({str: 'value'}, (key: string, value) => [key, value]); // Verify that with includeSymbols, key can be symbol mapObject({str: 'value'}, (key: string | symbol, value) => [String(key), value], {includeSymbols: true}); sindresorhus-map-obj-6a2faef/license000066400000000000000000000021351506372360100177120ustar00rootroot00000000000000MIT License Copyright (c) Sindre Sorhus (https://sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sindresorhus-map-obj-6a2faef/package.json000066400000000000000000000014771506372360100206430ustar00rootroot00000000000000{ "name": "map-obj", "version": "6.0.0", "description": "Map object keys and values into a new object", "license": "MIT", "repository": "sindresorhus/map-obj", "funding": "https://github.com/sponsors/sindresorhus", "author": { "name": "Sindre Sorhus", "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, "type": "module", "exports": { "types": "./index.d.ts", "default": "./index.js" }, "sideEffects": false, "engines": { "node": ">=20" }, "scripts": { "test": "xo && ava && tsd" }, "files": [ "index.js", "index.d.ts" ], "keywords": [ "map", "object", "key", "keys", "value", "values", "iterate", "iterator", "rename", "modify", "deep", "recurse", "recursive" ], "devDependencies": { "ava": "^6.4.1", "tsd": "^0.33.0", "xo": "^1.2.2" } } sindresorhus-map-obj-6a2faef/readme.md000066400000000000000000000060731506372360100201310ustar00rootroot00000000000000# map-obj > Map object keys and values into a new object ## Install ```sh npm install map-obj ``` ## Usage ```js import mapObject, {mapObjectSkip} from 'map-obj'; // Swap keys and values const newObject = mapObject({foo: 'bar'}, (key, value) => [value, key]); //=> {bar: 'foo'} // Convert keys to lowercase (shallow) const newObject = mapObject({FOO: true, bAr: {bAz: true}}, (key, value) => [key.toLowerCase(), value]); //=> {foo: true, bar: {bAz: true}} // Convert keys to lowercase (deep recursion) const newObject = mapObject({FOO: true, bAr: {bAz: true}}, (key, value) => [key.toLowerCase(), value], {deep: true}); //=> {foo: true, bar: {baz: true}} // Filter out specific values const newObject = mapObject({one: 1, two: 2}, (key, value) => value === 1 ? [key, value] : mapObjectSkip); //=> {one: 1} // Include symbol keys const symbol = Symbol('foo'); const newObject = mapObject({bar: 'baz', [symbol]: 'qux'}, (key, value) => [key, value], {includeSymbols: true}); //=> {bar: 'baz', [Symbol(foo)]: 'qux'} ``` ## API ### mapObject(source, mapper, options?) #### source Type: `object` The source object to copy properties from. #### mapper Type: `(sourceKey, sourceValue, source) => [targetKey, targetValue, mapperOptions?] | mapObjectSkip` A mapping function. > [!NOTE] > When `options.deep` is `true`, the mapper receives keys and values from nested objects and arrays. The `sourceKey` parameter is typed as `string | symbol` and `sourceValue` as `unknown` to reflect the actual runtime behavior when recursing into unknown shapes. The third argument `source` is always the original input object, not the current nested owner. ##### mapperOptions Type: `object` ###### shouldRecurse Type: `boolean`\ Default: `true` Whether to recurse into `targetValue`. Requires `deep: true`. #### options Type: `object` ##### deep Type: `boolean`\ Default: `false` Recurse nested objects and objects in arrays. Built-in objects like `RegExp`, `Error`, `Date`, `Map`, `Set`, `WeakMap`, `WeakSet`, `Promise`, `ArrayBuffer`, `DataView`, typed arrays (Uint8Array, etc.), and `Blob` are not recursed into. Special objects like Jest matchers are also automatically excluded. ##### includeSymbols Type: `boolean`\ Default: `false` Include symbol keys in the iteration. By default, symbol keys are completely ignored and not passed to the mapper function. When enabled, the mapper will also be called with symbol keys from the source object, allowing them to be transformed or included in the result. Only enumerable symbol properties are included. ##### target Type: `object`\ Default: `{}` The target object to map properties onto. ### mapObjectSkip Return this value from a `mapper` function to exclude the key from the new object. ```js import mapObject, {mapObjectSkip} from 'map-obj'; const object = {one: 1, two: 2}; const mapper = (key, value) => value === 1 ? [key, value] : mapObjectSkip; const result = mapObject(object, mapper); console.log(result); //=> {one: 1} ``` ## Related - [filter-obj](https://github.com/sindresorhus/filter-obj) - Filter object keys and values into a new object sindresorhus-map-obj-6a2faef/test.js000066400000000000000000000203601506372360100176620ustar00rootroot00000000000000import test from 'ava'; import mapObject, {mapObjectSkip} from './index.js'; test('main', t => { t.is(mapObject({foo: 'bar'}, key => [key, 'unicorn']).foo, 'unicorn'); t.is(mapObject({foo: 'bar'}, (key, value) => ['unicorn', value]).unicorn, 'bar'); t.is(mapObject({foo: 'bar'}, (key, value) => [value, key]).bar, 'foo'); }); test('mapper source argument is always the original input', t => { const input = {foo: {bar: 1}, a: {b: {c: 1}}}; // Test shallow mode mapObject(input, (key, value, source) => { t.is(source, input); return [key, value]; }); // Test deep mode mapObject(input, (key, value, source) => { t.is(source, input); // Should always be the root input object, not nested objects return [key, value]; }, {deep: true}); }); test('target option', t => { const input = {x: 1, foo: 'bar'}; const target = {y: 2}; // Test that the function returns the target object and source argument is correct const result = mapObject(input, (key, value, source) => { t.is(source, input); t.not(source, target); return [key === 'foo' ? 'baz' : key, value]; }, {target}); t.is(result, target); t.deepEqual(target, {y: 2, x: 1, baz: 'bar'}); }); test('deep option', t => { const object = { one: 1, object: { two: 2, three: 3, }, array: [ { four: 4, }, 5, ], }; const expected = { one: 2, object: { two: 4, three: 6, }, array: [ { four: 8, }, 5, ], }; const mapper = (key, value) => [key, typeof value === 'number' ? value * 2 : value]; const actual = mapObject(object, mapper, {deep: true}); t.deepEqual(actual, expected); }); test('shouldRecurse mapper option', t => { const object = { one: 1, object: { two: 2, three: 3, }, array: [ { four: 4, }, 5, ], }; const expected = { one: 2, object: { two: 2, three: 3, }, array: [ { four: 8, }, 5, ], }; const mapper = (key, value) => { if (key === 'object') { return [key, value, {shouldRecurse: false}]; } return [key, typeof value === 'number' ? value * 2 : value]; }; const actual = mapObject(object, mapper, {deep: true}); t.deepEqual(actual, expected); }); test('nested arrays', t => { const object = { array: [ [ 0, 1, 2, { a: 3, }, ], ], }; const expected = { array: [ [ 0, 1, 2, { a: 6, }, ], ], }; const mapper = (key, value) => [key, typeof value === 'number' ? value * 2 : value]; const actual = mapObject(object, mapper, {deep: true}); t.deepEqual(actual, expected); }); test('handles circular references', t => { const object = { one: 1, array: [ 2, ], }; object.circular = object; object.array2 = object.array; object.array.push(object); const mapper = (key, value) => [key.toUpperCase(), value]; const actual = mapObject(object, mapper, {deep: true}); const expected = { ONE: 1, ARRAY: [ 2, ], }; expected.CIRCULAR = expected; expected.ARRAY2 = expected.ARRAY; expected.ARRAY.push(expected); t.deepEqual(actual, expected); }); test('validates input', t => { t.throws(() => { mapObject(1, () => {}); }, { instanceOf: TypeError, }); t.throws(() => { mapObject([1, 2], (key, value) => [value, key]); }, { instanceOf: TypeError, }); }); test('__proto__ keys are safely dropped', t => { const input = {['__proto__']: {one: 1}}; const output = mapObject(input, (key, value) => [key, value]); t.deepEqual(output, {}); // AVA's equality checking isn't quite strict enough to catch the difference // between plain objects as prototypes and Object.prototype, so we also check // the prototype by identity t.is(Object.getPrototypeOf(output), Object.prototype); }); test('remove keys (#36)', t => { const object = { one: 1, two: 2, }; const expected = { one: 1, }; const mapper = (key, value) => value === 1 ? [key, value] : mapObjectSkip; const actual = mapObject(object, mapper, {deep: true}); t.deepEqual(actual, expected); }); test('should not recurse into Jest-like matchers', t => { // Mock a Jest asymmetric matcher like expect.anything() const jestMatcher = { $$typeof: Symbol.for('jest.asymmetricMatcher'), asymmetricMatch: () => true, toString: () => 'expect.anything()', }; const input = { normal: {nested: 'value'}, matcher: jestMatcher, }; let calls = 0; const result = mapObject(input, (key, value) => { calls++; // Should not recurse into jestMatcher properties t.not(key, '$$typeof'); t.not(key, 'asymmetricMatch'); t.not(key, 'toString'); return [key, value]; }, {deep: true}); t.is(result.matcher, jestMatcher); t.is(result.normal.nested, 'value'); t.is(calls, 3); // 'normal', 'nested', 'matcher' }); test('options object is not mutated', t => { const options = {deep: true, target: {}}; const originalOptions = {...options}; mapObject({a: 1}, (key, value) => [key, value], options); t.deepEqual(options, originalOptions); }); test('built-in objects are not recursed into', t => { const date = new Date(); const regex = /test/; const error = new Error('test'); const map = new Map([['key', 'value']]); const set = new Set([1, 2, 3]); const promise = Promise.resolve(42); const buffer = new ArrayBuffer(8); const uint8Array = new Uint8Array(4); const input = { date, regex, error, map, set, promise, buffer, uint8Array, normal: {nested: 'value'}, }; const calls = []; mapObject(input, (key, value) => { calls.push(key); return [key, value]; }, {deep: true}); // Should visit top-level keys and nested normal object t.deepEqual(calls.sort(), ['buffer', 'date', 'error', 'map', 'nested', 'normal', 'promise', 'regex', 'set', 'uint8Array']); }); test('symbol keys are ignored by default', t => { const symbol = Symbol('test'); const input = { regular: 'value', [symbol]: 'symbolValue', }; const result = mapObject(input, (key, value) => [key, value]); t.true('regular' in result); t.false(symbol in result); t.is(result.regular, 'value'); t.is(result[symbol], undefined); }); test('symbol keys are included with includeSymbols option', t => { const symbol = Symbol('test'); const input = { regular: 'value', [symbol]: 'symbolValue', }; const result = mapObject(input, (key, value) => [key, value], {includeSymbols: true}); t.true('regular' in result); t.true(symbol in result); t.is(result.regular, 'value'); t.is(result[symbol], 'symbolValue'); }); test('symbol keys work with deep option', t => { const symbol1 = Symbol('outer'); const symbol2 = Symbol('inner'); const input = { regular: 'value', nested: { regularNested: 'nestedValue', [symbol2]: 'innerSymbol', }, [symbol1]: 'outerSymbol', }; const result = mapObject(input, (key, value) => [key, value], { deep: true, includeSymbols: true, }); t.is(result.regular, 'value'); t.is(result.nested.regularNested, 'nestedValue'); t.is(result.nested[symbol2], 'innerSymbol'); t.is(result[symbol1], 'outerSymbol'); }); test('handles invalid mapper return values gracefully', t => { t.throws(() => { mapObject({a: 1}, () => null); }, { instanceOf: TypeError, message: 'Mapper must return an array or mapObjectSkip, got null', }); t.throws(() => { mapObject({a: 1}, () => 'string'); }, { instanceOf: TypeError, message: 'Mapper must return an array or mapObjectSkip, got string', }); t.throws(() => { mapObject({a: 1}, () => []); }, { instanceOf: TypeError, message: 'Mapper must return an array with at least 2 elements [key, value], got 0 elements', }); t.throws(() => { mapObject({a: 1}, () => ['key']); }, { instanceOf: TypeError, message: 'Mapper must return an array with at least 2 elements [key, value], got 1 elements', }); }); test('handles non-configurable target properties gracefully', t => { const target = {}; Object.defineProperty(target, 'readonly', { value: 'original', writable: false, configurable: false, }); const result = mapObject({readonly: 'new', other: 'value'}, (key, value) => [key, value], {target}); t.is(result, target); t.is(result.readonly, 'original'); // Should remain unchanged t.is(result.other, 'value'); // Should be added }); test('handles property key collisions', t => { const result = mapObject({a: 1, b: 2}, (key, value) => ['same', value]); // Last value wins t.deepEqual(result, {same: 2}); t.is(Object.keys(result).length, 1); });