diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 493ab295000..b9e8078dd79 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -58,6 +58,31 @@ jobs:
- name: Run ssr unit tests
run: pnpm run test-unit server-renderer
+ benchmarks:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
+ env:
+ PUPPETEER_SKIP_DOWNLOAD: 'true'
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v2
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.node-version'
+ cache: 'pnpm'
+
+ - run: pnpm install
+
+ - name: Run benchmarks
+ uses: CodSpeedHQ/action@v2
+ with:
+ run: pnpm vitest bench --run
+ token: ${{ secrets.CODSPEED_TOKEN }}
+
e2e-test:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8598e84614..d523b6731f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,20 @@
+## [3.3.11](https://github.com/vuejs/core/compare/v3.3.10...v3.3.11) (2023-12-08)
+
+
+### Bug Fixes
+
+* **custom-element:** correctly handle number type props in prod ([#8989](https://github.com/vuejs/core/issues/8989)) ([d74d364](https://github.com/vuejs/core/commit/d74d364d62db8e48881af6b5a75ce4fb5f36cc35))
+* **reactivity:** fix mutation on user proxy of reactive Array ([6ecbd5c](https://github.com/vuejs/core/commit/6ecbd5ce2a7f59314a8326a1d193874b87f4d8c8)), closes [#9742](https://github.com/vuejs/core/issues/9742) [#9751](https://github.com/vuejs/core/issues/9751) [#9750](https://github.com/vuejs/core/issues/9750)
+* **runtime-dom:** fix width and height prop check condition ([5b00286](https://github.com/vuejs/core/commit/5b002869c533220706f9788b496b8ca8d8e98609)), closes [#9762](https://github.com/vuejs/core/issues/9762)
+* **shared:** handle Map with symbol keys in toDisplayString ([#9731](https://github.com/vuejs/core/issues/9731)) ([364821d](https://github.com/vuejs/core/commit/364821d6bdb1775e2f55a69bcfb9f40f7acf1506)), closes [#9727](https://github.com/vuejs/core/issues/9727)
+* **shared:** handle more Symbol cases in toDisplayString ([983d45d](https://github.com/vuejs/core/commit/983d45d4f8eb766b5a16b7ea93b86d3c51618fa6))
+* **Suspense:** properly get anchor when mount fallback vnode ([#9770](https://github.com/vuejs/core/issues/9770)) ([b700328](https://github.com/vuejs/core/commit/b700328342e17dc16b19316c2e134a26107139d2)), closes [#9769](https://github.com/vuejs/core/issues/9769)
+* **types:** ref() return type should not be any when initial value is any ([#9768](https://github.com/vuejs/core/issues/9768)) ([cdac121](https://github.com/vuejs/core/commit/cdac12161ec27b45ded48854c3d749664b6d4a6d))
+* **watch:** should not fire pre watcher on child component unmount ([#7181](https://github.com/vuejs/core/issues/7181)) ([6784f0b](https://github.com/vuejs/core/commit/6784f0b1f8501746ea70d87d18ed63a62cf6b76d)), closes [#7030](https://github.com/vuejs/core/issues/7030)
+
+
+
+
# [3.4.0-alpha.4](https://github.com/vuejs/core/compare/v3.3.10...v3.4.0-alpha.4) (2023-12-04)
@@ -19,74 +36,6 @@
-# [3.4.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.0-alpha.2...v3.4.0-alpha.3) (2023-11-28)
-
-
-### Bug Fixes
-
-* **parser:** directive arg should be undefined on shorthands with no arg ([e49dffc](https://github.com/vuejs/core/commit/e49dffc9ece86bddf094b9ad4ad15eb4856d6277))
-
-
-### Features
-
-* **dx:** link errors to docs in prod build ([#9165](https://github.com/vuejs/core/issues/9165)) ([9f8ba98](https://github.com/vuejs/core/commit/9f8ba9821fe166f77e63fa940e9e7e13ec3344fa))
-
-
-
-# [3.4.0-alpha.2](https://github.com/vuejs/core/compare/v3.3.9...v3.4.0-alpha.2) (2023-11-27)
-
-
-### Bug Fixes
-
-* avoid confusing breakage in @vitejs/plugin-vue ([ceec69c](https://github.com/vuejs/core/commit/ceec69c8ccb96c433a4a506ad2e85e276998bade))
-* **compiler-core:** fix line/column tracking when fast forwarding ([2e65ea4](https://github.com/vuejs/core/commit/2e65ea481f74db8649df8110a031cbdc98f98c84))
-* **compiler-sfc:** fix ast reuse for ssr ([fb619cf](https://github.com/vuejs/core/commit/fb619cf9a440239f0ba88e327d10001a6a3c8171))
-* **compiler-sfc:** support `:is` and `:where` selector in scoped css rewrite ([#8929](https://github.com/vuejs/core/issues/8929)) ([c6083dc](https://github.com/vuejs/core/commit/c6083dcad31f3e9292c687fada9e32f287e2317f))
-* **compiler-sfc:** use correct compiler when re-parsing in ssr mode ([678378a](https://github.com/vuejs/core/commit/678378afd559481badb486b243722b6287862e09))
-
-
-* feat!: remove reactivity transform (#9321) ([79b8a09](https://github.com/vuejs/core/commit/79b8a0905bf363bf82edd2096fef10c3db6d9c3c)), closes [#9321](https://github.com/vuejs/core/issues/9321)
-
-
-### Features
-
-* **compiler-core:** support specifying root namespace when parsing ([40f72d5](https://github.com/vuejs/core/commit/40f72d5e50b389cb11b7ca13461aa2a75ddacdb4))
-* **compiler-core:** support v-bind shorthand for key and value with the same name ([#9451](https://github.com/vuejs/core/issues/9451)) ([26399aa](https://github.com/vuejs/core/commit/26399aa6fac1596b294ffeba06bb498d86f5508c))
-* **compiler:** improve parsing tolerance for language-tools ([41ff68e](https://github.com/vuejs/core/commit/41ff68ea579d933333392146625560359acb728a))
-* **reactivity:** expose last result for computed getter ([#9497](https://github.com/vuejs/core/issues/9497)) ([48b47a1](https://github.com/vuejs/core/commit/48b47a1ab63577e2dbd91947eea544e3ef185b85))
-
-
-### Performance Improvements
-
-* avoid sfc source map unnecessary serialization and parsing ([f15d2f6](https://github.com/vuejs/core/commit/f15d2f6cf69c0c39f8dfb5c33122790c68bf92e2))
-* **codegen:** optimize line / column calculation during codegen ([3be53d9](https://github.com/vuejs/core/commit/3be53d9b974dae1a10eb795cade71ae765e17574))
-* **codegen:** optimize source map generation ([c11002f](https://github.com/vuejs/core/commit/c11002f16afd243a2b15b546816e73882eea9e4d))
-* **compiler-sfc:** remove magic-string trim on script ([e8e3ec6](https://github.com/vuejs/core/commit/e8e3ec6ca7392e43975c75b56eaaa711d5ea9410))
-* **compiler-sfc:** use faster source map addMapping ([50cde7c](https://github.com/vuejs/core/commit/50cde7cfbcc49022ba88f5f69fa9b930b483c282))
-* optimize away isBuiltInType ([66c0ed0](https://github.com/vuejs/core/commit/66c0ed0a3c1c6f37dafc6b1c52b75c6bf60e3136))
-* optimize makeMap ([ae6fba9](https://github.com/vuejs/core/commit/ae6fba94954bac6430902f77b0d1113a98a75b18))
-* optimize position cloning ([2073236](https://github.com/vuejs/core/commit/20732366b9b3530d33b842cf1fc985919afb9317))
-
-
-### BREAKING CHANGES
-
-* Reactivity Transform was marked deprecated in 3.3 and is now removed in 3.4. This change does not require a major due to the feature being experimental. Users who wish to continue using the feature can do so via the external plugin at https://vue-macros.dev/features/reactivity-transform.html
-
-
-
-# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28)
-
-
-### Features
-
-* **compiler-core:** export error message ([#8729](https://github.com/vuejs/core/issues/8729)) ([f7e80ee](https://github.com/vuejs/core/commit/f7e80ee4a065a9eaba98720abf415d9e87756cbd))
-* **compiler-sfc:** expose resolve type-based props and emits ([#8874](https://github.com/vuejs/core/issues/8874)) ([9e77580](https://github.com/vuejs/core/commit/9e77580c0c2f0d977bd0031a1d43cc334769d433))
-* export runtime error strings ([#9301](https://github.com/vuejs/core/issues/9301)) ([feb2f2e](https://github.com/vuejs/core/commit/feb2f2edce2d91218a5e9a52c81e322e4033296b))
-* **reactivity:** more efficient reactivity system ([#5912](https://github.com/vuejs/core/issues/5912)) ([16e06ca](https://github.com/vuejs/core/commit/16e06ca08f5a1e2af3fc7fb35de153dbe0c3087d)), closes [#311](https://github.com/vuejs/core/issues/311) [#1811](https://github.com/vuejs/core/issues/1811) [#6018](https://github.com/vuejs/core/issues/6018) [#7160](https://github.com/vuejs/core/issues/7160) [#8714](https://github.com/vuejs/core/issues/8714) [#9149](https://github.com/vuejs/core/issues/9149) [#9419](https://github.com/vuejs/core/issues/9419) [#9464](https://github.com/vuejs/core/issues/9464)
-* **runtime-core:** add `once` option to watch ([#9034](https://github.com/vuejs/core/issues/9034)) ([a645e7a](https://github.com/vuejs/core/commit/a645e7aa51006516ba668b3a4365d296eb92ee7d))
-
-
-
## [3.3.10](https://github.com/vuejs/core/compare/v3.3.9...v3.3.10) (2023-12-04)
diff --git a/package.json b/package.json
index 9564d3e51ff..5d38cae71e4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"version": "3.4.0-alpha.4",
- "packageManager": "pnpm@8.11.0",
+ "packageManager": "pnpm@8.12.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@@ -22,6 +22,7 @@
"test-dts": "run-s build-dts test-dts-only",
"test-dts-only": "tsc -p ./packages/dts-test/tsconfig.test.json",
"test-coverage": "vitest -c vitest.unit.config.ts --coverage",
+ "test-bench": "vitest bench",
"release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
@@ -60,6 +61,7 @@
"devDependencies": {
"@babel/parser": "^7.23.5",
"@babel/types": "^7.23.5",
+ "@codspeed/vitest-plugin": "^2.3.1",
"@rollup/plugin-alias": "^5.0.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.0.1",
@@ -68,33 +70,33 @@
"@rollup/plugin-terser": "^0.4.4",
"@types/hash-sum": "^1.0.2",
"@types/minimist": "^1.2.5",
- "@types/node": "^20.10.3",
+ "@types/node": "^20.10.4",
"@types/semver": "^7.5.5",
- "@typescript-eslint/parser": "^6.13.0",
- "@vitest/coverage-istanbul": "^0.34.6",
+ "@typescript-eslint/parser": "^6.13.2",
+ "@vitest/coverage-istanbul": "^1.0.4",
"@vue/consolidate": "0.17.3",
"conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.4.1",
"esbuild": "^0.19.5",
"esbuild-plugin-polyfill-node": "^0.3.0",
- "eslint": "^8.54.0",
+ "eslint": "^8.55.0",
"eslint-define-config": "^1.24.1",
"eslint-plugin-jest": "^27.6.0",
"estree-walker": "^2.0.2",
"execa": "^8.0.1",
- "jsdom": "^22.1.0",
- "lint-staged": "^15.1.0",
+ "jsdom": "^23.0.1",
+ "lint-staged": "^15.2.0",
"lodash": "^4.17.21",
"magic-string": "^0.30.5",
"markdown-table": "^3.0.3",
- "marked": "^9.1.6",
+ "marked": "^11.0.1",
"minimist": "^1.2.8",
"npm-run-all": "^4.1.5",
"picocolors": "^1.0.0",
- "prettier": "^3.1.0",
+ "prettier": "^3.1.1",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.2",
- "puppeteer": "~21.5.2",
+ "puppeteer": "~21.6.0",
"rimraf": "^5.0.5",
"rollup": "^4.1.4",
"rollup-plugin-dts": "^6.1.0",
@@ -108,7 +110,7 @@
"tslib": "^2.6.2",
"tsx": "^4.6.2",
"typescript": "^5.2.2",
- "vite": "^5.0.0",
- "vitest": "^1.0.0"
+ "vite": "^5.0.5",
+ "vitest": "^1.0.4"
}
}
diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap
index 38c4419b5d3..ce5eaed18fd 100644
--- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap
@@ -18,6 +18,47 @@ return { props, bar }
}"
`;
+exports[`defineProps > custom element retains the props type & default value & production mode 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+interface Props {
+ foo?: number;
+ }
+
+export default /*#__PURE__*/_defineComponent({
+ __name: 'app.ce',
+ props: {
+ foo: { default: 5.5, type: Number }
+ },
+ setup(__props: any, { expose: __expose }) {
+ __expose();
+
+ const props = __props;
+
+return { props }
+}
+
+})"
+`;
+
+exports[`defineProps > custom element retains the props type & production mode 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ __name: 'app.ce',
+ props: {
+ foo: {type: Number}
+ },
+ setup(__props: any, { expose: __expose }) {
+ __expose();
+
+ const props = __props
+
+return { props }
+}
+
+})"
+`;
+
exports[`defineProps > defineProps w/ runtime options 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
diff --git a/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts
index 23d6a806f0d..7d46c4d2187 100644
--- a/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts
@@ -710,4 +710,35 @@ const props = defineProps({ foo: String })
'da-sh': BindingTypes.PROPS
})
})
+
+ // #8989
+ test('custom element retains the props type & production mode', () => {
+ const { content } = compile(
+ ``,
+ { isProd: true, customElement: filename => /\.ce\.vue$/.test(filename) },
+ { filename: 'app.ce.vue' }
+ )
+
+ expect(content).toMatch(`foo: {type: Number}`)
+ assertCode(content)
+ })
+
+ test('custom element retains the props type & default value & production mode', () => {
+ const { content } = compile(
+ ``,
+ { isProd: true, customElement: filename => /\.ce\.vue$/.test(filename) },
+ { filename: 'app.ce.vue' }
+ )
+ expect(content).toMatch(`foo: { default: 5.5, type: Number }`)
+ assertCode(content)
+ })
})
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index 1783c6db9ee..3a375a05d20 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -117,6 +117,10 @@ export interface SFCScriptCompileOptions {
fileExists(file: string): boolean
readFile(file: string): string | undefined
}
+ /**
+ * Transform Vue SFCs into custom elements.
+ */
+ customElement?: boolean | ((filename: string) => boolean)
}
export interface ImportBinding {
diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts
index 900cf109260..7643b28fd50 100644
--- a/packages/compiler-sfc/src/script/context.ts
+++ b/packages/compiler-sfc/src/script/context.ts
@@ -12,6 +12,7 @@ import { TypeScope } from './resolveType'
export class ScriptCompileContext {
isJS: boolean
isTS: boolean
+ isCE = false
scriptAst: Program | null
scriptSetupAst: Program | null
@@ -95,6 +96,14 @@ export class ScriptCompileContext {
scriptSetupLang === 'ts' ||
scriptSetupLang === 'tsx'
+ const customElement = options.customElement
+ const filename = this.descriptor.filename
+ if (customElement) {
+ this.isCE =
+ typeof customElement === 'boolean'
+ ? customElement
+ : customElement(filename)
+ }
// resolve parser plugins
const plugins: ParserPlugin[] = resolveParserPlugins(
(scriptLang || scriptSetupLang)!,
diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts
index 3df6daea7d6..cc7b86ba2cb 100644
--- a/packages/compiler-sfc/src/script/defineProps.ts
+++ b/packages/compiler-sfc/src/script/defineProps.ts
@@ -281,6 +281,17 @@ function genRuntimePropFromType(
defaultString
])} }`
} else {
+ // #8989 for custom element, should keep the type
+ if (ctx.isCE) {
+ if (defaultString) {
+ return `${finalKey}: ${`{ ${defaultString}, type: ${toRuntimeTypeString(
+ type
+ )} }`}`
+ } else {
+ return `${finalKey}: {type: ${toRuntimeTypeString(type)}}`
+ }
+ }
+
// production: checks are useless
return `${finalKey}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
}
diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts
index ed25ba9a734..f38374bd5bb 100644
--- a/packages/compiler-sfc/src/script/resolveType.ts
+++ b/packages/compiler-sfc/src/script/resolveType.ts
@@ -83,6 +83,9 @@ export type SimpleTypeResolveContext = Pick<
// emits
| 'emitsTypeDecl'
+
+ // customElement
+ | 'isCE'
> &
Partial<
Pick
@@ -1475,6 +1478,7 @@ export function inferRuntimeType(
scope
)
}
+ break
case 'TSMethodSignature':
case 'TSFunctionType':
return ['Function']
diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts
index 542d9d6a9ef..a467c446d0a 100644
--- a/packages/dts-test/ref.test-d.ts
+++ b/packages/dts-test/ref.test-d.ts
@@ -18,7 +18,7 @@ import {
computed,
ShallowRef
} from 'vue'
-import { expectType, describe, IsUnion } from './utils'
+import { expectType, describe, IsUnion, IsAny } from './utils'
function plainType(arg: number | Ref) {
// ref coercing
@@ -79,6 +79,10 @@ function plainType(arg: number | Ref) {
// should still unwrap in objects nested in arrays
const arr2 = ref([{ a: ref(1) }]).value
expectType(arr2[0].a)
+
+ // any value should return Ref, not any
+ const a = ref(1 as any)
+ expectType>(false)
}
plainType(1)
@@ -191,6 +195,12 @@ if (refStatus.value === 'initial') {
expectType>(false)
}
+{
+ // any value should return Ref, not any
+ const a = shallowRef(1 as any)
+ expectType>(false)
+}
+
describe('shallowRef with generic', () => {
const r = ref({}) as MaybeRef
expectType | Ref>(shallowRef(r))
diff --git a/packages/dts-test/setupHelpers.test-d.ts b/packages/dts-test/setupHelpers.test-d.ts
index 51f95c00944..53c4d859788 100644
--- a/packages/dts-test/setupHelpers.test-d.ts
+++ b/packages/dts-test/setupHelpers.test-d.ts
@@ -260,6 +260,30 @@ describe('defineSlots', () => {
expectType(slotsUntype)
})
+describe('defineSlots generic', >() => {
+ const props = defineProps<{
+ item: T
+ }>()
+
+ const slots = defineSlots<
+ {
+ [K in keyof T as `slot-${K & string}`]?: (props: { item: T }) => any
+ } & {
+ label?: (props: { item: T }) => any
+ }
+ >()
+
+ for (const key of Object.keys(props.item) as (keyof T & string)[]) {
+ slots[`slot-${String(key)}`]?.({
+ item: props.item
+ })
+ }
+ slots.label?.({ item: props.item })
+
+ // @ts-expect-error calling wrong slot
+ slots.foo({})
+})
+
describe('defineModel', () => {
// overload 1
const modelValueRequired = defineModel({ required: true })
@@ -336,6 +360,78 @@ describe('useSlots', () => {
expectType(slots)
})
+describe('defineSlots generic', >() => {
+ const props = defineProps<{
+ item: T
+ }>()
+
+ const slots = defineSlots<
+ {
+ [K in keyof T as `slot-${K & string}`]?: (props: { item: T }) => any
+ } & {
+ label?: (props: { item: T }) => any
+ }
+ >()
+
+ // @ts-expect-error slots should be readonly
+ slots.label = () => {}
+
+ // @ts-expect-error non existing slot
+ slots['foo-asdas']?.({
+ item: props.item
+ })
+ for (const key in props.item) {
+ slots[`slot-${String(key)}`]?.({
+ item: props.item
+ })
+ slots[`slot-${String(key as keyof T)}`]?.({
+ item: props.item
+ })
+ }
+
+ for (const key of Object.keys(props.item) as (keyof T)[]) {
+ slots[`slot-${String(key)}`]?.({
+ item: props.item
+ })
+ }
+ slots.label?.({ item: props.item })
+
+ // @ts-expect-error calling wrong slot
+ slots.foo({})
+})
+
+describe('defineSlots generic strict', () => {
+ const props = defineProps<{
+ item: T
+ }>()
+
+ const slots = defineSlots<
+ {
+ [K in keyof T as `slot-${K & string}`]?: (props: { item: T }) => any
+ } & {
+ label?: (props: { item: T }) => any
+ }
+ >()
+
+ // slot-bar/foo should be automatically inferred
+ slots['slot-bar']?.({ item: props.item })
+ slots['slot-foo']?.({ item: props.item })
+
+ slots.label?.({ item: props.item })
+
+ // @ts-expect-error not part of the extends
+ slots['slot-RANDOM']?.({ item: props.item })
+
+ // @ts-expect-error slots should be readonly
+ slots.label = () => {}
+
+ // @ts-expect-error calling wrong slot
+ slots.foo({})
+})
+
// #6420
describe('toRefs w/ type declaration', () => {
const props = defineProps<{
diff --git a/packages/dts-test/tsx.test-d.tsx b/packages/dts-test/tsx.test-d.tsx
index 4b4a0dbf9df..5e171e1f75f 100644
--- a/packages/dts-test/tsx.test-d.tsx
+++ b/packages/dts-test/tsx.test-d.tsx
@@ -112,3 +112,11 @@ expectType(
)
// @ts-expect-error
;
+
+// svg
+expectType(
+
+)
diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json
new file mode 100644
index 00000000000..4baea5d3a55
--- /dev/null
+++ b/packages/reactivity-transform/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@vue/reactivity-transform",
+ "version": "3.3.11",
+ "description": "@vue/reactivity-transform",
+ "main": "dist/reactivity-transform.cjs.js",
+ "files": [
+ "dist"
+ ],
+ "buildOptions": {
+ "formats": [
+ "cjs"
+ ],
+ "prod": false
+ },
+ "types": "dist/reactivity-transform.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vuejs/core.git",
+ "directory": "packages/reactivity-transform"
+ },
+ "keywords": [
+ "vue"
+ ],
+ "author": "Evan You",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/vuejs/core/issues"
+ },
+ "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme",
+ "dependencies": {
+ "@babel/parser": "^7.23.5",
+ "@vue/compiler-core": "workspace:*",
+ "@vue/shared": "workspace:*",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.5"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.23.5",
+ "@babel/types": "^7.23.5"
+ }
+}
diff --git a/packages/reactivity/__tests__/computed.bench.ts b/packages/reactivity/__tests__/computed.bench.ts
new file mode 100644
index 00000000000..73c20acdfad
--- /dev/null
+++ b/packages/reactivity/__tests__/computed.bench.ts
@@ -0,0 +1,126 @@
+import { describe, bench } from 'vitest'
+import { ComputedRef, Ref, computed, ref } from '../src/index'
+
+describe('computed', () => {
+ bench('create computed', () => {
+ computed(() => 100)
+ })
+
+ {
+ let i = 0
+ const o = ref(100)
+ bench('write independent ref dep', () => {
+ o.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ computed(() => v.value * 2)
+ let i = 0
+ bench("write ref, don't read computed (never invoked)", () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ computed(() => {
+ return v.value * 2
+ })
+ let i = 0
+ bench("write ref, don't read computed (never invoked)", () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ const c = computed(() => {
+ return v.value * 2
+ })
+ c.value
+ let i = 0
+ bench("write ref, don't read computed (invoked)", () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ const c = computed(() => {
+ return v.value * 2
+ })
+ let i = 0
+ bench('write ref, read computed', () => {
+ v.value = i++
+ c.value
+ })
+ }
+
+ {
+ const v = ref(100)
+ const computeds = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return v.value * 2
+ })
+ computeds.push(c)
+ }
+ let i = 0
+ bench("write ref, don't read 1000 computeds (never invoked)", () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ const computeds = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return v.value * 2
+ })
+ c.value
+ computeds.push(c)
+ }
+ let i = 0
+ bench("write ref, don't read 1000 computeds (invoked)", () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ const computeds: ComputedRef[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return v.value * 2
+ })
+ c.value
+ computeds.push(c)
+ }
+ let i = 0
+ bench('write ref, read 1000 computeds', () => {
+ v.value = i++
+ computeds.forEach(c => c.value)
+ })
+ }
+
+ {
+ const refs: Ref[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ refs.push(ref(i))
+ }
+ const c = computed(() => {
+ let total = 0
+ refs.forEach(ref => (total += ref.value))
+ return total
+ })
+ let i = 0
+ const n = refs.length
+ bench('1000 refs, 1 computed', () => {
+ refs[i++ % n].value++
+ c.value
+ })
+ }
+})
diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts
index e7fe18252ab..1ea1ba4a901 100644
--- a/packages/reactivity/__tests__/reactive.spec.ts
+++ b/packages/reactivity/__tests__/reactive.spec.ts
@@ -158,6 +158,21 @@ describe('reactivity/reactive', () => {
expect(original.bar).toBe(original2)
})
+ // #1246
+ test('mutation on objects using reactive as prototype should not trigger', () => {
+ const observed = reactive({ foo: 1 })
+ const original = Object.create(observed)
+ let dummy
+ effect(() => (dummy = original.foo))
+ expect(dummy).toBe(1)
+ observed.foo = 2
+ expect(dummy).toBe(2)
+ original.foo = 3
+ expect(dummy).toBe(2)
+ original.foo = 4
+ expect(dummy).toBe(2)
+ })
+
test('toRaw', () => {
const original = { foo: 1 }
const observed = reactive(original)
@@ -166,11 +181,18 @@ describe('reactivity/reactive', () => {
})
test('toRaw on object using reactive as prototype', () => {
- const original = reactive({})
- const obj = Object.create(original)
+ const original = { foo: 1 }
+ const observed = reactive(original)
+ const inherted = Object.create(observed)
+ expect(toRaw(inherted)).toBe(inherted)
+ })
+
+ test('toRaw on user Proxy wrapping reactive', () => {
+ const original = {}
+ const re = reactive(original)
+ const obj = new Proxy(re, {})
const raw = toRaw(obj)
- expect(raw).toBe(obj)
- expect(raw).not.toBe(toRaw(original))
+ expect(raw).toBe(original)
})
test('should not unwrap Ref', () => {
diff --git a/packages/reactivity/__tests__/reactiveArray.bench.ts b/packages/reactivity/__tests__/reactiveArray.bench.ts
new file mode 100644
index 00000000000..596b9f1a33e
--- /dev/null
+++ b/packages/reactivity/__tests__/reactiveArray.bench.ts
@@ -0,0 +1,92 @@
+import { bench } from 'vitest'
+import { computed, reactive, readonly, shallowRef, triggerRef } from '../src'
+
+for (let amount = 1e1; amount < 1e4; amount *= 10) {
+ {
+ const rawArray = []
+ for (let i = 0, n = amount; i < n; i++) {
+ rawArray.push(i)
+ }
+ const r = reactive(rawArray)
+ const c = computed(() => {
+ return r.reduce((v, a) => a + v, 0)
+ })
+
+ bench(`reduce *reactive* array, ${amount} elements`, () => {
+ for (let i = 0, n = r.length; i < n; i++) {
+ r[i]++
+ }
+ c.value
+ })
+ }
+
+ {
+ const rawArray = []
+ for (let i = 0, n = amount; i < n; i++) {
+ rawArray.push(i)
+ }
+ const r = reactive(rawArray)
+ const c = computed(() => {
+ return r.reduce((v, a) => a + v, 0)
+ })
+
+ bench(
+ `reduce *reactive* array, ${amount} elements, only change first value`,
+ () => {
+ r[0]++
+ c.value
+ }
+ )
+ }
+
+ {
+ const rawArray = []
+ for (let i = 0, n = amount; i < n; i++) {
+ rawArray.push(i)
+ }
+ const r = reactive({ arr: readonly(rawArray) })
+ const c = computed(() => {
+ return r.arr.reduce((v, a) => a + v, 0)
+ })
+
+ bench(`reduce *readonly* array, ${amount} elements`, () => {
+ r.arr = r.arr.map(v => v + 1)
+ c.value
+ })
+ }
+
+ {
+ const rawArray = []
+ for (let i = 0, n = amount; i < n; i++) {
+ rawArray.push(i)
+ }
+ const r = shallowRef(rawArray)
+ const c = computed(() => {
+ return r.value.reduce((v, a) => a + v, 0)
+ })
+
+ bench(`reduce *raw* array, copied, ${amount} elements`, () => {
+ r.value = r.value.map(v => v + 1)
+ c.value
+ })
+ }
+
+ {
+ const rawArray: number[] = []
+ for (let i = 0, n = amount; i < n; i++) {
+ rawArray.push(i)
+ }
+ const r = shallowRef(rawArray)
+ const c = computed(() => {
+ return r.value.reduce((v, a) => a + v, 0)
+ })
+
+ bench(`reduce *raw* array, manually triggered, ${amount} elements`, () => {
+ for (let i = 0, n = rawArray.length; i < n; i++) {
+ rawArray[i]++
+ }
+ triggerRef(r)
+ c.value
+ })
+ }
+}
diff --git a/packages/reactivity/__tests__/reactiveArray.spec.ts b/packages/reactivity/__tests__/reactiveArray.spec.ts
index f4eb7b58384..7eb800b80bb 100644
--- a/packages/reactivity/__tests__/reactiveArray.spec.ts
+++ b/packages/reactivity/__tests__/reactiveArray.spec.ts
@@ -175,6 +175,15 @@ describe('reactivity/reactive/Array', () => {
expect(length).toBe('01')
})
+ // #9742
+ test('mutation on user proxy of reactive Array', () => {
+ const array = reactive([])
+ const proxy = new Proxy(array, {})
+ proxy.push(1)
+ expect(array).toHaveLength(1)
+ expect(proxy).toHaveLength(1)
+ })
+
describe('Array methods w/ refs', () => {
let original: any[]
beforeEach(() => {
diff --git a/packages/reactivity/__tests__/reactiveMap.bench.ts b/packages/reactivity/__tests__/reactiveMap.bench.ts
new file mode 100644
index 00000000000..579904bafe7
--- /dev/null
+++ b/packages/reactivity/__tests__/reactiveMap.bench.ts
@@ -0,0 +1,143 @@
+import { bench } from 'vitest'
+import { reactive, computed, ComputedRef } from '../src'
+
+function createMap(obj: Record) {
+ const map = new Map()
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ map.set(key, obj[key])
+ }
+ }
+ return map
+}
+
+bench('create reactive map', () => {
+ reactive(createMap({ a: 1 }))
+})
+
+{
+ let i = 0
+ const r = reactive(createMap({ a: 1 }))
+ bench('write reactive map property', () => {
+ r.set('a', i++)
+ })
+}
+
+{
+ const r = reactive(createMap({ a: 1 }))
+ computed(() => {
+ return r.get('a') * 2
+ })
+ let i = 0
+ bench("write reactive map, don't read computed (never invoked)", () => {
+ r.set('a', i++)
+ })
+}
+
+{
+ const r = reactive(createMap({ a: 1 }))
+ const c = computed(() => {
+ return r.get('a') * 2
+ })
+ c.value
+ let i = 0
+ bench("write reactive map, don't read computed (invoked)", () => {
+ r.set('a', i++)
+ })
+}
+
+{
+ const r = reactive(createMap({ a: 1 }))
+ const c = computed(() => {
+ return r.get('a') * 2
+ })
+ let i = 0
+ bench('write reactive map, read computed', () => {
+ r.set('a', i++)
+ c.value
+ })
+}
+
+{
+ const _m = new Map()
+ for (let i = 0; i < 10000; i++) {
+ _m.set(i, i)
+ }
+ const r = reactive(_m)
+ const c = computed(() => {
+ let total = 0
+ r.forEach((value, key) => {
+ total += value
+ })
+ return total
+ })
+ bench("write reactive map (10'000 items), read computed", () => {
+ r.set(5000, r.get(5000) + 1)
+ c.value
+ })
+}
+
+{
+ const r = reactive(createMap({ a: 1 }))
+ const computeds = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return r.get('a') * 2
+ })
+ computeds.push(c)
+ }
+ let i = 0
+ bench("write reactive map, don't read 1000 computeds (never invoked)", () => {
+ r.set('a', i++)
+ })
+}
+
+{
+ const r = reactive(createMap({ a: 1 }))
+ const computeds = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return r.get('a') * 2
+ })
+ c.value
+ computeds.push(c)
+ }
+ let i = 0
+ bench("write reactive map, don't read 1000 computeds (invoked)", () => {
+ r.set('a', i++)
+ })
+}
+
+{
+ const r = reactive(createMap({ a: 1 }))
+ const computeds: ComputedRef[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return r.get('a') * 2
+ })
+ computeds.push(c)
+ }
+ let i = 0
+ bench('write reactive map, read 1000 computeds', () => {
+ r.set('a', i++)
+ computeds.forEach(c => c.value)
+ })
+}
+
+{
+ const reactives: Map[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ reactives.push(reactive(createMap({ a: i })))
+ }
+ const c = computed(() => {
+ let total = 0
+ reactives.forEach(r => (total += r.get('a')))
+ return total
+ })
+ let i = 0
+ const n = reactives.length
+ bench('1000 reactive maps, 1 computed', () => {
+ reactives[i++ % n].set('a', reactives[i++ % n].get('a') + 1)
+ c.value
+ })
+}
diff --git a/packages/reactivity/__tests__/reactiveObject.bench.ts b/packages/reactivity/__tests__/reactiveObject.bench.ts
new file mode 100644
index 00000000000..43af8a96c51
--- /dev/null
+++ b/packages/reactivity/__tests__/reactiveObject.bench.ts
@@ -0,0 +1,114 @@
+import { bench } from 'vitest'
+import { ComputedRef, computed, reactive } from '../src'
+
+bench('create reactive obj', () => {
+ reactive({ a: 1 })
+})
+
+{
+ let i = 0
+ const r = reactive({ a: 1 })
+ bench('write reactive obj property', () => {
+ r.a = i++
+ })
+}
+
+{
+ const r = reactive({ a: 1 })
+ computed(() => {
+ return r.a * 2
+ })
+ let i = 0
+ bench("write reactive obj, don't read computed (never invoked)", () => {
+ r.a = i++
+ })
+}
+
+{
+ const r = reactive({ a: 1 })
+ const c = computed(() => {
+ return r.a * 2
+ })
+ c.value
+ let i = 0
+ bench("write reactive obj, don't read computed (invoked)", () => {
+ r.a = i++
+ })
+}
+
+{
+ const r = reactive({ a: 1 })
+ const c = computed(() => {
+ return r.a * 2
+ })
+ let i = 0
+ bench('write reactive obj, read computed', () => {
+ r.a = i++
+ c.value
+ })
+}
+
+{
+ const r = reactive({ a: 1 })
+ const computeds = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return r.a * 2
+ })
+ computeds.push(c)
+ }
+ let i = 0
+ bench("write reactive obj, don't read 1000 computeds (never invoked)", () => {
+ r.a = i++
+ })
+}
+
+{
+ const r = reactive({ a: 1 })
+ const computeds = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return r.a * 2
+ })
+ c.value
+ computeds.push(c)
+ }
+ let i = 0
+ bench("write reactive obj, don't read 1000 computeds (invoked)", () => {
+ r.a = i++
+ })
+}
+
+{
+ const r = reactive({ a: 1 })
+ const computeds: ComputedRef[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ const c = computed(() => {
+ return r.a * 2
+ })
+ computeds.push(c)
+ }
+ let i = 0
+ bench('write reactive obj, read 1000 computeds', () => {
+ r.a = i++
+ computeds.forEach(c => c.value)
+ })
+}
+
+{
+ const reactives: Record[] = []
+ for (let i = 0, n = 1000; i < n; i++) {
+ reactives.push(reactive({ a: i }))
+ }
+ const c = computed(() => {
+ let total = 0
+ reactives.forEach(r => (total += r.a))
+ return total
+ })
+ let i = 0
+ const n = reactives.length
+ bench('1000 reactive objs, 1 computed', () => {
+ reactives[i++ % n].a++
+ c.value
+ })
+}
diff --git a/packages/reactivity/__tests__/ref.bench.ts b/packages/reactivity/__tests__/ref.bench.ts
new file mode 100644
index 00000000000..a1b625ab7ac
--- /dev/null
+++ b/packages/reactivity/__tests__/ref.bench.ts
@@ -0,0 +1,33 @@
+import { describe, bench } from 'vitest'
+import { ref } from '../src/index'
+
+describe('ref', () => {
+ bench('create ref', () => {
+ ref(100)
+ })
+
+ {
+ let i = 0
+ const v = ref(100)
+ bench('write ref', () => {
+ v.value = i++
+ })
+ }
+
+ {
+ const v = ref(100)
+ bench('read ref', () => {
+ v.value
+ })
+ }
+
+ {
+ let i = 0
+ const v = ref(100)
+ bench('write/read ref', () => {
+ v.value = i++
+
+ v.value
+ })
+ }
+})
diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts
index d365b673360..0dffcf35889 100644
--- a/packages/reactivity/src/baseHandlers.ts
+++ b/packages/reactivity/src/baseHandlers.ts
@@ -101,19 +101,25 @@ class BaseReactiveHandler implements ProxyHandler {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
- } else if (
- key === ReactiveFlags.RAW &&
- receiver ===
- (isReadonly
- ? shallow
- ? shallowReadonlyMap
- : readonlyMap
- : shallow
- ? shallowReactiveMap
- : reactiveMap
- ).get(target)
- ) {
- return target
+ } else if (key === ReactiveFlags.RAW) {
+ if (
+ receiver ===
+ (isReadonly
+ ? shallow
+ ? shallowReadonlyMap
+ : readonlyMap
+ : shallow
+ ? shallowReactiveMap
+ : reactiveMap
+ ).get(target) ||
+ // receiver is not the reactive proxy, but has the same prototype
+ // this means the reciever is a user proxy of the reactive proxy
+ Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
+ ) {
+ return target
+ }
+ // early return undefined
+ return
}
const targetIsArray = isArray(target)
@@ -169,17 +175,19 @@ class MutableReactiveHandler extends BaseReactiveHandler {
receiver: object
): boolean {
let oldValue = (target as any)[key]
- if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
- return false
- }
if (!this._shallow) {
+ const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
- oldValue.value = value
- return true
+ if (isOldValueReadonly) {
+ return false
+ } else {
+ oldValue.value = value
+ return true
+ }
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts
index 56dc7667c99..eb399dffdbd 100644
--- a/packages/reactivity/src/ref.ts
+++ b/packages/reactivity/src/ref.ts
@@ -100,7 +100,6 @@ export function isRef(r: any): r is Ref {
* @param value - The object to wrap in the ref.
* @see {@link https://vuejs.org/api/reactivity-core.html#ref}
*/
-export function ref(value: T): T
export function ref(value: T): Ref>
export function ref(): Ref
export function ref(value?: unknown) {
diff --git a/packages/runtime-core/__tests__/apiWatch.bench.ts b/packages/runtime-core/__tests__/apiWatch.bench.ts
new file mode 100644
index 00000000000..89693a446ac
--- /dev/null
+++ b/packages/runtime-core/__tests__/apiWatch.bench.ts
@@ -0,0 +1,58 @@
+import { nextTick, ref, watch, watchEffect } from '../src'
+import { bench } from 'vitest'
+
+bench('create watcher', () => {
+ const v = ref(100)
+ watch(v, v => {})
+})
+
+{
+ const v = ref(100)
+ watch(v, v => {})
+ let i = 0
+ bench('update ref to trigger watcher (scheduled but not executed)', () => {
+ v.value = i++
+ })
+}
+
+{
+ const v = ref(100)
+ watch(v, v => {})
+ let i = 0
+ bench('update ref to trigger watcher (executed)', async () => {
+ v.value = i++
+ return nextTick()
+ })
+}
+
+{
+ bench('create watchEffect', () => {
+ watchEffect(() => {})
+ })
+}
+
+{
+ const v = ref(100)
+ watchEffect(() => {
+ v.value
+ })
+ let i = 0
+ bench(
+ 'update ref to trigger watchEffect (scheduled but not executed)',
+ () => {
+ v.value = i++
+ }
+ )
+}
+
+{
+ const v = ref(100)
+ watchEffect(() => {
+ v.value
+ })
+ let i = 0
+ bench('update ref to trigger watchEffect (executed)', async () => {
+ v.value = i++
+ await nextTick()
+ })
+}
diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts
index 1bc012bb36b..0f5782e3009 100644
--- a/packages/runtime-core/__tests__/apiWatch.spec.ts
+++ b/packages/runtime-core/__tests__/apiWatch.spec.ts
@@ -549,6 +549,98 @@ describe('api: watch', () => {
expect(cb).not.toHaveBeenCalled()
})
+ // #7030
+ it('should not fire on child component unmount w/ flush: pre', async () => {
+ const visible = ref(true)
+ const cb = vi.fn()
+ const Parent = defineComponent({
+ props: ['visible'],
+ render() {
+ return visible.value ? h(Comp) : null
+ }
+ })
+ const Comp = {
+ setup() {
+ watch(visible, cb, { flush: 'pre' })
+ },
+ render() {}
+ }
+ const App = {
+ render() {
+ return h(Parent, {
+ visible: visible.value
+ })
+ }
+ }
+ render(h(App), nodeOps.createElement('div'))
+ expect(cb).not.toHaveBeenCalled()
+ visible.value = false
+ await nextTick()
+ expect(cb).not.toHaveBeenCalled()
+ })
+
+ // #7030
+ it('flush: pre watcher in child component should not fire before parent update', async () => {
+ const b = ref(0)
+ const calls: string[] = []
+
+ const Comp = {
+ setup() {
+ watch(
+ () => b.value,
+ val => {
+ calls.push('watcher child')
+ },
+ { flush: 'pre' }
+ )
+ return () => {
+ b.value
+ calls.push('render child')
+ }
+ }
+ }
+
+ const Parent = {
+ props: ['a'],
+ setup() {
+ watch(
+ () => b.value,
+ val => {
+ calls.push('watcher parent')
+ },
+ { flush: 'pre' }
+ )
+ return () => {
+ b.value
+ calls.push('render parent')
+ return h(Comp)
+ }
+ }
+ }
+
+ const App = {
+ render() {
+ return h(Parent, {
+ a: b.value
+ })
+ }
+ }
+
+ render(h(App), nodeOps.createElement('div'))
+ expect(calls).toEqual(['render parent', 'render child'])
+
+ b.value++
+ await nextTick()
+ expect(calls).toEqual([
+ 'render parent',
+ 'render child',
+ 'watcher parent',
+ 'render parent',
+ 'watcher child',
+ 'render child'
+ ])
+ })
+
// #1763
it('flush: pre watcher watching props should fire before child update', async () => {
const a = ref(0)
diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts
index d822a992816..6fec755106a 100644
--- a/packages/runtime-core/__tests__/components/Suspense.spec.ts
+++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts
@@ -1185,6 +1185,72 @@ describe('Suspense', () => {
expect(calls).toEqual([`one mounted`, `one unmounted`, `two mounted`])
})
+ test('mount the fallback content is in the correct position', async () => {
+ const makeComp = (name: string, delay = 0) =>
+ defineAsyncComponent(
+ {
+ setup() {
+ return () => h('div', [name])
+ }
+ },
+ delay
+ )
+
+ const One = makeComp('one')
+ const Two = makeComp('two', 20)
+
+ const view = shallowRef(One)
+
+ const Comp = {
+ setup() {
+ return () =>
+ h('div', [
+ h(
+ Suspense,
+ {
+ timeout: 10
+ },
+ {
+ default: h(view.value),
+ fallback: h('div', 'fallback')
+ }
+ ),
+ h('div', 'three')
+ ])
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(
+ ``
+ )
+
+ await deps[0]
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ ``
+ )
+
+ view.value = Two
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ ``
+ )
+
+ await new Promise(r => setTimeout(r, 10))
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ ``
+ )
+
+ await deps[1]
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ ``
+ )
+ })
+
// #2214
// Since suspense renders its own root like a component, it should not patch
// its content in optimized mode.
diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts
index e3910182509..2c2f0d8326b 100644
--- a/packages/runtime-core/src/apiSetupHelpers.ts
+++ b/packages/runtime-core/src/apiSetupHelpers.ts
@@ -66,9 +66,9 @@ const warnRuntimeUsage = (method: string) =>
* foo?: string
* bar: number
* }>()
+ * ```
*
* @see {@link https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits}
- * ```
*
* This is only usable inside `