chore: Merge branch 'main' into fix-hydrate-teleport

This commit is contained in:
linzhe141 2024-10-23 15:01:39 +08:00
commit 9f1d198a4b
93 changed files with 2778 additions and 1160 deletions

View File

@ -1,3 +1,107 @@
## [3.5.12](https://github.com/vuejs/core/compare/v3.5.11...v3.5.12) (2024-10-11)
### Bug Fixes
* **compiler-dom:** avoid stringify option with null value ([#12096](https://github.com/vuejs/core/issues/12096)) ([f6d9926](https://github.com/vuejs/core/commit/f6d99262364b7444ebab8742158599e8cdd79eaa)), closes [#12093](https://github.com/vuejs/core/issues/12093)
* **compiler-sfc:** do not skip TSInstantiationExpression when transforming props destructure ([#12064](https://github.com/vuejs/core/issues/12064)) ([d3ecde8](https://github.com/vuejs/core/commit/d3ecde8a696ff62c8d0ab067fd1d7ee0565b63c5))
* **compiler-sfc:** use sass modern api if available and avoid deprecation warning ([#11992](https://github.com/vuejs/core/issues/11992)) ([4474c11](https://github.com/vuejs/core/commit/4474c113d1fb1c26298dd6794275d5b5c7cc4d93))
* **compiler:** clone loc to `ifNode` ([#12131](https://github.com/vuejs/core/issues/12131)) ([cde2c06](https://github.com/vuejs/core/commit/cde2c0671b00d4f6111fcbd7aa76e45872f20b0c)), closes [vuejs/language-tools#4911](https://github.com/vuejs/language-tools/issues/4911)
* **custom-element:** properly remove hyphenated attribute ([#12143](https://github.com/vuejs/core/issues/12143)) ([e16e9a7](https://github.com/vuejs/core/commit/e16e9a7341e7cfb3c443da4e5e5b06e8158712c3)), closes [#12139](https://github.com/vuejs/core/issues/12139)
* **defineModel:** handle kebab-case model correctly ([#12063](https://github.com/vuejs/core/issues/12063)) ([c0418a3](https://github.com/vuejs/core/commit/c0418a3b8fa96a0b108ab71b7aab5d3388f90557)), closes [#12060](https://github.com/vuejs/core/issues/12060)
* **deps:** update dependency monaco-editor to ^0.52.0 ([#12119](https://github.com/vuejs/core/issues/12119)) ([f7cbea2](https://github.com/vuejs/core/commit/f7cbea2111c7770a180b640f36f6a5d4d6abc698))
* **hydration:** provide compat fallback for idle callback hydration strategy ([#11935](https://github.com/vuejs/core/issues/11935)) ([1ae545a](https://github.com/vuejs/core/commit/1ae545a3786abef983be1c969726489685569c92))
* **reactivity:** trigger reactivity for Map key `undefined` ([#12055](https://github.com/vuejs/core/issues/12055)) ([7ad289e](https://github.com/vuejs/core/commit/7ad289e1e7fea654524008ff91e43a8b8a55ef22)), closes [#12054](https://github.com/vuejs/core/issues/12054)
* **runtime-core:** allow symbol values for slot prop key ([#12069](https://github.com/vuejs/core/issues/12069)) ([d9d4d4e](https://github.com/vuejs/core/commit/d9d4d4e158cd51a9ddda249f29de8467f60b2792)), closes [#12068](https://github.com/vuejs/core/issues/12068)
* **runtime-core:** fix required prop check false positive for kebab-case edge cases ([#12034](https://github.com/vuejs/core/issues/12034)) ([9da1ac1](https://github.com/vuejs/core/commit/9da1ac156552ac449754e1373aac7e349841becb)), closes [#12011](https://github.com/vuejs/core/issues/12011)
* **runtime-dom:** prevent unnecessary updates in v-model checkbox when value is unchanged ([#12146](https://github.com/vuejs/core/issues/12146)) ([ea943af](https://github.com/vuejs/core/commit/ea943afe404c4ca4b729906c5e8daf7aa2ccde9b)), closes [#12144](https://github.com/vuejs/core/issues/12144)
* **teleport:** handle disabled teleport with updateCssVars ([#12113](https://github.com/vuejs/core/issues/12113)) ([76a8223](https://github.com/vuejs/core/commit/76a8223199c148b79a5c0ea19e235164809760cd)), closes [#12112](https://github.com/vuejs/core/issues/12112)
* **transition/ssr:** make transition appear work with Suspense in SSR ([#12047](https://github.com/vuejs/core/issues/12047)) ([f1a4f67](https://github.com/vuejs/core/commit/f1a4f67aedfe83e440c54222213f070774faa421)), closes [#12046](https://github.com/vuejs/core/issues/12046)
* **types:** ensure `this.$props` type does not include `string` ([#12123](https://github.com/vuejs/core/issues/12123)) ([704173e](https://github.com/vuejs/core/commit/704173e24276706de672cca6c9507e4dd9651197)), closes [#12122](https://github.com/vuejs/core/issues/12122)
* **types:** retain union type narrowing with defaults applied ([#12108](https://github.com/vuejs/core/issues/12108)) ([05685a9](https://github.com/vuejs/core/commit/05685a9d7c42d4cd37169b867833776b91154fed)), closes [#12106](https://github.com/vuejs/core/issues/12106)
* **useId:** ensure useId consistency when using serverPrefetch ([#12128](https://github.com/vuejs/core/issues/12128)) ([b4d3534](https://github.com/vuejs/core/commit/b4d35349d8bc39aa15bd3f1094d230e5928b177c)), closes [#12102](https://github.com/vuejs/core/issues/12102)
* **watch:** watchEffect clean-up with SSR ([#12097](https://github.com/vuejs/core/issues/12097)) ([b094c72](https://github.com/vuejs/core/commit/b094c72b3d40c52c7124f145a9db028509a11202)), closes [#11956](https://github.com/vuejs/core/issues/11956)
### Performance Improvements
* **reactivity:** avoid unnecessary recursion in removeSub ([#12135](https://github.com/vuejs/core/issues/12135)) ([ec917cf](https://github.com/vuejs/core/commit/ec917cfdb9d0169cd0835d3a0e28244242657dc9))
## [3.5.11](https://github.com/vuejs/core/compare/v3.5.10...v3.5.11) (2024-10-03)
### Bug Fixes
* **compiler-sfc:** do not skip `TSSatisfiesExpression` when transforming props destructure ([#12062](https://github.com/vuejs/core/issues/12062)) ([2328b05](https://github.com/vuejs/core/commit/2328b051f4efa1f1394b7d4e73b7c3f76e430e7c)), closes [#12061](https://github.com/vuejs/core/issues/12061)
* **reactivity:** prevent overwriting `next` property during batch processing ([#12075](https://github.com/vuejs/core/issues/12075)) ([d3f5e6e](https://github.com/vuejs/core/commit/d3f5e6e5319b4ffaa55ca9a2ea3d95d78e76fa58)), closes [#12072](https://github.com/vuejs/core/issues/12072)
* **scheduler:** job ordering when the post queue is flushing ([#12090](https://github.com/vuejs/core/issues/12090)) ([577edca](https://github.com/vuejs/core/commit/577edca8e7795436efd710d1c289ea8ea2642b0e))
* **types:** correctly infer `TypeProps` when it is `any` ([#12073](https://github.com/vuejs/core/issues/12073)) ([57315ab](https://github.com/vuejs/core/commit/57315ab9688c9741a271d1075bbd28cbe5f71e2f)), closes [#12058](https://github.com/vuejs/core/issues/12058)
* **types:** should not intersect `PublicProps` with `Props` ([#12077](https://github.com/vuejs/core/issues/12077)) ([6f85894](https://github.com/vuejs/core/commit/6f8589437635706f825ccec51800effba1d2bf5f))
* **types:** infer the first generic type of `Ref` correctly ([#12094](https://github.com/vuejs/core/issues/12094)) ([c97bb84](https://github.com/vuejs/core/commit/c97bb84d0b0a16b012f886b6498e924415ed63e5))
## [3.5.10](https://github.com/vuejs/core/compare/v3.5.9...v3.5.10) (2024-09-27)
### Bug Fixes
* **custom-element:** properly set kebab-case props on Vue custom elements ([ea3efa0](https://github.com/vuejs/core/commit/ea3efa09e008918c1d9ba7226833a8b1a7a57244)), closes [#12030](https://github.com/vuejs/core/issues/12030) [#12032](https://github.com/vuejs/core/issues/12032)
* **reactivity:** fix nested batch edge case ([93c95dd](https://github.com/vuejs/core/commit/93c95dd4cd416503f43a98a1455f62658d22b0b2))
* **reactivity:** only clear notified flags for computed in first batch iteration ([aa9ef23](https://github.com/vuejs/core/commit/aa9ef2386a0cd39a174e5a887ec2b1a3525034fc)), closes [#12045](https://github.com/vuejs/core/issues/12045)
* **types/ref:** handle nested refs in UnwrapRef ([#12049](https://github.com/vuejs/core/issues/12049)) ([e2c19c2](https://github.com/vuejs/core/commit/e2c19c20cfee9788519a80c0e53e216b78505994)), closes [#12044](https://github.com/vuejs/core/issues/12044)
## [3.5.9](https://github.com/vuejs/core/compare/v3.5.8...v3.5.9) (2024-09-26)
### Bug Fixes
* **reactivity:** fix property dep removal regression ([6001e5c](https://github.com/vuejs/core/commit/6001e5c81a05c894586f9287fbd991677bdd0455)), closes [#12020](https://github.com/vuejs/core/issues/12020) [#12021](https://github.com/vuejs/core/issues/12021)
* **reactivity:** fix recursive sync watcher on computed edge case ([10ff159](https://github.com/vuejs/core/commit/10ff15924053d9bd95ad706f78ce09e288213fcf)), closes [#12033](https://github.com/vuejs/core/issues/12033) [#12037](https://github.com/vuejs/core/issues/12037)
* **runtime-core:** avoid rendering plain object as VNode ([#12038](https://github.com/vuejs/core/issues/12038)) ([cb34b28](https://github.com/vuejs/core/commit/cb34b28a4a9bf868be4785b001c526163eda342e)), closes [#12035](https://github.com/vuejs/core/issues/12035) [vitejs/vite-plugin-vue#353](https://github.com/vitejs/vite-plugin-vue/issues/353)
* **runtime-core:** make useId() always return a string ([a177092](https://github.com/vuejs/core/commit/a177092754642af2f98c33a4feffe8f198c3c950))
* **types:** correct type inference of union event names ([#12022](https://github.com/vuejs/core/issues/12022)) ([4da6881](https://github.com/vuejs/core/commit/4da688141d9e7c15b622c289deaa81b11845b2c7))
* **vue:** properly cache runtime compilation ([#12019](https://github.com/vuejs/core/issues/12019)) ([fa0ba24](https://github.com/vuejs/core/commit/fa0ba24b3ace02d7ecab65e57c2bea89a2550dcb))
## [3.5.8](https://github.com/vuejs/core/compare/v3.5.7...v3.5.8) (2024-09-22)
### Bug Fixes
* **reactivity:** do not remove dep from depsMap when cleaning up deps of computed ([#11995](https://github.com/vuejs/core/issues/11995)) ([0267a58](https://github.com/vuejs/core/commit/0267a588017eee4951ac2a877fe1ccae84cad905))
## [3.5.7](https://github.com/vuejs/core/compare/v3.5.6...v3.5.7) (2024-09-20)
### Bug Fixes
* **compile-core:** fix v-model with newlines edge case ([#11960](https://github.com/vuejs/core/issues/11960)) ([6224288](https://github.com/vuejs/core/commit/62242886d705ece88dbcad45bb78072ecccad0ca)), closes [#8306](https://github.com/vuejs/core/issues/8306)
* **compiler-sfc:** initialize scope with null prototype object ([#11963](https://github.com/vuejs/core/issues/11963)) ([215e154](https://github.com/vuejs/core/commit/215e15407294bf667261360218f975b88c99c2e5))
* **hydration:** avoid observing non-Element node ([#11954](https://github.com/vuejs/core/issues/11954)) ([7257e6a](https://github.com/vuejs/core/commit/7257e6a34200409b3fc347d3bb807e11e2785974)), closes [#11952](https://github.com/vuejs/core/issues/11952)
* **reactivity:** do not remove dep from depsMap when unsubbed by computed ([960706e](https://github.com/vuejs/core/commit/960706eebf73f08ebc9d5dd853a05def05e2c153))
* **reactivity:** fix dev-only memory leak by updating dep.subsHead on sub removal ([5c8b76e](https://github.com/vuejs/core/commit/5c8b76ed6cfbbcee4cbaac0b72beab7291044e4f)), closes [#11956](https://github.com/vuejs/core/issues/11956)
* **reactivity:** fix memory leak from dep instances of garbage collected objects ([235ea47](https://github.com/vuejs/core/commit/235ea4772ed2972914cf142da8b7ac1fb04f7585)), closes [#11979](https://github.com/vuejs/core/issues/11979) [#11971](https://github.com/vuejs/core/issues/11971)
* **reactivity:** fix triggerRef call on ObjectRefImpl returned by toRef ([#11986](https://github.com/vuejs/core/issues/11986)) ([b030c8b](https://github.com/vuejs/core/commit/b030c8bc7327877efb98aa3d9a58eb287a6ff07a)), closes [#11982](https://github.com/vuejs/core/issues/11982)
* **scheduler:** ensure recursive jobs can't be queued twice ([#11955](https://github.com/vuejs/core/issues/11955)) ([d18d6aa](https://github.com/vuejs/core/commit/d18d6aa1b20dc57a8103c51ec4d61e8e53ed936d))
* **ssr:** don't render comments in TransitionGroup ([#11961](https://github.com/vuejs/core/issues/11961)) ([a2f6ede](https://github.com/vuejs/core/commit/a2f6edeb02faedbb673c4bc5c6a59d9a79a37d07)), closes [#11958](https://github.com/vuejs/core/issues/11958)
* **transition:** respect `duration` setting even when it is `0` ([#11967](https://github.com/vuejs/core/issues/11967)) ([f927a4a](https://github.com/vuejs/core/commit/f927a4ae6f7c453f70ba89498ee0c737dc9866fd))
* **types:** correct type inference of all-optional props ([#11644](https://github.com/vuejs/core/issues/11644)) ([9eca65e](https://github.com/vuejs/core/commit/9eca65ee9871d1ac878755afa9a3eb1b02030350)), closes [#11733](https://github.com/vuejs/core/issues/11733) [vuejs/language-tools#4704](https://github.com/vuejs/language-tools/issues/4704)
### Performance Improvements
* **hydration:** avoid observer if element is in viewport ([#11639](https://github.com/vuejs/core/issues/11639)) ([e075dfa](https://github.com/vuejs/core/commit/e075dfad5c7649c6045e3711687ec888e7aa1a39))
## [3.5.6](https://github.com/vuejs/core/compare/v3.5.5...v3.5.6) (2024-09-16)

View File

@ -1,6 +1,6 @@
import importX from 'eslint-plugin-import-x'
import tseslint from 'typescript-eslint'
import vitest from 'eslint-plugin-vitest'
import vitest from '@vitest/eslint-plugin'
import { builtinModules } from 'node:module'
const DOMGlobals = ['window', 'document']

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "3.5.6",
"packageManager": "pnpm@9.10.0",
"version": "3.5.12",
"packageManager": "pnpm@9.12.2",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@ -61,51 +61,51 @@
"devDependencies": {
"@babel/parser": "catalog:",
"@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.7.26",
"@swc/core": "^1.7.36",
"@types/hash-sum": "^1.0.2",
"@types/node": "^20.16.5",
"@types/node": "^20.16.13",
"@types/semver": "^7.5.8",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^2.1.1",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
"esbuild": "^0.23.1",
"esbuild": "^0.24.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.10.0",
"eslint-plugin-import-x": "^4.2.1",
"eslint-plugin-vitest": "^0.5.4",
"eslint": "^9.13.0",
"eslint-plugin-import-x": "^4.3.1",
"@vitest/eslint-plugin": "^1.0.1",
"estree-walker": "catalog:",
"jsdom": "^25.0.0",
"lint-staged": "^15.2.10",
"lodash": "^4.17.21",
"magic-string": "^0.30.11",
"magic-string": "^0.30.12",
"markdown-table": "^3.0.3",
"marked": "13.0.3",
"npm-run-all2": "^6.2.3",
"picocolors": "^1.1.0",
"npm-run-all2": "^6.2.6",
"picocolors": "^1.1.1",
"prettier": "^3.3.3",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~23.3.0",
"rimraf": "^6.0.1",
"rollup": "^4.21.3",
"rollup": "^4.24.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.3",
"serve": "^14.2.3",
"serve-handler": "^6.1.5",
"serve": "^14.2.4",
"serve-handler": "^6.1.6",
"simple-git-hooks": "^2.11.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.7.0",
"tslib": "^2.8.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.5.0",
"typescript-eslint": "^8.10.0",
"vite": "catalog:",
"vitest": "^2.1.1"
},

View File

@ -2068,3 +2068,13 @@ expectString(instance.actionText)
// public prop on $props should be optional
// @ts-expect-error
expectString(instance.$props.actionText)
// #12122
defineComponent({
props: { foo: String },
render() {
expectType<{ readonly foo?: string }>(this.$props)
// @ts-expect-error
expectType<string>(this.$props)
},
})

View File

@ -189,6 +189,24 @@ describe('allow getter and setter types to be unrelated', <T>() => {
f.value = ref(1)
})
describe('correctly unwraps nested refs', () => {
const obj = {
n: 24,
ref: ref(24),
nestedRef: ref({ n: ref(0) }),
}
const a = ref(obj)
expectType<number>(a.value.n)
expectType<number>(a.value.ref)
expectType<number>(a.value.nestedRef.n)
const b = reactive({ a })
expectType<number>(b.a.n)
expectType<number>(b.a.ref)
expectType<number>(b.a.nestedRef.n)
})
// computed
describe('allow computed getter and setter types to be unrelated', () => {
const obj = ref({

View File

@ -240,6 +240,23 @@ describe('withDefaults w/ defineProp type is different from the defaults type',
res1.value
})
describe('withDefaults w/ defineProp discriminate union type', () => {
const props = withDefaults(
defineProps<
{ type: 'button'; buttonType?: 'submit' } | { type: 'link'; href: string }
>(),
{
type: 'button',
},
)
if (props.type === 'button') {
expectType<'submit' | undefined>(props.buttonType)
}
if (props.type === 'link') {
expectType<string>(props.href)
}
})
describe('defineProps w/ runtime declaration', () => {
// runtime declaration
const props = defineProps({

View File

@ -123,6 +123,7 @@ onMounted(() => {
:prod="productionMode"
:ssr="useSSRMode"
:autoSave="autoSave"
:theme="theme"
@toggle-theme="toggleTheme"
@toggle-prod="toggleProdMode"
@toggle-ssr="toggleSSR"
@ -136,7 +137,8 @@ onMounted(() => {
@keydown.ctrl.s.prevent
@keydown.meta.s.prevent
:ssr="useSSRMode"
:autoSave="autoSave"
:model-value="autoSave"
:editorOptions="{ autoSaveText: false }"
:store="store"
:showCompileOutput="true"
:autoResize="true"

View File

@ -15,6 +15,7 @@ const props = defineProps<{
prod: boolean
ssr: boolean
autoSave: boolean
theme: 'dark' | 'light'
}>()
const emit = defineEmits([
'toggle-theme',
@ -117,7 +118,11 @@ function toggleDark() {
>
<span>{{ autoSave ? 'AutoSave ON' : 'AutoSave OFF' }}</span>
</button>
<button title="Toggle dark mode" class="toggle-dark" @click="toggleDark">
<button
:title="`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`"
class="toggle-dark"
@click="toggleDark"
>
<Sun class="light" />
<Moon class="dark" />
</button>

View File

@ -11,7 +11,7 @@
"vue": "^3.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.3",
"vite": "^5.4.5"
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.9"
}
}

View File

@ -11,7 +11,7 @@
"enableNonBrowserBranches": true
},
"dependencies": {
"monaco-editor": "^0.51.0",
"monaco-editor": "^0.52.0",
"source-map-js": "^1.2.1"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
"version": "3.5.6",
"version": "3.5.12",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",

View File

@ -933,6 +933,10 @@ function getLoc(start: number, end?: number): SourceLocation {
}
}
export function cloneLoc(loc: SourceLocation): SourceLocation {
return getLoc(loc.start.offset, loc.end.offset)
}
function setLocEnd(loc: SourceLocation, end: number) {
loc.end = tokenizer.getPos(end)
loc.source = getSlice(loc.start.offset, end)

View File

@ -30,6 +30,7 @@ import {
import { ErrorCodes, createCompilerError } from '../errors'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { cloneLoc } from '../parser'
import { CREATE_COMMENT, FRAGMENT } from '../runtimeHelpers'
import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils'
import { PatchFlags } from '@vue/shared'
@ -110,7 +111,7 @@ export function processIf(
const branch = createIfBranch(node, dir)
const ifNode: IfNode = {
type: NodeTypes.IF,
loc: node.loc,
loc: cloneLoc(node.loc),
branches: [branch],
}
context.replaceNode(ifNode)

View File

@ -31,7 +31,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
// we assume v-model directives are always parsed
// (not artificially created by a transform)
const rawExp = exp.loc.source
const rawExp = exp.loc.source.trim()
const expString =
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp

View File

@ -32,6 +32,23 @@ return function render(_ctx, _cache) {
}"
`;
exports[`stringify static html > should bail for <option> elements with null values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("select", null, [
_createElementVNode("option", { value: null }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" })
], -1 /* HOISTED */)
])))
}"
`;
exports[`stringify static html > should bail for <option> elements with number values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

View File

@ -470,6 +470,17 @@ describe('stringify static html', () => {
expect(code).toMatchSnapshot()
})
test('should bail for <option> elements with null values', () => {
const { ast, code } = compileWithStringify(
`<div><select><option :value="null" />${repeat(
`<option value="1" />`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select></div>`,
)
expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
expect(code).toMatchSnapshot()
})
test('eligible content (elements > 20) + non-eligible content', () => {
const { code } = compileWithStringify(
`<div>${repeat(

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
"version": "3.5.6",
"version": "3.5.12",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",

View File

@ -261,8 +261,7 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
isOptionTag &&
isStaticArgOf(p.arg, 'value') &&
p.exp &&
p.exp.ast &&
p.exp.ast.type !== 'StringLiteral'
!p.exp.isStatic
) {
return bail()
}

View File

@ -1084,6 +1084,29 @@ return (_ctx, _cache) => {
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > v-model w/ newlines codegen 1`] = `
"import { unref as _unref, isRef as _isRef, vModelText as _vModelText, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export default {
setup(__props) {
const count = ref(0)
return (_ctx, _cache) => {
return _withDirectives((_openBlock(), _createElementBlock("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (_isRef(count) ? (count).value = $event : null))
}, null, 512 /* NEED_PATCH */)), [
[_vModelText,
_unref(count)
]
])
}
}
}"
`;
exports[`SFC compile <script setup> > inlineTemplate mode > with defineExpose() 1`] = `
"
export default {

View File

@ -472,6 +472,23 @@ describe('SFC compile <script setup>', () => {
assertCode(content)
})
test('v-model w/ newlines codegen', () => {
const { content } = compile(
`<script setup>
const count = ref(0)
</script>
<template>
<input v-model="
count
">
</template>
`,
{ inlineTemplate: true },
)
expect(content).toMatch(`_isRef(count) ? (count).value = $event : null`)
assertCode(content)
})
test('v-model should not generate ref assignment code for non-setup bindings', () => {
const { content } = compile(
`<script setup>

View File

@ -233,6 +233,33 @@ export default /*@__PURE__*/_defineComponent({
return { }
}
})"
`;
exports[`defineProps > w/ extends intersection type 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Foo = {
x?: number;
};
interface Props extends Foo {
z: number
y: string
}
export default /*@__PURE__*/_defineComponent({
props: {
z: { type: Number, required: true },
y: { type: String, required: true },
x: { type: Number, required: false }
},
setup(__props: any, { expose: __expose }) {
__expose();
return { }
}
@ -268,6 +295,31 @@ export default /*@__PURE__*/_defineComponent({
return { }
}
})"
`;
exports[`defineProps > w/ intersection type 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Foo = {
x?: number;
};
type Bar = {
y: string;
};
export default /*@__PURE__*/_defineComponent({
props: {
x: { type: Number, required: false },
y: { type: String, required: true }
},
setup(__props: any, { expose: __expose }) {
__expose();
return { }
}

View File

@ -320,3 +320,22 @@ return { rest }
}"
`;
exports[`sfc reactive props destructure > with TSInstantiationExpression 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Foo = <T extends string | number>(data: T) => void
export default /*@__PURE__*/_defineComponent({
props: {
value: { type: Function }
},
setup(__props: any) {
const foo = __props.value<123>
return () => {}
}
})"
`;

View File

@ -261,6 +261,51 @@ const props = defineProps({ foo: String })
})
})
test('w/ extends intersection type', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
type Foo = {
x?: number;
};
interface Props extends Foo {
z: number
y: string
}
defineProps<Props>()
</script>
`)
assertCode(content)
expect(content).toMatch(`z: { type: Number, required: true }`)
expect(content).toMatch(`y: { type: String, required: true }`)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS,
y: BindingTypes.PROPS,
z: BindingTypes.PROPS,
})
})
test('w/ intersection type', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
type Foo = {
x?: number;
};
type Bar = {
y: string;
};
defineProps<Foo & Bar>()
</script>
`)
assertCode(content)
expect(content).toMatch(`y: { type: String, required: true }`)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS,
y: BindingTypes.PROPS,
})
})
test('w/ exported interface', () => {
const { content, bindings } = compile(`
<script setup lang="ts">

View File

@ -198,6 +198,21 @@ describe('sfc reactive props destructure', () => {
}`)
})
test('with TSInstantiationExpression', () => {
const { content } = compile(
`
<script setup lang="ts">
type Foo = <T extends string | number>(data: T) => void
const { value } = defineProps<{ value: Foo }>()
const foo = value<123>
</script>
`,
{ isProd: true },
)
assertCode(content)
expect(content).toMatch(`const foo = __props.value<123>`)
})
test('aliasing', () => {
const { content, bindings } = compile(`
<script setup>

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.5.6",
"version": "3.5.12",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@ -62,6 +62,6 @@
"postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.2",
"pug": "^3.0.3",
"sass": "^1.78.0"
"sass": "^1.80.3"
}
}

View File

@ -18,6 +18,7 @@ import { createCache } from './cache'
import type { ImportBinding } from './compileScript'
import { isImportUsed } from './script/importUsageCheck'
import type { LRUCache } from 'lru-cache'
import { genCacheKey } from '@vue/shared'
export const DEFAULT_FILENAME = 'anonymous.vue'
@ -103,24 +104,14 @@ export const parseCache:
| Map<string, SFCParseResult>
| LRUCache<string, SFCParseResult> = createCache<SFCParseResult>()
function genCacheKey(source: string, options: SFCParseOptions): string {
return (
source +
JSON.stringify(
{
...options,
compiler: { parse: options.compiler?.parse },
},
(_, val) => (typeof val === 'function' ? val.toString() : val),
)
)
}
export function parse(
source: string,
options: SFCParseOptions = {},
): SFCParseResult {
const sourceKey = genCacheKey(source, options)
const sourceKey = genCacheKey(source, {
...options,
compiler: { parse: options.compiler?.parse },
})
const cache = parseCache.get(sourceKey)
if (cache) {
return cache

View File

@ -10,6 +10,7 @@ import type {
import { walk } from 'estree-walker'
import {
BindingTypes,
TS_NODE_TYPES,
extractIdentifiers,
isFunctionType,
isInDestructureAssignment,
@ -102,7 +103,7 @@ export function transformDestructuredProps(
return
}
const rootScope: Scope = {}
const rootScope: Scope = Object.create(null)
const scopeStack: Scope[] = [rootScope]
let currentScope: Scope = rootScope
const excludedIds = new WeakSet<Identifier>()
@ -240,9 +241,7 @@ export function transformDestructuredProps(
if (
parent &&
parent.type.startsWith('TS') &&
parent.type !== 'TSAsExpression' &&
parent.type !== 'TSNonNullExpression' &&
parent.type !== 'TSTypeAssertion'
!TS_NODE_TYPES.includes(parent.type)
) {
return this.skip()
}

View File

@ -23,28 +23,48 @@ export interface StylePreprocessorResults {
// .scss/.sass processor
const scss: StylePreprocessor = (source, map, options, load = require) => {
const nodeSass = load('sass')
const finalOptions = {
...options,
data: getSource(source, options.filename, options.additionalData),
file: options.filename,
outFile: options.filename,
sourceMap: !!map,
}
const nodeSass: typeof import('sass') = load('sass')
const { compileString, renderSync } = nodeSass
const data = getSource(source, options.filename, options.additionalData)
let css: string
let dependencies: string[]
let sourceMap: any
try {
const result = nodeSass.renderSync(finalOptions)
const dependencies = result.stats.includedFiles
if (map) {
return {
code: result.css.toString(),
map: merge(map, JSON.parse(result.map.toString())),
errors: [],
dependencies,
}
if (compileString) {
const { pathToFileURL, fileURLToPath }: typeof import('url') = load('url')
const result = compileString(data, {
...options,
url: pathToFileURL(options.filename),
sourceMap: !!map,
})
css = result.css
dependencies = result.loadedUrls.map(url => fileURLToPath(url))
sourceMap = map ? result.sourceMap! : undefined
} else {
const result = renderSync({
...options,
data,
file: options.filename,
outFile: options.filename,
sourceMap: !!map,
})
css = result.css.toString()
dependencies = result.stats.includedFiles
sourceMap = map ? JSON.parse(result.map!.toString()) : undefined
}
return { code: result.css.toString(), errors: [], dependencies }
if (map) {
return {
code: css,
errors: [],
dependencies,
map: merge(map, sourceMap!),
}
}
return { code: css, errors: [], dependencies }
} catch (e: any) {
return { code: '', errors: [e], dependencies: [] }
}

View File

@ -39,7 +39,7 @@ describe('transition-group', () => {
})
// #11514
test('with static tag + comment', () => {
test('with static tag + v-if comment', () => {
expect(
compile(
`<transition-group tag="ul"><div v-for="i in list"/><div v-if="false"></div></transition-group>`,
@ -60,6 +60,25 @@ describe('transition-group', () => {
`)
})
// #11958
test('with static tag + comment', () => {
expect(
compile(
`<transition-group tag="ul"><div v-for="i in list"/><!--test--></transition-group>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<ul\${_ssrRenderAttrs(_attrs)}>\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</ul>\`)
}"
`)
})
test('with dynamic tag', () => {
expect(
compile(

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
"version": "3.5.6",
"version": "3.5.12",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",

View File

@ -156,7 +156,7 @@ export function processChildren(
context: SSRTransformContext,
asFragment = false,
disableNestedFragments = false,
disableCommentAsIfAlternate = false,
disableComment = false,
): void {
if (asFragment) {
context.pushStringPart(`<!--[-->`)
@ -197,7 +197,9 @@ export function processChildren(
case NodeTypes.COMMENT:
// no need to escape comment here because the AST can only
// contain valid comments.
context.pushStringPart(`<!--${child.content}-->`)
if (!disableComment) {
context.pushStringPart(`<!--${child.content}-->`)
}
break
case NodeTypes.INTERPOLATION:
context.pushStringPart(
@ -207,12 +209,7 @@ export function processChildren(
)
break
case NodeTypes.IF:
ssrProcessIf(
child,
context,
disableNestedFragments,
disableCommentAsIfAlternate,
)
ssrProcessIf(child, context, disableNestedFragments, disableComment)
break
case NodeTypes.FOR:
ssrProcessFor(child, context, disableNestedFragments)

View File

@ -27,7 +27,7 @@ export function ssrProcessIf(
node: IfNode,
context: SSRTransformContext,
disableNestedFragments = false,
disableCommentAsIfAlternate = false,
disableComment = false,
): void {
const [rootBranch] = node.branches
const ifStatement = createIfStatement(
@ -56,7 +56,7 @@ export function ssrProcessIf(
}
}
if (!currentIf.alternate && !disableCommentAsIfAlternate) {
if (!currentIf.alternate && !disableComment) {
currentIf.alternate = createBlockStatement([
createCallExpression(`_push`, ['`<!---->`']),
])

View File

@ -1006,9 +1006,137 @@ describe('reactivity/computed', () => {
expect(serializeInner(root)).toBe(`<button>Step</button><p>Step 2</p>`)
})
it('manual trigger computed', () => {
test('manual trigger computed', () => {
const cValue = computed(() => 1)
triggerRef(cValue)
expect(cValue.value).toBe(1)
})
test('computed should remain live after losing all subscribers', () => {
const state = reactive({ a: 1 })
const p = computed(() => state.a + 1)
const { effect: e } = effect(() => p.value)
e.stop()
expect(p.value).toBe(2)
state.a++
expect(p.value).toBe(3)
})
// #11995
test('computed dep cleanup should not cause property dep to be deleted', () => {
const toggle = ref(true)
const state = reactive({ a: 1 })
const p = computed(() => {
return toggle.value ? state.a : 111
})
const pp = computed(() => state.a)
effect(() => p.value)
expect(pp.value).toBe(1)
toggle.value = false
state.a++
expect(pp.value).toBe(2)
})
// #12020
test('computed value updates correctly after dep cleanup', () => {
const obj = reactive({ foo: 1, flag: 1 })
const c1 = computed(() => obj.foo)
let foo
effect(() => {
foo = obj.flag ? (obj.foo, c1.value) : 0
})
expect(foo).toBe(1)
obj.flag = 0
expect(foo).toBe(0)
obj.foo = 2
obj.flag = 1
expect(foo).toBe(2)
})
// #11928
test('should not lead to exponential perf cost with deeply chained computed', () => {
const start = {
prop1: shallowRef(1),
prop2: shallowRef(2),
prop3: shallowRef(3),
prop4: shallowRef(4),
}
let layer = start
const LAYERS = 1000
for (let i = LAYERS; i > 0; i--) {
const m = layer
const s = {
prop1: computed(() => m.prop2.value),
prop2: computed(() => m.prop1.value - m.prop3.value),
prop3: computed(() => m.prop2.value + m.prop4.value),
prop4: computed(() => m.prop3.value),
}
effect(() => s.prop1.value)
effect(() => s.prop2.value)
effect(() => s.prop3.value)
effect(() => s.prop4.value)
s.prop1.value
s.prop2.value
s.prop3.value
s.prop4.value
layer = s
}
const t = performance.now()
start.prop1.value = 4
start.prop2.value = 3
start.prop3.value = 2
start.prop4.value = 1
expect(performance.now() - t).toBeLessThan(process.env.CI ? 100 : 30)
const end = layer
expect([
end.prop1.value,
end.prop2.value,
end.prop3.value,
end.prop4.value,
]).toMatchObject([-2, -4, 2, 3])
})
test('performance when removing dependencies from deeply nested computeds', () => {
const base = ref(1)
const trigger = ref(true)
const computeds: ComputedRef<number>[] = []
const LAYERS = 30
for (let i = 0; i < LAYERS; i++) {
const earlier = [...computeds]
computeds.push(
computed(() => {
return base.value + earlier.reduce((sum, c) => sum + c.value, 0)
}),
)
}
const tail = computed(() =>
trigger.value ? computeds[computeds.length - 1].value : 0,
)
const t0 = performance.now()
expect(tail.value).toBe(2 ** (LAYERS - 1))
const t1 = performance.now()
expect(t1 - t0).toBeLessThan(process.env.CI ? 100 : 30)
trigger.value = false
expect(tail.value).toBe(0)
const t2 = performance.now()
expect(t2 - t1).toBeLessThan(process.env.CI ? 100 : 30)
})
})

View File

@ -13,6 +13,7 @@ import {
} from '../src/reactive'
import { computed } from '../src/computed'
import { effect } from '../src/effect'
import { targetMap } from '../src/dep'
describe('reactivity/reactive', () => {
test('Object', () => {
@ -398,4 +399,24 @@ describe('reactivity/reactive', () => {
a.value++
}).not.toThrow()
})
// #11979
test('should release property Dep instance if it no longer has subscribers', () => {
let obj = { x: 1 }
let a = reactive(obj)
const e = effect(() => a.x)
expect(targetMap.get(obj)?.get('x')).toBeTruthy()
e.effect.stop()
expect(targetMap.get(obj)?.get('x')).toBeFalsy()
})
test('should trigger reactivity when Map key is undefined', () => {
const map = reactive(new Map())
const c = computed(() => map.get(void 0))
expect(c.value).toBe(void 0)
map.set(void 0, 1)
expect(c.value).toBe(1)
})
})

View File

@ -51,6 +51,7 @@ describe('reactivity/reactive/Array', () => {
const raw = {}
const arr = reactive([{}, {}])
arr.push(raw)
expect(arr.indexOf(raw)).toBe(2)
expect(arr.indexOf(raw, 3)).toBe(-1)
expect(arr.includes(raw)).toBe(true)
@ -89,6 +90,84 @@ describe('reactivity/reactive/Array', () => {
expect(index).toBe(1)
})
// only non-existent reactive will try to search by using its raw value
describe('Array identity methods should not be called more than necessary', () => {
const identityMethods = ['includes', 'indexOf', 'lastIndexOf'] as const
function instrumentArr(rawTarget: any[]) {
identityMethods.forEach(key => {
const spy = vi.fn(rawTarget[key] as any)
rawTarget[key] = spy
})
}
function searchValue(target: any[], ...args: unknown[]) {
return identityMethods.map(key => (target[key] as any)(...args))
}
function unInstrumentArr(rawTarget: any[]) {
identityMethods.forEach(key => {
;(rawTarget[key] as any).mockClear()
// relink to prototype method
rawTarget[key] = Array.prototype[key] as any
})
}
function expectHaveBeenCalledTimes(rawTarget: any[], times: number) {
identityMethods.forEach(key => {
expect(rawTarget[key]).toHaveBeenCalledTimes(times)
})
}
test('should be called once with a non-existent raw value', () => {
const reactiveArr = reactive([])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, {})
expectHaveBeenCalledTimes(toRaw(reactiveArr), 1)
expect(searchResult).toStrictEqual([false, -1, -1])
unInstrumentArr(toRaw(reactiveArr))
})
test('should be called once with an existent reactive value', () => {
const existReactiveValue = reactive({})
const reactiveArr = reactive([existReactiveValue, existReactiveValue])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, existReactiveValue)
expectHaveBeenCalledTimes(toRaw(reactiveArr), 1)
expect(searchResult).toStrictEqual([true, 0, 1])
unInstrumentArr(toRaw(reactiveArr))
})
test('should be called twice with a non-existent reactive value', () => {
const reactiveArr = reactive([])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, reactive({}))
expectHaveBeenCalledTimes(toRaw(reactiveArr), 2)
expect(searchResult).toStrictEqual([false, -1, -1])
unInstrumentArr(toRaw(reactiveArr))
})
test('should be called twice with a non-existent reactive value, but the raw value exists', () => {
const existRaw = {}
const reactiveArr = reactive([existRaw, existRaw])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, reactive(existRaw))
expectHaveBeenCalledTimes(toRaw(reactiveArr), 2)
expect(searchResult).toStrictEqual([true, 0, 1])
unInstrumentArr(toRaw(reactiveArr))
})
})
test('delete on Array should not trigger length dependency', () => {
const arr = reactive([1, 2, 3])
const fn = vi.fn()

View File

@ -4,6 +4,7 @@ import {
WatchErrorCodes,
type WatchOptions,
type WatchScheduler,
computed,
onWatcherCleanup,
ref,
watch,
@ -209,4 +210,71 @@ describe('watch', () => {
source.value++
expect(dummy).toBe(1)
})
// #12033
test('recursive sync watcher on computed', () => {
const r = ref(0)
const c = computed(() => r.value)
watch(c, v => {
if (v > 1) {
r.value--
}
})
expect(r.value).toBe(0)
expect(c.value).toBe(0)
r.value = 10
expect(r.value).toBe(1)
expect(c.value).toBe(1)
})
// edge case where a nested endBatch() causes an effect to be batched in a
// nested batch loop with its .next mutated, causing the outer loop to end
// early
test('nested batch edge case', () => {
// useClamp from VueUse
const clamp = (n: number, min: number, max: number) =>
Math.min(max, Math.max(min, n))
function useClamp(src: Ref<number>, min: number, max: number) {
return computed({
get() {
return (src.value = clamp(src.value, min, max))
},
set(val) {
src.value = clamp(val, min, max)
},
})
}
const src = ref(1)
const clamped = useClamp(src, 1, 5)
watch(src, val => (clamped.value = val))
const spy = vi.fn()
watch(clamped, spy)
src.value = 2
expect(spy).toHaveBeenCalledTimes(1)
src.value = 10
expect(spy).toHaveBeenCalledTimes(2)
})
test('should ensure correct execution order in batch processing', () => {
const dummy: number[] = []
const n1 = ref(0)
const n2 = ref(0)
const sum = computed(() => n1.value + n2.value)
watch(n1, () => {
dummy.push(1)
n2.value++
})
watch(sum, () => dummy.push(2))
watch(n1, () => dummy.push(3))
n1.value++
expect(dummy).toEqual([1, 2, 3])
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
"version": "3.5.6",
"version": "3.5.12",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",

View File

@ -8,7 +8,14 @@ import {
} from './reactive'
import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared'
import {
capitalize,
extend,
hasChanged,
hasOwn,
isMap,
toRawType,
} from '@vue/shared'
import { warn } from './warning'
type CollectionTypes = IterableCollections | WeakCollections
@ -23,152 +30,6 @@ const toShallow = <T extends unknown>(value: T): T => value
const getProto = <T extends CollectionTypes>(v: T): any =>
Reflect.getPrototypeOf(v)
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false,
) {
// #1772: readonly(reactive(Map)) should return readonly + reactive version
// of the value
target = target[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
}
const { has } = getProto(rawTarget)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
// #3602 readonly(reactive(Map))
// ensure that the nested reactive `Map` can do tracking for itself
target.get(key)
}
}
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
}
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
function size(target: IterableCollections, isReadonly = false) {
target = target[ReactiveFlags.RAW]
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
function add(this: SetTypes, value: unknown, _isShallow = false) {
if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
}
function set(this: MapTypes, key: unknown, value: unknown, _isShallow = false) {
if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
}
function deleteEntry(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = target.clear()
if (hadItems) {
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
function createForEach(isReadonly: boolean, isShallow: boolean) {
return function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown,
) {
const observed = this
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
@ -232,74 +93,158 @@ function createReadonlyMethod(type: TriggerOpTypes): Function {
type Instrumentations = Record<string | symbol, Function | number>
function createInstrumentations() {
const mutableInstrumentations: Instrumentations = {
function createInstrumentations(
readonly: boolean,
shallow: boolean,
): Instrumentations {
const instrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key)
// #1772: readonly(reactive(Map)) should return readonly + reactive version
// of the value
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!readonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
}
const { has } = getProto(rawTarget)
const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
// #3602 readonly(reactive(Map))
// ensure that the nested reactive `Map` can do tracking for itself
target.get(key)
}
},
get size() {
return size(this as unknown as IterableCollections)
const target = (this as unknown as IterableCollections)[ReactiveFlags.RAW]
!readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
},
has(this: CollectionTypes, key: unknown): boolean {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!readonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
}
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
},
forEach(this: IterableCollections, callback: Function, thisArg?: unknown) {
const observed = this
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive
!readonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false),
}
const shallowInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key, false, true)
},
get size() {
return size(this as unknown as IterableCollections)
},
has,
add(this: SetTypes, value: unknown) {
return add.call(this, value, true)
},
set(this: MapTypes, key: unknown, value: unknown) {
return set.call(this, key, value, true)
},
delete: deleteEntry,
clear,
forEach: createForEach(false, true),
}
extend(
instrumentations,
readonly
? {
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
}
: {
add(this: SetTypes, value: unknown) {
if (!shallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
},
set(this: MapTypes, key: unknown, value: unknown) {
if (!shallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const { has, get } = getProto(target)
const readonlyInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key, true)
},
get size() {
return size(this as unknown as IterableCollections, true)
},
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, false),
}
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const shallowReadonlyInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key, true, true)
},
get size() {
return size(this as unknown as IterableCollections, true)
},
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, true),
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
},
delete(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
},
clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = target.clear()
if (hadItems) {
trigger(
target,
TriggerOpTypes.CLEAR,
undefined,
undefined,
oldTarget,
)
}
return result
},
},
)
const iteratorMethods = [
'keys',
@ -309,39 +254,14 @@ function createInstrumentations() {
] as const
iteratorMethods.forEach(method => {
mutableInstrumentations[method] = createIterableMethod(method, false, false)
readonlyInstrumentations[method] = createIterableMethod(method, true, false)
shallowInstrumentations[method] = createIterableMethod(method, false, true)
shallowReadonlyInstrumentations[method] = createIterableMethod(
method,
true,
true,
)
instrumentations[method] = createIterableMethod(method, readonly, shallow)
})
return [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations,
]
return instrumentations
}
const [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations,
] = /* @__PURE__*/ createInstrumentations()
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
const instrumentations = createInstrumentations(isReadonly, shallow)
return (
target: CollectionTypes,

View File

@ -84,9 +84,13 @@ export class ComputedRefImpl<T = any> implements Subscriber {
* @internal
*/
isSSR: boolean
/**
* @internal
*/
next?: Subscriber = undefined
// for backwards compat
effect: this = this
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
@ -117,7 +121,7 @@ export class ComputedRefImpl<T = any> implements Subscriber {
// avoid infinite self recursion
activeSub !== this
) {
batch(this)
batch(this, true)
return true
} else if (__DEV__) {
// TODO warn

View File

@ -82,6 +82,17 @@ export class Dep {
*/
subsHead?: Link
/**
* For object property deps cleanup
*/
map?: KeyToDepMap = undefined
key?: unknown = undefined
/**
* Subscriber counter
*/
sc: number = 0
constructor(public computed?: ComputedRefImpl | undefined) {
if (__DEV__) {
this.subsHead = undefined
@ -106,9 +117,7 @@ export class Dep {
activeSub.depsTail = link
}
if (activeSub.flags & EffectFlags.TRACKING) {
addSub(link)
}
addSub(link)
} else if (link.version === -1) {
// reused from last run - already a sub, just sync version
link.version = this.version
@ -190,27 +199,30 @@ export class Dep {
}
function addSub(link: Link) {
const computed = link.dep.computed
// computed getting its first subscriber
// enable tracking + lazily subscribe to all its deps
if (computed && !link.dep.subs) {
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
for (let l = computed.deps; l; l = l.nextDep) {
addSub(l)
link.dep.sc++
if (link.sub.flags & EffectFlags.TRACKING) {
const computed = link.dep.computed
// computed getting its first subscriber
// enable tracking + lazily subscribe to all its deps
if (computed && !link.dep.subs) {
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
for (let l = computed.deps; l; l = l.nextDep) {
addSub(l)
}
}
}
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
if (__DEV__ && link.dep.subsHead === undefined) {
link.dep.subsHead = link
}
if (__DEV__ && link.dep.subsHead === undefined) {
link.dep.subsHead = link
}
link.dep.subs = link
link.dep.subs = link
}
}
// The main WeakMap that stores {target -> key -> dep} connections.
@ -218,7 +230,8 @@ function addSub(link: Link) {
// which maintains a Set of subscribers, but we simply store them as
// raw Maps to reduce memory overhead.
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<object, KeyToDepMap>()
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()
export const ITERATE_KEY: unique symbol = Symbol(
__DEV__ ? 'Object iterate' : '',
@ -249,6 +262,8 @@ export function track(target: object, type: TrackOpTypes, key: unknown): void {
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Dep()))
dep.map = depsMap
dep.key = key
}
if (__DEV__) {
dep.track({
@ -325,7 +340,7 @@ export function trigger(
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
if (key !== void 0 || depsMap.has(void 0)) {
run(depsMap.get(key))
}
@ -367,13 +382,10 @@ export function trigger(
endBatch()
}
/**
* Test only
*/
export function getDepFromReactive(
object: any,
key: string | number | symbol,
): Dep | undefined {
// eslint-disable-next-line
return targetMap.get(object)?.get(key)
const depMap = targetMap.get(object)
return depMap && depMap.get(key)
}

View File

@ -234,9 +234,15 @@ export class ReactiveEffect<T = any>
let batchDepth = 0
let batchedSub: Subscriber | undefined
let batchedComputed: Subscriber | undefined
export function batch(sub: Subscriber): void {
export function batch(sub: Subscriber, isComputed = false): void {
sub.flags |= EffectFlags.NOTIFIED
if (isComputed) {
sub.next = batchedComputed
batchedComputed = sub
return
}
sub.next = batchedSub
batchedSub = sub
}
@ -257,6 +263,17 @@ export function endBatch(): void {
return
}
if (batchedComputed) {
let e: Subscriber | undefined = batchedComputed
batchedComputed = undefined
while (e) {
const next: Subscriber | undefined = e.next
e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED
e = next
}
}
let error: unknown
while (batchedSub) {
let e: Subscriber | undefined = batchedSub
@ -399,7 +416,7 @@ export function refreshComputed(computed: ComputedRefImpl): undefined {
}
}
function removeSub(link: Link) {
function removeSub(link: Link, soft = false) {
const { dep, prevSub, nextSub } = link
if (prevSub) {
prevSub.nextSub = nextSub
@ -409,19 +426,33 @@ function removeSub(link: Link) {
nextSub.prevSub = prevSub
link.nextSub = undefined
}
if (__DEV__ && dep.subsHead === link) {
// was previous head, point new head to next
dep.subsHead = nextSub
}
if (dep.subs === link) {
// was previous tail, point new tail to prev
dep.subs = prevSub
if (!prevSub && dep.computed) {
// if computed, unsubscribe it from all its deps so this computed and its
// value can be GCed
dep.computed.flags &= ~EffectFlags.TRACKING
for (let l = dep.computed.deps; l; l = l.nextDep) {
// here we are only "soft" unsubscribing because the computed still keeps
// referencing the deps and the dep should not decrease its sub count
removeSub(l, true)
}
}
}
if (!dep.subs && dep.computed) {
// last subscriber removed
// if computed, unsubscribe it from all its deps so this computed and its
// value can be GCed
dep.computed.flags &= ~EffectFlags.TRACKING
for (let l = dep.computed.deps; l; l = l.nextDep) {
removeSub(l)
}
if (!soft && !--dep.sc && dep.map) {
// #11979
// property dep no longer has effect subscribers, delete it
// this mostly is for the case where an object is kept in memory but only a
// subset of its properties is tracked at one time
dep.map.delete(dep.key)
}
}

View File

@ -167,7 +167,7 @@ export type DeepReadonly<T> = T extends Builtin
? WeakSet<DeepReadonly<U>>
: T extends Promise<infer U>
? Promise<DeepReadonly<U>>
: T extends Ref<infer U>
: T extends Ref<infer U, unknown>
? Readonly<Ref<DeepReadonly<U>>>
: T extends {}
? { readonly [K in keyof T]: DeepReadonly<T[K]> }

View File

@ -62,7 +62,9 @@ export function ref(value?: unknown) {
declare const ShallowRefMarker: unique symbol
export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }
export type ShallowRef<T = any, S = T> = Ref<T, S> & {
[ShallowRefMarker]?: true
}
/**
* Shallow version of {@link ref()}.
@ -182,15 +184,18 @@ class RefImpl<T = any> {
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
*/
export function triggerRef(ref: Ref): void {
if (__DEV__) {
;(ref as unknown as RefImpl).dep.trigger({
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: (ref as unknown as RefImpl)._value,
})
} else {
;(ref as unknown as RefImpl).dep.trigger()
// ref may be an instance of ObjectRefImpl
if ((ref as unknown as RefImpl).dep) {
if (__DEV__) {
;(ref as unknown as RefImpl).dep.trigger({
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: (ref as unknown as RefImpl)._value,
})
} else {
;(ref as unknown as RefImpl).dep.trigger()
}
}
}
@ -484,12 +489,12 @@ export type ShallowUnwrapRef<T> = {
[K in keyof T]: DistributeRef<T[K]>
}
type DistributeRef<T> = T extends Ref<infer V> ? V : T
type DistributeRef<T> = T extends Ref<infer V, unknown> ? V : T
export type UnwrapRef<T> =
T extends ShallowRef<infer V>
T extends ShallowRef<infer V, unknown>
? V
: T extends Ref<infer V>
: T extends Ref<infer V, unknown>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>

View File

@ -1930,7 +1930,7 @@ describe('api: watch', () => {
warn.mockRestore()
})
it('should be executed correctly', () => {
test('should be executed correctly', () => {
const v = ref(1)
let foo = ''
@ -1957,4 +1957,30 @@ describe('api: watch', () => {
v.value++
expect(foo).toBe('12')
})
// 12045
test('sync watcher should not break pre watchers', async () => {
const count1 = ref(0)
const count2 = ref(0)
watch(
count1,
() => {
count2.value++
},
{ flush: 'sync' },
)
const spy1 = vi.fn()
watch([count1, count2], spy1)
const spy2 = vi.fn()
watch(count1, spy2)
count1.value++
await nextTick()
expect(spy1).toHaveBeenCalled()
expect(spy2).toHaveBeenCalled()
})
})

View File

@ -333,6 +333,30 @@ describe('component props', () => {
})
})
//#12011
test('replace camelize with hyphenate to handle props key', () => {
const Comp = {
props: {
hasB4BProp: { type: Boolean, required: true },
},
setup() {
return () => null
},
}
render(
h('div', {}, [
h(Comp, {
'has-b-4-b-prop': true,
}),
h(Comp, {
'has-b4-b-prop': true,
}),
]),
nodeOps.createElement('div'),
)
expect(`Missing required prop: "hasB4BProp"`).not.toHaveBeenWarned()
})
test('warn props mutation', () => {
let instance: ComponentInternalInstance
let setupProps: any

View File

@ -32,6 +32,12 @@ describe('renderSlot', () => {
expect(vnode.key).toBe('foo')
})
it('should allow symbol values for slot prop key', () => {
const key = Symbol()
const vnode = renderSlot({ default: () => [h('div')] }, 'default', { key })
expect(vnode.key).toBe('_default')
})
it('should render slot fallback', () => {
const vnode = renderSlot({}, 'default', { key: 'foo' }, () => ['fallback'])
expect(vnode.children).toEqual(['fallback'])

View File

@ -8,6 +8,7 @@ import {
defineAsyncComponent,
defineComponent,
h,
onServerPrefetch,
useId,
} from 'vue'
import { renderToString } from '@vue/server-renderer'
@ -145,6 +146,40 @@ describe('useId', () => {
expect(await getOutput(() => factory(16, 0))).toBe(expected)
})
test('components with serverPrefetch', async () => {
const factory = (): ReturnType<TestCaseFactory> => {
const SPOne = defineComponent({
setup() {
onServerPrefetch(() => {})
return () => h(BasicComponentWithUseId)
},
})
const SPTwo = defineComponent({
render() {
return h(BasicComponentWithUseId)
},
})
const app = createApp({
setup() {
const id1 = useId()
const id2 = useId()
return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)]
},
})
return [app, []]
}
const expected =
'v-0 v-1 ' + // root
'v-0-0 v-0-1 ' + // inside first async subtree
'v-2 v-3' // inside second async subtree
// assert different async resolution order does not affect id stable-ness
expect(await getOutput(() => factory())).toBe(expected)
expect(await getOutput(() => factory())).toBe(expected)
})
test('async setup()', async () => {
const factory = (
delay1: number,

View File

@ -153,10 +153,10 @@ describe('useModel', () => {
const compRender = vi.fn()
const Comp = defineComponent({
props: ['fooBar'],
emits: ['update:fooBar'],
props: ['foo-bar'],
emits: ['update:foo-bar'],
setup(props) {
foo = useModel(props, 'fooBar')
foo = useModel(props, 'foo-bar')
return () => {
compRender()
return foo.value
@ -192,10 +192,10 @@ describe('useModel', () => {
const compRender = vi.fn()
const Comp = defineComponent({
props: ['fooBar'],
emits: ['update:fooBar'],
props: ['foo-bar'],
emits: ['update:foo-bar'],
setup(props) {
foo = useModel(props, 'fooBar')
foo = useModel(props, 'foo-bar')
return () => {
compRender()
return foo.value

View File

@ -1613,6 +1613,36 @@ describe('SSR hydration', () => {
`)
})
test('Suspense + transition appear', async () => {
const { vnode, container } = mountWithHydration(
`<template><div>foo</div></template>`,
() =>
h(Suspense, {}, () =>
h(
Transition,
{ appear: true },
{
default: () => h('div', 'foo'),
},
),
),
)
expect(vnode.el).toBe(container.firstChild)
// wait for hydration to finish
await new Promise(r => setTimeout(r))
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="v-enter-from v-enter-active"
>
foo
</div>
`)
await nextTick()
expect(vnode.el).toBe(container.firstChild)
})
// #10607
test('update component stable slot (prod + optimized mode)', async () => {
__DEV__ = false

View File

@ -6,6 +6,7 @@
import {
Fragment,
type FunctionalComponent,
Teleport,
createBlock,
createCommentVNode,
createElementBlock,
@ -391,6 +392,26 @@ describe('attribute fallthrough', () => {
expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
})
it('should warn when fallthrough fails on teleport root node', () => {
const Parent = {
render() {
return h(Child, { class: 'parent' })
},
}
const root = document.createElement('div')
const Child = defineComponent({
render() {
return h(Teleport, { to: root }, h('div'))
},
})
document.body.appendChild(root)
render(h(Parent), root)
expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
})
it('should dedupe same listeners when $attrs is used during render', () => {
const click = vi.fn()
const count = ref(0)

View File

@ -65,6 +65,15 @@ test('array children -> text children', () => {
expect(inner(root)).toBe('<div>hello</div>')
})
test('plain object child', () => {
const root = nodeOps.createElement('div')
const foo = { foo: '1' }
// @ts-expect-error
render(h('div', null, [foo]), root)
expect('Invalid VNode type').not.toHaveBeenWarned()
expect(inner(root)).toBe('<div>[object Object]</div>')
})
describe('renderer: keyed children', () => {
let root: TestElement
let elm: TestElement

View File

@ -217,6 +217,7 @@ describe('api: template refs', () => {
}
render(h(Comp), root)
expect(state.refKey).toBe(root.children[0])
expect('Template ref "refKey" used on a non-ref value').toHaveBeenWarned()
})
test('multiple root refs', () => {

View File

@ -441,6 +441,29 @@ describe('scheduler', () => {
await nextTick()
expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
})
test('jobs added during post flush are ordered correctly', async () => {
const calls: string[] = []
const job1: SchedulerJob = () => {
calls.push('job1')
}
job1.id = 1
const job2: SchedulerJob = () => {
calls.push('job2')
}
job2.id = 2
queuePostFlushCb(() => {
queueJob(job2)
queueJob(job1)
})
await nextTick()
expect(calls).toEqual(['job1', 'job2'])
})
})
test('sort job based on id', async () => {
@ -517,6 +540,45 @@ describe('scheduler', () => {
await nextTick()
})
test('jobs can be re-queued after an error', async () => {
const err = new Error('test')
let shouldThrow = true
const job1: SchedulerJob = vi.fn(() => {
if (shouldThrow) {
shouldThrow = false
throw err
}
})
job1.id = 1
const job2: SchedulerJob = vi.fn()
job2.id = 2
queueJob(job1)
queueJob(job2)
try {
await nextTick()
} catch (e: any) {
expect(e).toBe(err)
}
expect(
`Unhandled error during execution of scheduler flush`,
).toHaveBeenWarned()
expect(job1).toHaveBeenCalledTimes(1)
expect(job2).toHaveBeenCalledTimes(0)
queueJob(job1)
queueJob(job2)
await nextTick()
expect(job1).toHaveBeenCalledTimes(2)
expect(job2).toHaveBeenCalledTimes(1)
})
test('should prevent self-triggering jobs by default', async () => {
let count = 0
const job = () => {
@ -558,6 +620,113 @@ describe('scheduler', () => {
expect(count).toBe(5)
})
test('recursive jobs can only be queued once non-recursively', async () => {
const job: SchedulerJob = vi.fn()
job.id = 1
job.flags = SchedulerJobFlags.ALLOW_RECURSE
queueJob(job)
queueJob(job)
await nextTick()
expect(job).toHaveBeenCalledTimes(1)
})
test('recursive jobs can only be queued once recursively', async () => {
let recurse = true
const job: SchedulerJob = vi.fn(() => {
if (recurse) {
queueJob(job)
queueJob(job)
recurse = false
}
})
job.id = 1
job.flags = SchedulerJobFlags.ALLOW_RECURSE
queueJob(job)
await nextTick()
expect(job).toHaveBeenCalledTimes(2)
})
test(`recursive jobs can't be re-queued by other jobs`, async () => {
let recurse = true
const job1: SchedulerJob = () => {
if (recurse) {
// job2 is already queued, so this shouldn't do anything
queueJob(job2)
recurse = false
}
}
job1.id = 1
const job2: SchedulerJob = vi.fn(() => {
if (recurse) {
queueJob(job1)
queueJob(job2)
}
})
job2.id = 2
job2.flags = SchedulerJobFlags.ALLOW_RECURSE
queueJob(job2)
await nextTick()
expect(job2).toHaveBeenCalledTimes(2)
})
test('jobs are de-duplicated correctly when calling flushPreFlushCbs', async () => {
let recurse = true
const job1: SchedulerJob = vi.fn(() => {
queueJob(job3)
queueJob(job3)
flushPreFlushCbs()
})
job1.id = 1
job1.flags = SchedulerJobFlags.PRE
const job2: SchedulerJob = vi.fn(() => {
if (recurse) {
// job2 does not allow recurse, so this shouldn't do anything
queueJob(job2)
// job3 is already queued, so this shouldn't do anything
queueJob(job3)
recurse = false
}
})
job2.id = 2
job2.flags = SchedulerJobFlags.PRE
const job3: SchedulerJob = vi.fn(() => {
if (recurse) {
queueJob(job2)
queueJob(job3)
// The jobs are already queued, so these should have no effect
queueJob(job2)
queueJob(job3)
}
})
job3.id = 3
job3.flags = SchedulerJobFlags.ALLOW_RECURSE | SchedulerJobFlags.PRE
queueJob(job1)
await nextTick()
expect(job1).toHaveBeenCalledTimes(1)
expect(job2).toHaveBeenCalledTimes(1)
expect(job3).toHaveBeenCalledTimes(2)
})
// #1947 flushPostFlushCbs should handle nested calls
// e.g. app.mount inside app.mount
test('flushPostFlushCbs', async () => {
@ -612,6 +781,37 @@ describe('scheduler', () => {
expect(spy).toHaveBeenCalledTimes(1)
})
test('flushPreFlushCbs inside a post job', async () => {
const calls: string[] = []
const callsAfterFlush: string[] = []
const job1: SchedulerJob = () => {
calls.push('job1')
}
job1.id = 1
job1.flags! |= SchedulerJobFlags.PRE
const job2: SchedulerJob = () => {
calls.push('job2')
}
job2.id = 2
job2.flags! |= SchedulerJobFlags.PRE
queuePostFlushCb(() => {
queueJob(job2)
queueJob(job1)
// e.g. nested app.mount() call
flushPreFlushCbs()
callsAfterFlush.push(...calls)
})
await nextTick()
expect(callsAfterFlush).toEqual(['job1', 'job2'])
expect(calls).toEqual(['job1', 'job2'])
})
it('nextTick should return promise', async () => {
const fn = vi.fn(() => {
return 1

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-core",
"version": "3.5.6",
"version": "3.5.12",
"description": "@vue/runtime-core",
"main": "index.js",
"module": "dist/runtime-core.esm-bundler.js",

View File

@ -27,7 +27,7 @@ import type {
EmitsToProps,
TypeEmitsToOptions,
} from './componentEmits'
import { extend, isFunction } from '@vue/shared'
import { type IsKeyValues, extend, isFunction } from '@vue/shared'
import type { VNodeProps } from './vnode'
import type {
ComponentPublicInstanceConstructor,
@ -79,7 +79,7 @@ export type DefineComponent<
Mixin,
Extends,
E,
PP & Props,
PP,
Defaults,
MakeDefaultsOptional,
{},
@ -208,13 +208,13 @@ export function defineComponent<
ResolvedEmits extends EmitsOptions = {} extends RuntimeEmitsOptions
? TypeEmitsToOptions<TypeEmits>
: RuntimeEmitsOptions,
InferredProps = unknown extends TypeProps
? string extends RuntimePropsKeys
InferredProps = IsKeyValues<TypeProps> extends true
? TypeProps
: string extends RuntimePropsKeys
? ComponentObjectPropsOptions extends RuntimePropsOptions
? {}
: ExtractPropTypes<RuntimePropsOptions>
: { [key in RuntimePropsKeys]?: any }
: TypeProps,
: { [key in RuntimePropsKeys]?: any },
TypeRefs extends Record<string, unknown> = {},
TypeEl extends Element = any,
>(
@ -265,7 +265,7 @@ export function defineComponent<
Mixin,
Extends,
ResolvedEmits,
RuntimeEmitsKeys,
{},
{},
false,
InjectOptions,

View File

@ -331,21 +331,23 @@ type PropsWithDefaults<
T,
Defaults extends InferDefaults<T>,
BKeys extends keyof T,
> = Readonly<MappedOmit<T, keyof Defaults>> & {
readonly [K in keyof Defaults as K extends keyof T
? K
: never]-?: K extends keyof T
? Defaults[K] extends undefined
? IfAny<Defaults[K], NotUndefined<T[K]>, T[K]>
: NotUndefined<T[K]>
: never
} & {
readonly [K in BKeys]-?: K extends keyof Defaults
? Defaults[K] extends undefined
? boolean | undefined
: boolean
: boolean
}
> = T extends unknown
? Readonly<MappedOmit<T, keyof Defaults>> & {
readonly [K in keyof Defaults as K extends keyof T
? K
: never]-?: K extends keyof T
? Defaults[K] extends undefined
? IfAny<Defaults[K], NotUndefined<T[K]>, T[K]>
: NotUndefined<T[K]>
: never
} & {
readonly [K in BKeys]-?: K extends keyof Defaults
? Defaults[K] extends undefined
? boolean | undefined
: boolean
: boolean
}
: never
/**
* Vue `<script setup>` compiler macro for providing props default values when

View File

@ -170,15 +170,14 @@ function doWatch(
if (__DEV__) baseWatchOptions.onWarn = warn
// immediate watcher or watchEffect
const runsImmediately = (cb && immediate) || (!cb && flush !== 'post')
let ssrCleanup: (() => void)[] | undefined
if (__SSR__ && isInSSRComponentSetup) {
if (flush === 'sync') {
const ctx = useSSRContext()!
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
} else if (!cb || immediate) {
// immediately watch or watchEffect
baseWatchOptions.once = true
} else {
} else if (!runsImmediately) {
const watchStopHandle = () => {}
watchStopHandle.stop = NOOP
watchStopHandle.resume = NOOP
@ -226,7 +225,14 @@ function doWatch(
const watchHandle = baseWatch(source, cb, baseWatchOptions)
if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle)
if (__SSR__ && isInSSRComponentSetup) {
if (ssrCleanup) {
ssrCleanup.push(watchHandle)
} else if (runsImmediately) {
watchHandle()
}
}
return watchHandle
}

View File

@ -856,11 +856,10 @@ function setupStatefulComponent(
// 2. call setup()
const { setup } = Component
if (setup) {
pauseTracking()
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
const reset = setCurrentInstance(instance)
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
@ -870,12 +869,16 @@ function setupStatefulComponent(
setupContext,
],
)
const isAsyncSetup = isPromise(setupResult)
resetTracking()
reset()
if (isPromise(setupResult)) {
// async setup, mark as async boundary for useId()
if (!isAsyncWrapper(instance)) markAsyncBoundary(instance)
if ((isAsyncSetup || instance.sp) && !isAsyncWrapper(instance)) {
// async setup / serverPrefetch, mark as async boundary for useId()
markAsyncBoundary(instance)
}
if (isAsyncSetup) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
@ -1003,7 +1006,7 @@ export function finishComponentSetup(
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
Component.template ||
resolveMergedOptions(instance).template
(__FEATURE_OPTIONS_API__ && resolveMergedOptions(instance).template)
if (template) {
if (__DEV__) {
startMeasure(instance, `compile`)

View File

@ -66,11 +66,23 @@ export type TypeEmitsToOptions<T extends ComponentTypeEmits> = {
: {})
type ParametersToFns<T extends any[]> = {
[K in T[0]]: K extends `${infer C}`
? (...args: T extends [C, ...infer Args] ? Args : never) => any
[K in T[0]]: IsStringLiteral<K> extends true
? (
...args: T extends [e: infer E, ...args: infer P]
? K extends E
? P
: never
: never
) => any
: never
}
type IsStringLiteral<T> = T extends string
? string extends T
? false
: true
: false
export type ShortEmitsToObject<E> =
E extends Record<string, any[]>
? {

View File

@ -125,7 +125,9 @@ type InferPropType<T, NullAsAny = true> = [T] extends [null]
: InferPropType<U, false>
: [T] extends [Prop<infer V, infer D>]
? unknown extends V
? IfAny<V, V, D>
? keyof V extends never
? IfAny<V, V, D>
: V
: V
: T
@ -652,6 +654,7 @@ function validateProps(
) {
const resolvedValues = toRaw(props)
const options = instance.propsOptions[0]
const camelizePropsKey = Object.keys(rawProps).map(key => camelize(key))
for (const key in options) {
let opt = options[key]
if (opt == null) continue
@ -660,7 +663,7 @@ function validateProps(
resolvedValues[key],
opt,
__DEV__ ? shallowReadonly(resolvedValues) : resolvedValues,
!hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key)),
!camelizePropsKey.includes(key),
)
}
}

View File

@ -190,7 +190,7 @@ export function renderComponentRoot(
`Extraneous non-props attributes (` +
`${extraAttrs.join(', ')}) ` +
`were passed to component but could not be automatically inherited ` +
`because component renders fragment or text root nodes.`,
`because component renders fragment or text or teleport root nodes.`,
)
}
if (eventAttrs.length) {

View File

@ -147,7 +147,7 @@ export const TeleportImpl = {
}
if (!disabled) {
mount(target, targetAnchor)
updateCssVars(n2)
updateCssVars(n2, false)
}
} else if (__DEV__ && !disabled) {
warn(
@ -160,7 +160,7 @@ export const TeleportImpl = {
if (disabled) {
mount(container, mainAnchor)
updateCssVars(n2)
updateCssVars(n2, true)
}
if (isTeleportDeferred(n2.props)) {
@ -267,7 +267,7 @@ export const TeleportImpl = {
)
}
}
updateCssVars(n2)
updateCssVars(n2, disabled)
}
},
@ -407,13 +407,14 @@ function hydrateTeleport(
vnode.props,
querySelector,
))
const disabled = isTeleportDisabled(vnode.props)
if (target) {
// if multiple teleports rendered to the same target element, we need to
// pick up from where the last teleport finished instead of the first node
const targetNode =
(target as TeleportTargetElement)._lpa || target.firstChild
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (isTeleportDisabled(vnode.props)) {
if (disabled) {
hydrateDisabledTeleport(
node,
vnode,
@ -460,8 +461,8 @@ function hydrateTeleport(
)
}
}
updateCssVars(vnode)
} else if (isTeleportDisabled(vnode.props)) {
updateCssVars(vnode, disabled)
} else if (disabled) {
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
hydrateDisabledTeleport(node, vnode, node, nextSibling(node))
}
@ -480,13 +481,20 @@ export const Teleport = TeleportImpl as unknown as {
}
}
function updateCssVars(vnode: VNode) {
function updateCssVars(vnode: VNode, isDisabled: boolean) {
// presence of .ut method indicates owner component uses css vars.
// code path here can assume browser environment.
const ctx = vnode.ctx
if (ctx && ctx.ut) {
let node = vnode.targetStart
while (node && node !== vnode.targetAnchor) {
let node, anchor
if (isDisabled) {
node = vnode.el
anchor = vnode.anchor
} else {
node = vnode.targetStart
anchor = vnode.targetAnchor
}
while (node && node !== anchor) {
if (node.nodeType === 1) node.setAttribute('data-v-owner', ctx.uid)
node = node.nextSibling
}

View File

@ -14,7 +14,7 @@ import {
isVNode,
openBlock,
} from '../vnode'
import { PatchFlags, SlotFlags } from '@vue/shared'
import { PatchFlags, SlotFlags, isSymbol } from '@vue/shared'
import { warn } from '../warning'
import { isAsyncWrapper } from '../apiAsyncComponent'
@ -72,15 +72,16 @@ export function renderSlot(
}
openBlock()
const validSlotContent = slot && ensureValidVNode(slot(props))
const slotKey =
props.key ||
// slot content array of a dynamic conditional slot may have a branch
// key attached in the `createSlots` helper, respect that
(validSlotContent && (validSlotContent as any).key)
const rendered = createBlock(
Fragment,
{
key:
(props.key ||
// slot content array of a dynamic conditional slot may have a branch
// key attached in the `createSlots` helper, respect that
(validSlotContent && (validSlotContent as any).key) ||
`_${name}`) +
(slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) +
// #7256 force differentiate fallback content from actual content
(!validSlotContent && fallback ? '_fb' : ''),
},

View File

@ -4,7 +4,7 @@ import {
} from '../component'
import { warn } from '../warning'
export function useId(): string | undefined {
export function useId(): string {
const i = getCurrentInstance()
if (i) {
return (i.appContext.config.idPrefix || 'v') + '-' + i.ids[0] + i.ids[1]++
@ -14,6 +14,7 @@ export function useId(): string | undefined {
`instance to be associated with.`,
)
}
return ''
}
/**

View File

@ -28,14 +28,14 @@ export function useModel(
return ref() as any
}
if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {
const camelizedName = camelize(name)
if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[camelizedName]) {
warn(`useModel() called with prop "${name}" which is not declared.`)
return ref() as any
}
const camelizedName = camelize(name)
const hyphenatedName = hyphenate(name)
const modifiers = getModelModifiers(props, name)
const modifiers = getModelModifiers(props, camelizedName)
const res = customRef((track, trigger) => {
let localValue: any
@ -43,7 +43,7 @@ export function useModel(
let prevEmittedValue: any
watchSyncEffect(() => {
const propValue = props[name]
const propValue = props[camelizedName]
if (hasChanged(localValue, propValue)) {
localValue = propValue
trigger()

View File

@ -1,8 +1,8 @@
import {
Comment,
Fragment,
Static,
Text,
Comment as VComment,
type VNode,
type VNodeHook,
createTextVNode,
@ -195,7 +195,7 @@ export function createHydrationFunctions(
nextNode = nextSibling(node)
}
break
case Comment:
case VComment:
if (isTemplateNode(node)) {
nextNode = nextSibling(node)
// wrapped <transition appear>
@ -385,7 +385,10 @@ export function createHydrationFunctions(
let needCallTransitionHooks = false
if (isTemplateNode(el)) {
needCallTransitionHooks =
needTransition(parentSuspense, transition) &&
needTransition(
null, // no need check parentSuspense in hydration
transition,
) &&
parentComponent &&
parentComponent.vnode.props &&
parentComponent.vnode.props.appear

View File

@ -1,6 +1,13 @@
import { isString } from '@vue/shared'
import { getGlobalThis, isString } from '@vue/shared'
import { DOMNodeTypes, isComment } from './hydration'
// Polyfills for Safari support
// see https://caniuse.com/requestidlecallback
const requestIdleCallback: Window['requestIdleCallback'] =
getGlobalThis().requestIdleCallback || (cb => setTimeout(cb, 1))
const cancelIdleCallback: Window['cancelIdleCallback'] =
getGlobalThis().cancelIdleCallback || (id => clearTimeout(id))
/**
* A lazy hydration strategy for async components.
* @param hydrate - call this to perform the actual hydration.
@ -48,6 +55,7 @@ export const hydrateOnVisible: HydrationStrategyFactory<
}
}, opts)
forEach(el => {
if (!(el instanceof Element)) return
if (elementIsVisibleInViewport(el)) {
hydrate()
ob.disconnect()

View File

@ -69,8 +69,17 @@ export function setRef(
setupState === EMPTY_OBJ
? () => false
: (key: string) => {
if (__DEV__ && knownTemplateRefs.has(rawSetupState[key] as any)) {
return false
if (__DEV__) {
if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) {
warn(
`Template ref "${key}" used on a non-ref value. ` +
`It will not work in the production build.`,
)
}
if (knownTemplateRefs.has(rawSetupState[key] as any)) {
return false
}
}
return hasOwn(rawSetupState, key)
}

View File

@ -40,11 +40,8 @@ export interface SchedulerJob extends Function {
export type SchedulerJobs = SchedulerJob | SchedulerJob[]
let isFlushing = false
let isFlushPending = false
const queue: SchedulerJob[] = []
let flushIndex = 0
let flushIndex = -1
const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
@ -74,7 +71,7 @@ export function nextTick<T = void, R = void>(
// watcher should be inserted immediately before the update job. This allows
// watchers to be skipped if the component is unmounted by the parent update.
function findInsertionIndex(id: number) {
let start = isFlushing ? flushIndex + 1 : 0
let start = flushIndex + 1
let end = queue.length
while (start < end) {
@ -115,8 +112,7 @@ export function queueJob(job: SchedulerJob): void {
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
if (!currentFlushPromise) {
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
@ -141,8 +137,8 @@ export function queuePostFlushCb(cb: SchedulerJobs): void {
export function flushPreFlushCbs(
instance?: ComponentInternalInstance,
seen?: CountMap,
// if currently flushing, skip the current job itself
i: number = isFlushing ? flushIndex + 1 : 0,
// skip the current job
i: number = flushIndex + 1,
): void {
if (__DEV__) {
seen = seen || new Map()
@ -162,7 +158,9 @@ export function flushPreFlushCbs(
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
cb()
cb.flags! &= ~SchedulerJobFlags.QUEUED
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
}
}
}
@ -209,8 +207,6 @@ const getId = (job: SchedulerJob): number =>
job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}
@ -239,7 +235,9 @@ function flushJobs(seen?: CountMap) {
job.i,
job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
)
job.flags! &= ~SchedulerJobFlags.QUEUED
if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
job.flags! &= ~SchedulerJobFlags.QUEUED
}
}
}
} finally {
@ -251,15 +249,13 @@ function flushJobs(seen?: CountMap) {
}
}
flushIndex = 0
flushIndex = -1
queue.length = 0
flushPostFlushCbs(seen)
isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
// If new jobs have been added to either queue, keep flushing
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}

View File

@ -793,7 +793,7 @@ export function normalizeVNode(child: VNodeChild): VNode {
// #3666, avoid reference pollution when reusing vnode
child.slice(),
)
} else if (typeof child === 'object') {
} else if (isVNode(child)) {
// already vnode, this should be the most common since compiled templates
// always produce all-vnode children arrays
return cloneIfMounted(child)

View File

@ -223,6 +223,21 @@ describe('defineCustomElement', () => {
expect(e.getAttribute('baz-qux')).toBe('four')
})
test('props via hyphen property', async () => {
const Comp = defineCustomElement({
props: {
fooBar: Boolean,
},
render() {
return 'Comp'
},
})
customElements.define('my-el-comp', Comp)
render(h('my-el-comp', { 'foo-bar': true }), container)
const el = container.children[0]
expect((el as any).outerHTML).toBe('<my-el-comp foo-bar=""></my-el-comp>')
})
test('attribute -> prop type casting', async () => {
const E = defineCustomElement({
props: {
@ -1371,4 +1386,39 @@ describe('defineCustomElement', () => {
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(`false,boolean`)
})
test('hyphenated attr removal', async () => {
const E = defineCustomElement({
props: {
fooBar: {
type: Boolean,
},
},
render() {
return this.fooBar
},
})
customElements.define('el-hyphenated-attr-removal', E)
const toggle = ref(true)
const Comp = {
render() {
return h('el-hyphenated-attr-removal', {
'foo-bar': toggle.value ? '' : null,
})
},
}
render(h(Comp), container)
const el = container.children[0]
expect(el.hasAttribute('foo-bar')).toBe(true)
expect((el as any).outerHTML).toBe(
`<el-hyphenated-attr-removal foo-bar=""></el-hyphenated-attr-removal>`,
)
toggle.value = false
await nextTick()
expect(el.hasAttribute('foo-bar')).toBe(false)
expect((el as any).outerHTML).toBe(
`<el-hyphenated-attr-removal></el-hyphenated-attr-removal>`,
)
})
})

View File

@ -350,6 +350,7 @@ describe('useCssVars', () => {
expect(() => render(h(App), root)).not.toThrow(TypeError)
await nextTick()
expect(target.children.length).toBe(0)
expect(root.children[0].outerHTML.includes('data-v-owner')).toBe(true)
})
test('with string style', async () => {

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-dom",
"version": "3.5.6",
"version": "3.5.12",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",

View File

@ -344,7 +344,7 @@ function whenTransitionEnds(
}
}
if (explicitTimeout) {
if (explicitTimeout != null) {
return setTimeout(resolveIfNotStale, explicitTimeout)
}

View File

@ -173,6 +173,7 @@ function setChecked(
} else if (isSet(value)) {
checked = value.has(vnode.props!.value)
} else {
if (value === oldValue) return
checked = looseEqual(value, getCheckboxValue(el, true))
}
@ -225,20 +226,20 @@ export const vModelSelect: ModelDirective<HTMLSelectElement, 'number'> = {
},
// set value in mounted & updated because <select> relies on its children
// <option>s.
mounted(el, { value, modifiers: { number } }) {
setSelected(el, value, number)
mounted(el, { value }) {
setSelected(el, value)
},
beforeUpdate(el, _binding, vnode) {
el[assignKey] = getModelAssigner(vnode)
},
updated(el, { value, modifiers: { number } }) {
updated(el, { value }) {
if (!el._assigning) {
setSelected(el, value, number)
setSelected(el, value)
}
},
}
function setSelected(el: HTMLSelectElement, value: any, number: boolean) {
function setSelected(el: HTMLSelectElement, value: any) {
const isMultiple = el.multiple
const isArrayValue = isArray(value)
if (isMultiple && !isArrayValue && !isSet(value)) {

View File

@ -8,6 +8,7 @@ export function patchDOMProp(
key: string,
value: any,
parentComponent: any,
attrName?: string,
): void {
// __UNSAFE__
// Reason: potentially setting innerHTML.
@ -106,5 +107,5 @@ export function patchDOMProp(
)
}
}
needRemove && el.removeAttribute(key)
needRemove && el.removeAttribute(attrName || key)
}

View File

@ -3,7 +3,13 @@ import { patchStyle } from './modules/style'
import { patchAttr } from './modules/attrs'
import { patchDOMProp } from './modules/props'
import { patchEvent } from './modules/events'
import { isFunction, isModelListener, isOn, isString } from '@vue/shared'
import {
camelize,
isFunction,
isModelListener,
isOn,
isString,
} from '@vue/shared'
import type { RendererOptions } from '@vue/runtime-core'
import type { VueElement } from './apiCustomElement'
@ -51,6 +57,12 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
) {
patchAttr(el, key, nextValue, isSVG, parentComponent, key !== 'value')
}
} else if (
// #11081 force set props for possible async custom element
(el as VueElement)._isVueCE &&
(/[A-Z]/.test(key) || !isString(nextValue))
) {
patchDOMProp(el, camelize(key), nextValue, parentComponent, key)
} else {
// special case for <input v-model type="checkbox"> with
// :true-value & :false-value
@ -128,14 +140,5 @@ function shouldSetAsProp(
return false
}
if (key in el) {
return true
}
// #11081 force set props for possible async custom element
if ((el as VueElement)._isVueCE && (/[A-Z]/.test(key) || !isString(value))) {
return true
}
return false
return key in el
}

View File

@ -1,4 +1,12 @@
import { createSSRApp, defineComponent, h, ref, watch } from 'vue'
import {
createSSRApp,
defineComponent,
h,
nextTick,
ref,
watch,
watchEffect,
} from 'vue'
import { type SSRContext, renderToString } from '../src'
describe('ssr: watch', () => {
@ -27,4 +35,168 @@ describe('ssr: watch', () => {
expect(html).toMatch('hello world')
})
test('should work with flush: sync and immediate: true', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watch(
text,
() => {
msg = text.value
},
{ flush: 'sync', immediate: true },
)
expect(msg).toBe('start')
text.value = 'changed'
expect(msg).toBe('changed')
text.value = 'changed again'
expect(msg).toBe('changed again')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles!.length).toBe(1)
expect(html).toMatch('changed again')
await nextTick()
expect(msg).toBe('changed again')
})
test('should run once with immediate: true', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watch(
text,
() => {
msg = String(text.value)
},
{ immediate: true },
)
text.value = 'changed'
expect(msg).toBe('start')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles).toBeUndefined()
expect(html).toMatch('start')
await nextTick()
expect(msg).toBe('start')
})
test('should run once with immediate: true and flush: post', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watch(
text,
() => {
msg = String(text.value)
},
{ immediate: true, flush: 'post' },
)
text.value = 'changed'
expect(msg).toBe('start')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles).toBeUndefined()
expect(html).toMatch('start')
await nextTick()
expect(msg).toBe('start')
})
})
describe('ssr: watchEffect', () => {
test('should run with flush: sync', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watchEffect(
() => {
msg = text.value
},
{ flush: 'sync' },
)
expect(msg).toBe('start')
text.value = 'changed'
expect(msg).toBe('changed')
text.value = 'changed again'
expect(msg).toBe('changed again')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles!.length).toBe(1)
expect(html).toMatch('changed again')
await nextTick()
expect(msg).toBe('changed again')
})
test('should run once with default flush (pre)', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watchEffect(() => {
msg = text.value
})
text.value = 'changed'
expect(msg).toBe('start')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles).toBeUndefined()
expect(html).toMatch('start')
await nextTick()
expect(msg).toBe('start')
})
test('should not run for flush: post', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watchEffect(
() => {
msg = text.value
},
{ flush: 'post' },
)
text.value = 'changed'
expect(msg).toBe('unchanged')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles).toBeUndefined()
expect(html).toMatch('unchanged')
await nextTick()
expect(msg).toBe('unchanged')
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/server-renderer",
"version": "3.5.6",
"version": "3.5.12",
"description": "@vue/server-renderer",
"main": "index.js",
"module": "dist/server-renderer.esm-bundler.js",

View File

@ -1,4 +1,10 @@
import { normalizeClass, parseStringStyle } from '../src'
import {
normalizeClass,
normalizeProps,
normalizeStyle,
parseStringStyle,
stringifyStyle,
} from '../src'
describe('normalizeClass', () => {
test('handles undefined correctly', () => {
@ -15,6 +21,11 @@ describe('normalizeClass', () => {
)
})
test('handles string containing spaces correctly', () => {
expect(normalizeClass('foo1 ')).toEqual('foo1')
expect(normalizeClass(['foo ', ' baz '])).toEqual('foo baz')
})
test('handles empty array correctly', () => {
expect(normalizeClass([])).toEqual('')
})
@ -92,3 +103,132 @@ describe('normalizeClass', () => {
`)
})
})
describe('normalizeStyle', () => {
test('handles string correctly', () => {
expect(normalizeStyle('foo')).toEqual('foo')
})
test('handles array correctly', () => {
const style: any = normalizeStyle([
`border: 1px solid transparent;
background: linear-gradient(white, white) padding-box,
repeating-linear-gradient(
-45deg,
#ccc 0,
#ccc 0.5em,
white 0,
white 0.75em
);`,
])
expect(style.border).toEqual('1px solid transparent')
expect(style.background).toEqual(`linear-gradient(white, white) padding-box,
repeating-linear-gradient(
-45deg,
#ccc 0,
#ccc 0.5em,
white 0,
white 0.75em
)`)
})
test('handles object correctly', () => {
const styleObj = {
border: '1px solid transparent',
background: `linear-gradient(white, white) padding-box,
repeating-linear-gradient(
-45deg,
#ccc 0,
#ccc 0.5em,
white 0,
white 0.75em
)`,
}
const style: any = normalizeStyle(styleObj)
expect(style.border).toEqual(styleObj.border)
expect(style.background).toEqual(styleObj.background)
})
})
describe('stringifyStyle', () => {
test('should return empty string for undefined or string styles', () => {
expect(stringifyStyle(undefined)).toBe('')
expect(stringifyStyle('')).toBe('')
expect(stringifyStyle('color: blue;')).toBe('')
})
test('should return valid CSS string for normalized style object', () => {
const style = {
color: 'blue',
fontSize: '14px',
backgroundColor: 'white',
opacity: 0.8,
margin: 0,
'--custom-color': 'red',
}
expect(stringifyStyle(style)).toBe(
'color:blue;font-size:14px;background-color:white;opacity:0.8;margin:0;--custom-color:red;',
)
})
test('should ignore non-string or non-number values in style object', () => {
const style: any = {
color: 'blue',
fontSize: '14px',
lineHeight: true,
padding: null,
margin: undefined,
}
const expected = 'color:blue;font-size:14px;'
expect(stringifyStyle(style)).toBe(expected)
})
})
describe('normalizeProps', () => {
test('should return null when props is null', () => {
const props = null
const result = normalizeProps(props)
expect(result).toBeNull()
})
test('should normalize class prop when it is an array', () => {
const props = {
class: ['class1', 'class2'],
}
const result = normalizeProps(props)
expect(result).toEqual({
class: 'class1 class2',
})
})
test('should normalize class prop when it is an object', () => {
const props = {
class: {
class1: true,
class2: false,
class3: true,
},
}
const result = normalizeProps(props)
expect(result).toEqual({
class: 'class1 class3',
})
})
test('should normalize style prop', () => {
const props = {
style: ['color: blue', 'font-size: 14px'],
}
const result = normalizeProps(props)
expect(result).toEqual({
style: {
color: 'blue',
'font-size': '14px',
},
})
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/shared",
"version": "3.5.6",
"version": "3.5.12",
"description": "internal utils shared across @vue packages",
"main": "index.js",
"module": "dist/shared.esm-bundler.js",

View File

@ -208,3 +208,12 @@ export function genPropsAccessExp(name: string): string {
? `__props.${name}`
: `__props[${JSON.stringify(name)}]`
}
export function genCacheKey(source: string, options: any): string {
return (
source +
JSON.stringify(options, (_, val) =>
typeof val === 'function' ? val.toString() : val,
)
)
}

View File

@ -13,6 +13,12 @@ export type LooseRequired<T> = { [P in keyof (T & Required<T>)]: T[P] }
// https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N
export type IsKeyValues<T, K = string> = IfAny<
T,
false,
T extends object ? (keyof T extends K ? true : false) : false
>
/**
* Utility for extracting the parameters from a function overload (for typed emits)
* https://github.com/microsoft/TypeScript/issues/32164#issuecomment-1146737709

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compat",
"version": "3.5.6",
"version": "3.5.12",
"description": "Vue 3 compatibility build for Vue 2",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",

View File

@ -12,7 +12,13 @@ import {
registerRuntimeCompiler,
warn,
} from '@vue/runtime-dom'
import { NOOP, extend, generateCodeFrame, isString } from '@vue/shared'
import {
NOOP,
extend,
genCacheKey,
generateCodeFrame,
isString,
} from '@vue/shared'
import type { InternalRenderFunction } from 'packages/runtime-core/src/component'
import * as runtimeDom from '@vue/runtime-dom'
import {
@ -35,7 +41,7 @@ function compileToFunction(
}
}
const key = template
const key = genCacheKey(template, options)
const cached = compileCache[key]
if (cached) {
return cached

View File

@ -25,7 +25,7 @@
- For use with bundlers like `webpack`, `rollup` and `parcel`.
- Leaves prod/dev branches with `process.env.NODE_ENV` guards (must be replaced by bundler)
- Does not ship minified builds (to be done together with the rest of the code after bundling)
- Imports dependencies (e.g. `@vue/runtime-core`, `@vue/runtime-compiler`)
- Imports dependencies (e.g. `@vue/runtime-core`, `@vue/compiler-core`)
- Imported dependencies are also `esm-bundler` builds and will in turn import their dependencies (e.g. `@vue/runtime-core` imports `@vue/reactivity`)
- This means you **can** install/import these deps individually without ending up with different instances of these dependencies, but you must make sure they all resolve to the same version.
- In-browser template compilation:

View File

@ -11,9 +11,12 @@
<script>
const rootMargin = location.search.match(/rootMargin=(\d+)/)?.[1] ?? 0
const isFragment = location.search.includes('?fragment')
const isVIf = location.search.includes('?v-if')
if (isFragment) {
document.getElementById('app').innerHTML =
`<!--[--><!--[--><span>one</span><!--]--><button>0</button><span>two</span><!--]-->`
} else if (isVIf) {
document.getElementById('app').innerHTML = `<!---->`
}
window.isHydrated = false
@ -24,6 +27,7 @@
ref,
onMounted,
hydrateOnVisible,
createCommentVNode,
} = Vue
const Comp = {
@ -39,7 +43,9 @@
{ onClick: () => count.value++ },
count.value,
)
if (isFragment) {
if (isVIf) {
return createCommentVNode('v-if', true)
} else if (isFragment) {
return [[h('span', 'one')], button, h('span', 'two')]
} else {
return button

View File

@ -65,6 +65,17 @@ describe('async component hydration strategies', () => {
await assertHydrationSuccess()
})
test('visible (root v-if) should not throw error', async () => {
const spy = vi.fn()
const currentPage = page()
currentPage.on('pageerror', spy)
await goToCase('visible', '?v-if')
await page().waitForFunction(() => window.isRootMounted)
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
expect(spy).toBeCalledTimes(0)
currentPage.off('pageerror', spy)
})
test('media query', async () => {
await goToCase('media')
await page().waitForFunction(() => window.isRootMounted)

View File

@ -0,0 +1,57 @@
import path from 'node:path'
import { setupPuppeteer } from './e2eUtils'
const { page, click, isChecked } = setupPuppeteer()
import { nextTick } from 'vue'
beforeEach(async () => {
await page().addScriptTag({
path: path.resolve(__dirname, '../../dist/vue.global.js'),
})
await page().setContent(`<div id="app"></div>`)
})
// #12144
test('checkbox click with v-model', async () => {
await page().evaluate(() => {
const { createApp } = (window as any).Vue
createApp({
template: `
<label>
<input
id="first"
type="checkbox"
v-model="first"/>
First
</label>
<br>
<label>
<input
id="second"
type="checkbox"
v-model="second"
@click="secondClick"/>
Second
</label>
`,
data() {
return {
first: true,
second: false,
}
},
methods: {
secondClick(this: any) {
this.first = false
},
},
}).mount('#app')
})
expect(await isChecked('#first')).toBe(true)
expect(await isChecked('#second')).toBe(false)
await click('#second')
await nextTick()
expect(await isChecked('#first')).toBe(false)
expect(await isChecked('#second')).toBe(true)
})

View File

@ -1,6 +1,6 @@
{
"name": "vue",
"version": "3.5.6",
"version": "3.5.12",
"description": "The progressive JavaScript framework for building modern web UI.",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",

View File

@ -13,9 +13,9 @@ import {
} from '@vue/runtime-dom'
import * as runtimeDom from '@vue/runtime-dom'
import {
EMPTY_OBJ,
NOOP,
extend,
genCacheKey,
generateCodeFrame,
isString,
} from '@vue/shared'
@ -25,19 +25,7 @@ if (__DEV__) {
initDev()
}
const compileCache = new WeakMap<
CompilerOptions,
Record<string, RenderFunction>
>()
function getCache(options?: CompilerOptions) {
let c = compileCache.get(options ?? EMPTY_OBJ)
if (!c) {
c = Object.create(null) as Record<string, RenderFunction>
compileCache.set(options ?? EMPTY_OBJ, c)
}
return c
}
const compileCache: Record<string, RenderFunction> = Object.create(null)
function compileToFunction(
template: string | HTMLElement,
@ -52,9 +40,8 @@ function compileToFunction(
}
}
const key = template
const cache = getCache(options)
const cached = cache[key]
const key = genCacheKey(template, options)
const cached = compileCache[key]
if (cached) {
return cached
}
@ -111,7 +98,7 @@ function compileToFunction(
// mark the function as runtime compiled
;(render as InternalRenderFunction)._rc = true
return (cache[key] = render)
return (compileCache[key] = render)
}
registerRuntimeCompiler(compileToFunction)

File diff suppressed because it is too large Load Diff

View File

@ -306,12 +306,6 @@ async function main() {
if (args.publish) {
await buildPackages()
await publishPackages(targetVersion)
} else {
console.log(
pico.yellow(
'\nPublish step skipped (will be done in GitHub actions on successful push)',
),
)
}
// push to GitHub
@ -322,6 +316,15 @@ async function main() {
await runIfNotDry('git', ['push'])
}
if (!args.publish) {
console.log(
pico.yellow(
'\nRelease will be done via GitHub Actions.\n' +
'Check status at https://github.com/vuejs/core/actions/workflows/release.yml',
),
)
}
if (isDryRun) {
console.log(`\nDry run finished - run git diff to see package changes.`)
}