diff --git a/packages-private/local-playground/tsconfig.json b/packages-private/local-playground/tsconfig.json index 1b1d44c776d..8ed98192084 100644 --- a/packages-private/local-playground/tsconfig.json +++ b/packages-private/local-playground/tsconfig.json @@ -4,5 +4,5 @@ "isolatedDeclarations": false, "allowJs": true }, - "include": ["./**/*", "../packages/*/src"] + "include": ["./**/*", "../../packages/*/src"] } diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts index 7ba6023b1e9..db91b6a62da 100644 --- a/packages/runtime-vapor/__tests__/for.spec.ts +++ b/packages/runtime-vapor/__tests__/for.spec.ts @@ -4,7 +4,14 @@ import { getRestElement, renderEffect, } from '../src' -import { nextTick, ref, shallowRef, triggerRef } from '@vue/runtime-dom' +import { + nextTick, + reactive, + readonly, + ref, + shallowRef, + triggerRef, +} from '@vue/runtime-dom' import { makeRender } from './_utils' const define = makeRender() @@ -674,4 +681,57 @@ describe('createFor', () => { await nextTick() expectCalledTimesToBe('Clear rows', 1, 0, 0, 0) }) + + describe('readonly source', () => { + test('should not allow mutation', () => { + const arr = readonly(reactive([{ foo: 1 }])) + + const { host } = define(() => { + const n1 = createFor( + () => arr, + (item, key, index) => { + const span = document.createElement('li') + renderEffect(() => { + item.value.foo = 0 + span.innerHTML = `${item.value.foo}` + }) + return span + }, + idx => idx, + ) + return n1 + }).render() + + expect(host.innerHTML).toBe('
  • 1
  • ') + expect( + `Set operation on key "foo" failed: target is readonly.`, + ).toHaveBeenWarned() + }) + + test('should trigger effect for deep mutations', async () => { + const arr = reactive([{ foo: 1 }]) + const readonlyArr = readonly(arr) + + const { host } = define(() => { + const n1 = createFor( + () => readonlyArr, + (item, key, index) => { + const span = document.createElement('li') + renderEffect(() => { + span.innerHTML = `${item.value.foo}` + }) + return span + }, + idx => idx, + ) + return n1 + }).render() + + expect(host.innerHTML).toBe('
  • 1
  • ') + + arr[0].foo = 2 + await nextTick() + expect(host.innerHTML).toBe('
  • 2
  • ') + }) + }) }) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 0cd8317532f..e93fc6061be 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -2,12 +2,14 @@ import { EffectScope, type ShallowRef, isReactive, + isReadonly, isShallow, pauseTracking, resetTracking, shallowReadArray, shallowRef, toReactive, + toReadonly, } from '@vue/reactivity' import { getSequence, isArray, isObject, isString } from '@vue/shared' import { createComment, createTextNode } from './dom/node' @@ -55,6 +57,7 @@ type Source = any[] | Record | number | Set | Map type ResolvedSource = { values: any[] needsWrap: boolean + isReadonlySource: boolean keys?: string[] } @@ -387,11 +390,13 @@ export function createForSlots( function normalizeSource(source: any): ResolvedSource { let values = source let needsWrap = false + let isReadonlySource = false let keys if (isArray(source)) { if (isReactive(source)) { needsWrap = !isShallow(source) values = shallowReadArray(source) + isReadonlySource = isReadonly(source) } } else if (isString(source)) { values = source.split('') @@ -412,14 +417,23 @@ function normalizeSource(source: any): ResolvedSource { } } } - return { values, needsWrap, keys } + return { + values, + needsWrap, + isReadonlySource, + keys, + } } function getItem( - { keys, values, needsWrap }: ResolvedSource, + { keys, values, needsWrap, isReadonlySource }: ResolvedSource, idx: number, ): [item: any, key: any, index?: number] { - const value = needsWrap ? toReactive(values[idx]) : values[idx] + const value = needsWrap + ? isReadonlySource + ? toReadonly(toReactive(values[idx])) + : toReactive(values[idx]) + : values[idx] if (keys) { return [value, keys[idx], idx] } else {