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 {