diff --git a/packages/table-core/src/core/table/coreTablesFeature.utils.ts b/packages/table-core/src/core/table/coreTablesFeature.utils.ts index 236710a32b..b8a4e60582 100644 --- a/packages/table-core/src/core/table/coreTablesFeature.utils.ts +++ b/packages/table-core/src/core/table/coreTablesFeature.utils.ts @@ -68,7 +68,9 @@ export function table_reset< * Merges new table options with the current resolved options. * * If `options.mergeOptions` is provided, it owns the merge behavior; otherwise - * options are shallow-merged. + * options are shallow-merged. Static options that should never change after + * initialization are restored on a fresh object so framework merge helpers may + * return readonly getter/proxy objects. * * @example * ```ts @@ -82,17 +84,52 @@ export function table_mergeOptions< table: Table_Internal, newOptions: TableOptions, ) { - if (table.options.mergeOptions) { - return table.options.mergeOptions( - table.options as TableOptions, - newOptions, - ) + const { features, atoms, initialState } = table.options + + // simple merge if no mergeOptions is provided - performant + if (!table.options.mergeOptions) { + return { + ...table.options, + ...newOptions, + features, + atoms, + initialState, + } } - return { - ...table.options, - ...newOptions, + // else use the mergeOptions function and preserve getters/setters + const mergedOptions = table.options.mergeOptions( + table.options as TableOptions, + newOptions, + ) + const descriptors: PropertyDescriptorMap = { + ...Object.getOwnPropertyDescriptors(mergedOptions), } + + return Object.defineProperties( + Object.create(Object.getPrototypeOf(mergedOptions)), + { + ...descriptors, + features: { + value: features, + enumerable: true, + configurable: true, + writable: true, + }, + atoms: { + value: atoms, + enumerable: true, + configurable: true, + writable: true, + }, + initialState: { + value: initialState, + enumerable: true, + configurable: true, + writable: true, + }, + }, + ) as TableOptions } /** @@ -117,15 +154,7 @@ export function table_setOptions< updater, table.options as TableOptions, ) - // table static options that should never change after initialization - const { features, atoms, initialState } = table.options - const mergedOptions = Object.assign(table_mergeOptions(table, newOptions), { - // Once the table instance is created those properties should never change after initialization, - // so we assign them back preserving the `table_mergeOptions` object reference - features, - atoms, - initialState, - }) + const mergedOptions = table_mergeOptions(table, newOptions) if (table.optionsStore) { table.optionsStore.set(() => mergedOptions) diff --git a/packages/table-core/tests/unit/core/table/constructTable.test.ts b/packages/table-core/tests/unit/core/table/constructTable.test.ts index abb69736b8..038800927d 100644 --- a/packages/table-core/tests/unit/core/table/constructTable.test.ts +++ b/packages/table-core/tests/unit/core/table/constructTable.test.ts @@ -2,6 +2,36 @@ import { describe, expect, it } from 'vitest' import { constructTable, coreFeatures } from '../../../../src' import { storeReactivityBindings } from '../../../../src/store-reactivity-bindings' +function getterOnlyMerge(...sources: Array) { + const target = {} + + for (const source of sources) { + if (!source) { + continue + } + + for (const key of Reflect.ownKeys(source)) { + if (key in target) { + continue + } + + Object.defineProperty(target, key, { + enumerable: true, + get() { + for (let i = sources.length - 1; i >= 0; i--) { + const value = sources[i]?.[key] + if (value !== undefined) { + return value + } + } + }, + }) + } + } + + return target +} + describe('constructTable', () => { it('should create a table with all core table APIs and properties', () => { const table = constructTable({ @@ -46,4 +76,46 @@ describe('constructTable', () => { expect(table).toHaveProperty('setOptions') expect(table).toHaveProperty('store') // state is managed via store in v9 }) + + it('preserves static options without mutating mergeOptions results', () => { + const features = { + ...coreFeatures, + coreReactivityFeature: storeReactivityBindings(), + } + const atoms = {} + const initialState = {} + const data: Array<{ id: number }> = [] + const nextData = [{ id: 1 }] + const nextFeatures = { + ...coreFeatures, + coreReactivityFeature: storeReactivityBindings(), + } + const nextAtoms = {} + const nextInitialState = {} + + const table = constructTable({ + features, + atoms, + initialState, + columns: [], + data, + mergeOptions: (defaultOptions, options) => + getterOnlyMerge(defaultOptions, options) as any, + }) + + expect(() => { + table.setOptions((prev) => ({ + ...prev, + data: nextData, + features: nextFeatures, + atoms: nextAtoms, + initialState: nextInitialState, + })) + }).not.toThrow() + + expect(table.options.data).toBe(nextData) + expect(table.options.features).toBe(features) + expect(table.options.atoms).toBe(atoms) + expect(table.options.initialState).toBe(initialState) + }) })