Merge remote-tracking branch 'upstream/minor'

This commit is contained in:
三咲智子 Kevin Deng 2024-04-18 14:43:54 +08:00
commit b8c609f437
No known key found for this signature in database
87 changed files with 1798 additions and 782 deletions

View File

@ -26,13 +26,23 @@ module.exports = {
'no-restricted-syntax': [
'error',
banConstEnum,
// since we target ES2015 for baseline support, we need to forbid object
// rest spread usage in destructure as it compiles into a verbose helper.
'ObjectPattern > RestElement',
// tsc compiles assignment spread into Object.assign() calls, but esbuild
// still generates verbose helpers, so spread assignment is also prohiboted
'ObjectExpression > SpreadElement',
'AwaitExpression',
{
selector: 'ObjectPattern > RestElement',
message:
'Our output target is ES2016, and object rest spread results in ' +
'verbose helpers and should be avoided.',
},
{
selector: 'ObjectExpression > SpreadElement',
message:
'esbuild transpiles object spread into very verbose inline helpers.\n' +
'Please use the `extend` helper from @vue/shared instead.',
},
{
selector: 'AwaitExpression',
message:
'Our output target is ES2016, so async/await syntax should be avoided.',
},
],
'sort-imports': ['error', { ignoreDeclarationSort: true }],

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ TODOs.md
.eslintcache
dts-build/packages
*.tsbuildinfo
*.tgz

23
.vscode/launch.json vendored
View File

@ -5,24 +5,15 @@
"version": "0.2.0",
"configurations": [
{
"name": "Jest",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"stopOnEntry": false,
"args": ["${fileBasename}", "--runInBand", "--detectOpenHandles"],
"cwd": "${workspaceFolder}",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": ["--nolazy"],
"env": {
"NODE_ENV": "development"
},
"console": "integratedTerminal",
"sourceMaps": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
"name": "Vitest - Debug Current Test File",
"autoAttachChildProcesses": true,
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": ["run", "${relativeFile}"],
"smartStep": true,
"console": "integratedTerminal"
}
]
}

View File

@ -1,3 +1,44 @@
## [3.4.22](https://github.com/vuejs/core/compare/v3.4.21...v3.4.22) (2024-04-15)
### Bug Fixes
* **compat:** fix $options mutation + adjust private API initialization ([d58d133](https://github.com/vuejs/core/commit/d58d133b1cde5085cc5ab0012d544cafd62a6ee6)), closes [#10626](https://github.com/vuejs/core/issues/10626) [#10636](https://github.com/vuejs/core/issues/10636)
* **compile-sfc:** analyze v-bind shorthand usage in template ([#10518](https://github.com/vuejs/core/issues/10518)) ([e5919d4](https://github.com/vuejs/core/commit/e5919d4658cfe0bb18c76611dd3c3432c57f94ab)), closes [#10515](https://github.com/vuejs/core/issues/10515)
* **compiler-core:** fix loc.source for end tags with whitespace before > ([16174da](https://github.com/vuejs/core/commit/16174da21d6c8ac0aae027dd964fc35e221ded0a)), closes [#10694](https://github.com/vuejs/core/issues/10694) [#10695](https://github.com/vuejs/core/issues/10695)
* **compiler-core:** fix v-bind shorthand for component :is ([04af950](https://github.com/vuejs/core/commit/04af9504a720c8e6de26c04b1282cf14fa1bcee3)), closes [#10469](https://github.com/vuejs/core/issues/10469) [#10471](https://github.com/vuejs/core/issues/10471)
* **compiler-sfc:** :is() and :where() in compound selectors ([#10522](https://github.com/vuejs/core/issues/10522)) ([660cadc](https://github.com/vuejs/core/commit/660cadc7aadb909ef33a6055c4374902a82607a4)), closes [#10511](https://github.com/vuejs/core/issues/10511)
* **compiler-sfc:** also search for `.tsx` when type import's extension is omitted ([#10637](https://github.com/vuejs/core/issues/10637)) ([34106bc](https://github.com/vuejs/core/commit/34106bc9c715247211273bb9c64712f04bd4879d)), closes [#10635](https://github.com/vuejs/core/issues/10635)
* **compiler-sfc:** fix defineModel coercion for boolean + string union types ([#9603](https://github.com/vuejs/core/issues/9603)) ([0cef65c](https://github.com/vuejs/core/commit/0cef65cee411356e721bbc90d731fc52fc8fce94)), closes [#9587](https://github.com/vuejs/core/issues/9587) [#10676](https://github.com/vuejs/core/issues/10676)
* **compiler-sfc:** fix universal selector scope ([#10551](https://github.com/vuejs/core/issues/10551)) ([54a6afa](https://github.com/vuejs/core/commit/54a6afa75a546078e901ce0882da53b97420fe94)), closes [#10548](https://github.com/vuejs/core/issues/10548)
* **compiler-sfc:** use options module name if options provide runtimeModuleName options ([#10457](https://github.com/vuejs/core/issues/10457)) ([e76d743](https://github.com/vuejs/core/commit/e76d7430aa7470342f3fe263145a0fa92f5898ca)), closes [#10454](https://github.com/vuejs/core/issues/10454)
* **custom-element:** avoid setting attr to null if it is removed ([#9012](https://github.com/vuejs/core/issues/9012)) ([b49306a](https://github.com/vuejs/core/commit/b49306adff4572d90a42ccd231387f16eb966bbe)), closes [#9006](https://github.com/vuejs/core/issues/9006) [#10324](https://github.com/vuejs/core/issues/10324)
* **hydration:** properly handle optimized mode during hydrate node ([#10638](https://github.com/vuejs/core/issues/10638)) ([2ec06fd](https://github.com/vuejs/core/commit/2ec06fd6c8383e11cdf4efcab1707f973bd6a54c)), closes [#10607](https://github.com/vuejs/core/issues/10607)
* **reactivity:** computed should not be detected as true by isProxy ([#10401](https://github.com/vuejs/core/issues/10401)) ([9da34d7](https://github.com/vuejs/core/commit/9da34d7af81607fddd1f32f21b3b4002402ff1cc))
* **reactivity:** fix hasOwnProperty key coercion edge cases ([969c5fb](https://github.com/vuejs/core/commit/969c5fb30f4c725757c7385abfc74772514eae4b))
* **reactivity:** fix tracking when hasOwnProperty is called with non-string value ([c3c5dc9](https://github.com/vuejs/core/commit/c3c5dc93fbccc196771458f0b43cd5b7ad1863f4)), closes [#10455](https://github.com/vuejs/core/issues/10455) [#10464](https://github.com/vuejs/core/issues/10464)
* **runtime-core:** fix errorHandler causes an infinite loop during execution ([#9575](https://github.com/vuejs/core/issues/9575)) ([ab59bed](https://github.com/vuejs/core/commit/ab59bedae4e5e40b28804d88a51305b236d4a873))
* **runtime-core:** handle invalid values in callWithAsyncErrorHandling ([53d15d3](https://github.com/vuejs/core/commit/53d15d3f76184eed67a18d35e43d9a2062f8e121))
* **runtime-core:** show hydration mismatch details for non-rectified mismatches too when __PROD_HYDRATION_MISMATCH_DETAILS__ is set ([#10599](https://github.com/vuejs/core/issues/10599)) ([0dea7f9](https://github.com/vuejs/core/commit/0dea7f9a260d93eb6c39aabac8c94c2c9b2042dd))
* **runtime-dom:** `v-model` string/number coercion for multiselect options ([#10576](https://github.com/vuejs/core/issues/10576)) ([db374e5](https://github.com/vuejs/core/commit/db374e54c9f5e07324728b85c74eca84e28dd352))
* **runtime-dom:** fix css v-bind for suspensed components ([#8523](https://github.com/vuejs/core/issues/8523)) ([67722ba](https://github.com/vuejs/core/commit/67722ba23b7c36ab8f3fa2d2b4df08e4ddc322e1)), closes [#8520](https://github.com/vuejs/core/issues/8520)
* **runtime-dom:** force update v-model number with leading 0 ([#10506](https://github.com/vuejs/core/issues/10506)) ([15ffe8f](https://github.com/vuejs/core/commit/15ffe8f2c954359770c57e4d9e589b0b622e4a60)), closes [#10503](https://github.com/vuejs/core/issues/10503) [#10615](https://github.com/vuejs/core/issues/10615)
* **runtime-dom:** sanitize wrongly passed string value as event handler ([#8953](https://github.com/vuejs/core/issues/8953)) ([7ccd453](https://github.com/vuejs/core/commit/7ccd453dd004076cad49ec9f56cd5fe97b7b6ed8)), closes [#8818](https://github.com/vuejs/core/issues/8818)
* **ssr:** don't render v-if comments in TransitionGroup ([#6732](https://github.com/vuejs/core/issues/6732)) ([5a96267](https://github.com/vuejs/core/commit/5a9626708e970c6fc0b6f786e3c80c22273d126f)), closes [#6715](https://github.com/vuejs/core/issues/6715)
* **Transition:** ensure the KeepAlive children unmount w/ out-in mode ([#10632](https://github.com/vuejs/core/issues/10632)) ([fc99e4d](https://github.com/vuejs/core/commit/fc99e4d3f01b190ef9fd3c218a668ba9124a32bc)), closes [#10620](https://github.com/vuejs/core/issues/10620)
* **TransitionGroup:** avoid set transition hooks for comment nodes and text nodes ([#9421](https://github.com/vuejs/core/issues/9421)) ([140a768](https://github.com/vuejs/core/commit/140a7681cc3bba22f55d97fd85a5eafe97a1230f)), closes [#4621](https://github.com/vuejs/core/issues/4621) [#4622](https://github.com/vuejs/core/issues/4622) [#5153](https://github.com/vuejs/core/issues/5153) [#5168](https://github.com/vuejs/core/issues/5168) [#7898](https://github.com/vuejs/core/issues/7898) [#9067](https://github.com/vuejs/core/issues/9067)
* **types:** avoid merging object union types when using withDefaults ([#10596](https://github.com/vuejs/core/issues/10596)) ([37ba93c](https://github.com/vuejs/core/commit/37ba93c213a81f99a68a99ef5d4065d61b150ba3)), closes [#10594](https://github.com/vuejs/core/issues/10594)
### Performance Improvements
* add `__NO_SIDE_EFFECTS__` comments ([#9053](https://github.com/vuejs/core/issues/9053)) ([d46df6b](https://github.com/vuejs/core/commit/d46df6bdb14b0509eb2134b3f85297a306821c61))
* optimize component props/slots internal object checks ([6af733d](https://github.com/vuejs/core/commit/6af733d68eb400a3d2c5ef5f465fff32b72a324e))
* **ssr:** avoid calling markRaw on component instance proxy ([4bc9f39](https://github.com/vuejs/core/commit/4bc9f39f028af7313e5cf24c16915a1985d27bf8))
* **ssr:** optimize setup context creation for ssr in v8 ([ca84316](https://github.com/vuejs/core/commit/ca84316bfb3410efe21333670a6ad5cd21857396))
## [3.4.21](https://github.com/vuejs/core/compare/v3.4.20...v3.4.21) (2024-02-28)

7
FUNDING.json Normal file
View File

@ -0,0 +1,7 @@
{
"drips": {
"ethereum": {
"ownedBy": "0x5393BdeA2a020769256d9f337B0fc81a2F64850A"
}
}
}

View File

@ -6,7 +6,7 @@
"scripts": {
"dev": "node scripts/dev.js vue vue-vapor",
"build": "node scripts/build.js",
"build-dts": "tsc -p tsconfig.build.json && rollup -c rollup.dts.config.js",
"build-dts": "tsc -p tsconfig.build-browser.json && tsc -p tsconfig.build-node.json && rollup -c rollup.dts.config.js",
"clean": "rimraf packages/*/dist temp .eslintcache",
"size": "run-s \"size-*\" && tsx scripts/usage-size.ts",
"size-global": "node scripts/build.js vue vue-vapor runtime-dom runtime-vapor compiler-dom compiler-vapor -f global -p --size",
@ -20,7 +20,7 @@
"test-unit": "vitest -c vitest.unit.config.ts",
"test-e2e": "node scripts/build.js vue -f global -d && vitest -c vitest.e2e.config.ts",
"test-dts": "run-s build-dts test-dts-only",
"test-dts-only": "tsc -p ./packages/dts-test/tsconfig.test.json",
"test-dts-only": "tsc -p packages/dts-built-test/tsconfig.json && tsc -p ./packages/dts-test/tsconfig.test.json",
"test-coverage": "vitest -c vitest.unit.config.ts --coverage",
"test-bench": "vitest bench",
"release": "node scripts/release.js",
@ -59,9 +59,9 @@
"node": ">=18.12.0"
},
"devDependencies": {
"@babel/parser": "^7.24.0",
"@babel/parser": "^7.24.1",
"@babel/types": "^7.24.0",
"@codspeed/vitest-plugin": "^2.3.1",
"@codspeed/vitest-plugin": "^3.1.0",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
@ -70,16 +70,16 @@
"@rollup/plugin-terser": "^0.4.4",
"@types/hash-sum": "^1.0.2",
"@types/minimist": "^1.2.5",
"@types/node": "^20.11.25",
"@types/node": "^20.12.5",
"@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@vitest/coverage-istanbul": "^1.3.1",
"@vitest/ui": "^1.2.2",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"@vitest/coverage-istanbul": "^1.4.0",
"@vitest/ui": "^1.4.0",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.4.1",
"esbuild": "^0.20.1",
"esbuild": "^0.20.2",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^8.57.0",
"eslint-define-config": "^2.1.0",
@ -99,21 +99,21 @@
"prettier": "^3.2.5",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.2",
"puppeteer": "~22.4.1",
"puppeteer": "~22.6.3",
"rimraf": "^5.0.5",
"rollup": "^4.12.1",
"rollup": "^4.13.2",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.0",
"serve": "^14.2.1",
"simple-git-hooks": "^2.10.0",
"terser": "^5.29.1",
"simple-git-hooks": "^2.11.1",
"terser": "^5.30.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.6.2",
"tsx": "^4.7.1",
"typescript": "^5.2.2",
"vite": "^5.1.5",
"vitest": "^1.3.1"
"tsx": "^4.7.2",
"typescript": "~5.4.5",
"vite": "^5.2.7",
"vitest": "^1.4.0"
}
}

View File

@ -2070,6 +2070,16 @@ describe('compiler: parse', () => {
baseParse(`<Foo>`, { parseMode: 'sfc', onError() {} })
expect(() => baseParse(`{ foo }`)).not.toThrow()
})
test('correct loc when the closing > is foarmatted', () => {
const [span] = baseParse(`<span></span
>`).children
expect(span.loc.source).toBe('<span></span\n \n >')
expect(span.loc.start.offset).toBe(0)
expect(span.loc.end.offset).toBe(27)
})
})
describe('decodeEntities option', () => {
@ -2166,7 +2176,7 @@ describe('compiler: parse', () => {
})
test('should remove leading newline character immediately following the pre element start tag', () => {
const ast = baseParse(`<pre>\n foo bar </pre>`, {
const ast = parse(`<pre>\n foo bar </pre>`, {
isPreTag: tag => tag === 'pre',
})
expect(ast.children).toHaveLength(1)
@ -2176,7 +2186,7 @@ describe('compiler: parse', () => {
})
test('should NOT remove leading newline character immediately following child-tag of pre element', () => {
const ast = baseParse(`<pre><span></span>\n foo bar </pre>`, {
const ast = parse(`<pre><span></span>\n foo bar </pre>`, {
isPreTag: tag => tag === 'pre',
})
const preElement = ast.children[0] as ElementNode
@ -2187,7 +2197,7 @@ describe('compiler: parse', () => {
})
test('self-closing pre tag', () => {
const ast = baseParse(`<pre/><span>\n foo bar</span>`, {
const ast = parse(`<pre/><span>\n foo bar</span>`, {
isPreTag: tag => tag === 'pre',
})
const elementAfterPre = ast.children[1] as ElementNode
@ -2196,7 +2206,7 @@ describe('compiler: parse', () => {
})
test('should NOT condense whitespaces in RCDATA text mode', () => {
const ast = baseParse(`<textarea>Text:\n foo</textarea>`, {
const ast = parse(`<textarea>Text:\n foo</textarea>`, {
parseMode: 'html',
})
const preElement = ast.children[0] as ElementNode

View File

@ -1231,6 +1231,24 @@ describe('compiler: element transform', () => {
})
})
test('dynamic binding shorthand', () => {
const { node, root } = parseWithBind(`<component :is />`)
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node).toMatchObject({
isBlock: true,
tag: {
callee: RESOLVE_DYNAMIC_COMPONENT,
arguments: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'is',
isStatic: false,
},
],
},
})
})
test('is casting', () => {
const { node, root } = parseWithBind(`<div is="vue:foo" />`)
expect(root.helpers).toContain(RESOLVE_COMPONENT)

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
"version": "3.4.21",
"version": "3.4.22",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
@ -46,11 +46,11 @@
},
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-core#readme",
"dependencies": {
"@babel/parser": "^7.24.0",
"@babel/parser": "^7.24.1",
"@vue/shared": "workspace:*",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
},
"devDependencies": {
"@babel/types": "^7.24.0"

View File

@ -28,7 +28,7 @@ import {
getVNodeHelper,
locStub,
} from './ast'
import { type RawSourceMap, SourceMapGenerator } from 'source-map-js'
import { SourceMapGenerator } from 'source-map-js'
import {
advancePositionWithMutation,
assert,
@ -56,6 +56,45 @@ import {
} from './runtimeHelpers'
import type { ImportItem } from './transform'
/**
* The `SourceMapGenerator` type from `source-map-js` is a bit incomplete as it
* misses `toJSON()`. We also need to add types for internal properties which we
* need to access for better performance.
*
* Since TS 5.3, dts generation starts to strangely include broken triple slash
* references for source-map-js, so we are inlining all source map related types
* here to to workaround that.
*/
export interface CodegenSourceMapGenerator {
setSourceContent(sourceFile: string, sourceContent: string): void
// SourceMapGenerator has this method but the types do not include it
toJSON(): RawSourceMap
_sources: Set<string>
_names: Set<string>
_mappings: {
add(mapping: MappingItem): void
}
}
export interface RawSourceMap {
file?: string
sourceRoot?: string
version: string
sources: string[]
names: string[]
sourcesContent?: string[]
mappings: string
}
interface MappingItem {
source: string
generatedLine: number
generatedColumn: number
originalLine: number
originalColumn: number
name: string | null
}
const PURE_ANNOTATION = `/*#__PURE__*/`
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
@ -101,7 +140,7 @@ export interface CodegenContext
offset: number
indentLevel: number
pure: boolean
map?: SourceMapGenerator
map?: CodegenSourceMapGenerator
helper(key: symbol): string
push(code: string, newlineIndex?: number, node?: CodegenNode): void
indent(): void
@ -234,14 +273,14 @@ function createCodegenContext(
generatedLine: context.line,
generatedColumn: context.column - 1,
source: filename,
// @ts-expect-error it is possible to be null
name,
})
}
if (!__BROWSER__ && sourceMap) {
// lazy require source-map implementation, only in non-browser builds
context.map = new SourceMapGenerator()
context.map =
new SourceMapGenerator() as unknown as CodegenSourceMapGenerator
context.map.setSourceContent(filename, context.source)
context.map._sources.add(filename)
}

View File

@ -26,6 +26,8 @@ export {
NewlineType,
type CodegenContext,
type CodegenResult,
type CodegenSourceMapGenerator,
type RawSourceMap,
type BaseCodegenResult,
} from './codegen'
export {

View File

@ -74,6 +74,7 @@ export interface ParserOptions
delimiters?: [string, string]
/**
* Whitespace handling strategy
* @default 'condense'
*/
whitespace?: 'preserve' | 'condense'
/**

View File

@ -613,7 +613,7 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
// implied close, end should be backtracked to close
setLocEnd(el.loc, backTrack(end, CharCodes.Lt))
} else {
setLocEnd(el.loc, end + 1)
setLocEnd(el.loc, lookAhead(end, CharCodes.Gt) + 1)
}
if (tokenizer.inSFCRoot) {
@ -736,6 +736,12 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
}
}
function lookAhead(index: number, c: number) {
let i = index
while (currentInput.charCodeAt(i) !== c && i < currentInput.length - 1) i++
return i
}
function backTrack(index: number, c: number) {
let i = index
while (currentInput.charCodeAt(i) !== c && i >= 0) i--

View File

@ -64,6 +64,7 @@ import {
checkCompatEnabled,
isCompatEnabled,
} from '../compat/compatConfig'
import { processExpression } from './transformExpression'
// some directive transforms (e.g. v-model) may return a symbol for runtime
// import, which should be used instead of a resolveDirective call.
@ -253,7 +254,7 @@ export function resolveComponentType(
// 1. dynamic component
const isExplicitDynamic = isComponentTag(tag)
const isProp = findProp(node, 'is')
const isProp = findProp(node, 'is', false, true /* allow empty */)
if (isProp) {
if (
isExplicitDynamic ||
@ -263,10 +264,19 @@ export function resolveComponentType(
context,
))
) {
const exp =
isProp.type === NodeTypes.ATTRIBUTE
? isProp.value && createSimpleExpression(isProp.value.content, true)
: isProp.exp
let exp: ExpressionNode | undefined
if (isProp.type === NodeTypes.ATTRIBUTE) {
exp = isProp.value && createSimpleExpression(isProp.value.content, true)
} else {
exp = isProp.exp
if (!exp) {
// #10469 handle :is shorthand
exp = createSimpleExpression(`is`, false, isProp.loc)
if (!__BROWSER__) {
exp = isProp.exp = processExpression(exp, context)
}
}
}
if (exp) {
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
exp,

View File

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

View File

@ -103,6 +103,26 @@ return { modelValue }
})"
`;
exports[`defineModel() > w/ Boolean And Function types, production mode 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
props: {
"modelValue": { type: [Boolean, String] },
"modelModifiers": {},
},
emits: ["update:modelValue"],
setup(__props, { expose: __expose }) {
__expose();
const modelValue = _useModel<boolean | string>(__props, "modelValue")
return { modelValue }
}
})"
`;
exports[`defineModel() > w/ array props 1`] = `
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'

View File

@ -66,14 +66,14 @@ return { get vMyDir() { return vMyDir } }
exports[`dynamic arguments 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { FooBar, foo, bar, unused, baz } from './x'
import { FooBar, foo, bar, unused, baz, msg } from './x'
export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
return { get FooBar() { return FooBar }, get foo() { return foo }, get bar() { return bar }, get baz() { return baz } }
return { get FooBar() { return FooBar }, get foo() { return foo }, get bar() { return bar }, get baz() { return baz }, get msg() { return msg } }
}
})"

View File

@ -221,4 +221,24 @@ describe('defineModel()', () => {
assertCode(content)
expect(content).toMatch(`set: (v) => { return v + __props.x }`)
})
test('w/ Boolean And Function types, production mode', () => {
const { content, bindings } = compile(
`
<script setup lang="ts">
const modelValue = defineModel<boolean | string>()
</script>
`,
{ isProd: true },
)
assertCode(content)
expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
expect(content).toMatch('emits: ["update:modelValue"]')
expect(content).toMatch(
`const modelValue = _useModel<boolean | string>(__props, "modelValue")`,
)
expect(bindings).toStrictEqual({
modelValue: BindingTypes.SETUP_REF,
})
})
})

View File

@ -45,7 +45,7 @@ test('directive', () => {
test('dynamic arguments', () => {
const { content } = compile(`
<script setup lang="ts">
import { FooBar, foo, bar, unused, baz } from './x'
import { FooBar, foo, bar, unused, baz, msg } from './x'
</script>
<template>
<FooBar #[foo.slotName] />
@ -53,11 +53,12 @@ test('dynamic arguments', () => {
<div :[bar.attrName]="15"></div>
<div unused="unused"></div>
<div #[\`item:\${baz.key}\`]="{ value }"></div>
<FooBar :msg />
</template>
`)
expect(content).toMatch(
`return { get FooBar() { return FooBar }, get foo() { return foo }, ` +
`get bar() { return bar }, get baz() { return baz } }`,
`get bar() { return bar }, get baz() { return baz }, get msg() { return msg } }`,
)
assertCode(content)
})

View File

@ -561,6 +561,27 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
// #10635
test('relative tsx', () => {
const files = {
'/foo.tsx': 'export type P = { foo: number }',
'/bar/index.tsx': 'export type PP = { bar: string }',
}
const { props, deps } = resolve(
`
import { P } from './foo'
import { PP } from './bar'
defineProps<P & PP>()
`,
files,
)
expect(props).toStrictEqual({
foo: ['Number'],
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
test.runIf(process.platform === 'win32')('relative ts on Windows', () => {
const files = {
'C:\\Test\\FolderA\\foo.ts': 'export type P = { foo: number }',

View File

@ -161,6 +161,45 @@ describe('SFC scoped CSS', () => {
`)
})
// #10511
test(':is() and :where() in compound selectors', () => {
expect(
compileScoped(`.div { color: red; } .div:where(:hover) { color: blue; }`),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:where(:hover) { color: blue;
}"`)
expect(
compileScoped(`.div { color: red; } .div:is(:hover) { color: blue; }`),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:is(:hover) { color: blue;
}"`)
expect(
compileScoped(
`.div { color: red; } .div:where(.foo:hover) { color: blue; }`,
),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:where(.foo:hover) { color: blue;
}"`)
expect(
compileScoped(
`.div { color: red; } .div:is(.foo:hover) { color: blue; }`,
),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:is(.foo:hover) { color: blue;
}"`)
})
test('media query', () => {
expect(compileScoped(`@media print { .foo { color: red }}`))
.toMatchInlineSnapshot(`
@ -390,4 +429,23 @@ describe('SFC style preprocessors', () => {
expect(res.errors.length).toBe(0)
})
test('should mount scope on correct selector when have universal selector', () => {
expect(compileScoped(`* { color: red; }`)).toMatchInlineSnapshot(`
"[data-v-test] { color: red;
}"
`)
expect(compileScoped('* .foo { color: red; }')).toMatchInlineSnapshot(`
".foo[data-v-test] { color: red;
}"
`)
expect(compileScoped(`*.foo { color: red; }`)).toMatchInlineSnapshot(`
".foo[data-v-test] { color: red;
}"
`)
expect(compileScoped(`.foo * { color: red; }`)).toMatchInlineSnapshot(`
".foo[data-v-test] * { color: red;
}"
`)
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.4.21",
"version": "3.4.22",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@ -42,7 +42,7 @@
},
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-sfc#readme",
"dependencies": {
"@babel/parser": "^7.24.0",
"@babel/parser": "^7.24.1",
"@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*",
@ -50,8 +50,8 @@
"@vue/shared": "workspace:*",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.8",
"postcss": "^8.4.35",
"source-map-js": "^1.0.2"
"postcss": "^8.4.38",
"source-map-js": "^1.2.0"
},
"devDependencies": {
"@babel/types": "^7.24.0",
@ -59,10 +59,10 @@
"hash-sum": "^2.0.0",
"lru-cache": "10.1.0",
"merge-source-map": "^1.1.0",
"minimatch": "^9.0.3",
"minimatch": "^9.0.4",
"postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.0.15",
"postcss-selector-parser": "^6.0.16",
"pug": "^3.0.2",
"sass": "^1.71.1"
"sass": "^1.74.1"
}
}

View File

@ -13,7 +13,7 @@ import {
type StylePreprocessorResults,
processors,
} from './style/preprocessors'
import type { RawSourceMap } from 'source-map-js'
import type { RawSourceMap } from '@vue/compiler-core'
import { cssVarsPlugin } from './style/cssVars'
import postcssModules from 'postcss-modules'

View File

@ -6,14 +6,11 @@ import {
type NodeTransform,
NodeTypes,
type ParserOptions,
type RawSourceMap,
type RootNode,
createRoot,
} from '@vue/compiler-core'
import {
type RawSourceMap,
SourceMapConsumer,
SourceMapGenerator,
} from 'source-map-js'
import { SourceMapConsumer, SourceMapGenerator } from 'source-map-js'
import {
type AssetURLOptions,
type AssetURLTagConfig,

View File

@ -1,15 +1,17 @@
import {
type BindingMetadata,
type CodegenSourceMapGenerator,
type CompilerError,
type ElementNode,
NodeTypes,
type ParserOptions,
type RawSourceMap,
type RootNode,
type SourceLocation,
createRoot,
} from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom'
import { type RawSourceMap, SourceMapGenerator } from 'source-map-js'
import { SourceMapGenerator } from 'source-map-js'
import type { TemplateCompiler } from './compileTemplate'
import { parseCssVars } from './style/cssVars'
import { createCache } from './cache'
@ -382,7 +384,7 @@ function generateSourceMap(
const map = new SourceMapGenerator({
file: filename.replace(/\\/g, '/'),
sourceRoot: sourceRoot.replace(/\\/g, '/'),
})
}) as unknown as CodegenSourceMapGenerator
map.setSourceContent(filename, source)
map._sources.add(filename)
generated.split(splitRE).forEach((line, index) => {
@ -397,7 +399,6 @@ function generateSourceMap(
generatedLine,
generatedColumn: i,
source: filename,
// @ts-expect-error
name: null,
})
}

View File

@ -129,15 +129,19 @@ export function genModelProps(ctx: ScriptCompileContext) {
let runtimeTypes = type && inferRuntimeType(ctx, type)
if (runtimeTypes) {
const hasBoolean = runtimeTypes.includes('Boolean')
const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)
runtimeTypes = runtimeTypes.filter(el => {
if (el === UNKNOWN_TYPE) return false
return isProd
? el === 'Boolean' || (el === 'Function' && options)
: true
})
skipCheck = !isProd && hasUnknownType && runtimeTypes.length > 0
if (isProd || hasUnknownType) {
runtimeTypes = runtimeTypes.filter(
t =>
t === 'Boolean' ||
(hasBoolean && t === 'String') ||
(t === 'Function' && options),
)
skipCheck = !isProd && hasUnknownType && runtimeTypes.length > 0
}
}
let runtimeType =

View File

@ -60,6 +60,9 @@ function resolveTemplateUsedIdentifiers(sfc: SFCDescriptor): Set<string> {
extractIdentifiers(ids, prop.forParseResult!.source)
} else if (prop.exp) {
extractIdentifiers(ids, prop.exp)
} else if (prop.name === 'bind' && !prop.exp) {
// v-bind shorthand name as identifier
ids.add((prop.arg as SimpleExpressionNode).content)
}
}
if (

View File

@ -956,8 +956,10 @@ function resolveExt(filename: string, fs: FS) {
return (
tryResolve(filename) ||
tryResolve(filename + `.ts`) ||
tryResolve(filename + `.tsx`) ||
tryResolve(filename + `.d.ts`) ||
tryResolve(joinPaths(filename, `index.ts`)) ||
tryResolve(joinPaths(filename, `index.tsx`)) ||
tryResolve(joinPaths(filename, `index.d.ts`))
)
}

View File

@ -170,9 +170,37 @@ function rewriteSelector(
}
}
if (n.type === 'universal') {
const prev = selector.at(selector.index(n) - 1)
const next = selector.at(selector.index(n) + 1)
// * ... {}
if (!prev) {
// * .foo {} -> .foo[xxxxxxx] {}
if (next) {
if (next.type === 'combinator' && next.value === ' ') {
selector.removeChild(next)
}
selector.removeChild(n)
return
} else {
// * {} -> [xxxxxxx] {}
node = selectorParser.combinator({
value: '',
})
selector.insertBefore(n, node)
selector.removeChild(n)
return false
}
}
// .foo * -> .foo[xxxxxxx] *
if (node) return
}
if (
(n.type !== 'pseudo' && n.type !== 'combinator') ||
(n.type === 'pseudo' && (n.value === ':is' || n.value === ':where'))
(n.type === 'pseudo' &&
(n.value === ':is' || n.value === ':where') &&
!node)
) {
node = n
}

View File

@ -1,5 +1,5 @@
import merge from 'merge-source-map'
import type { RawSourceMap } from 'source-map-js'
import type { RawSourceMap } from '@vue/compiler-core'
import type { SFCStyleCompileOptions } from '../compileStyle'
import { isFunction } from '@vue/shared'

View File

@ -82,8 +82,6 @@ describe('transition-group', () => {
})
if (_ctx.ok) {
_push(\`<div>ok</div>\`)
} else {
_push(\`<!---->\`)
}
_push(\`<!--]-->\`)
}"

View File

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

View File

@ -141,6 +141,7 @@ export function processChildren(
context: SSRTransformContext,
asFragment = false,
disableNestedFragments = false,
disableCommentAsIfAlternate = false,
) {
if (asFragment) {
context.pushStringPart(`<!--[-->`)
@ -191,7 +192,12 @@ export function processChildren(
)
break
case NodeTypes.IF:
ssrProcessIf(child, context, disableNestedFragments)
ssrProcessIf(
child,
context,
disableNestedFragments,
disableCommentAsIfAlternate,
)
break
case NodeTypes.FOR:
ssrProcessFor(child, context, disableNestedFragments)

View File

@ -87,6 +87,13 @@ export function ssrProcessTransitionGroup(
* by disabling nested fragment wrappers from being generated.
*/
true,
/**
* TransitionGroup filters out comment children at runtime and thus
* doesn't expect comments to be present during hydration. We need to
* account for that by disabling the empty comment that is otherwise
* rendered for a falsy v-if that has no v-else specified. (#6715)
*/
true,
)
context.pushStringPart(`</`)
context.pushStringPart(tag.exp!)
@ -106,6 +113,6 @@ export function ssrProcessTransitionGroup(
}
} else {
// fragment
processChildren(node, context, true, true)
processChildren(node, context, true, true, true)
}
}

View File

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

View File

@ -2,7 +2,7 @@
"name": "@vue/dts-built-test",
"private": true,
"version": "0.0.0",
"types": "dist/dts-built-test.d.ts",
"types": "dist/index.d.ts",
"dependencies": {
"@vue/shared": "workspace:*",
"@vue/reactivity": "workspace:*",

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist",
"jsx": "preserve",
"module": "esnext",
"strict": true,
"moduleResolution": "Bundler",
"lib": ["esnext", "dom"],
"declaration": true,
"emitDeclarationOnly": true
},
"include": ["./src"]
}

View File

@ -102,6 +102,41 @@ describe('defineProps w/ union type declaration + withDefaults', () => {
)
})
describe('defineProps w/ object union + withDefaults', () => {
const props = withDefaults(
defineProps<
{
foo: string
} & (
| {
type: 'hello'
bar: string
}
| {
type: 'world'
bar: number
}
)
>(),
{
foo: 'default value!',
},
)
expectType<
| {
readonly type: 'hello'
readonly bar: string
readonly foo: string
}
| {
readonly type: 'world'
readonly bar: number
readonly foo: string
}
>(props)
})
describe('defineProps w/ generic type declaration + withDefaults', <T extends
number, TA extends {
a: string

12
packages/global.d.ts vendored
View File

@ -45,18 +45,6 @@ declare module 'estree-walker' {
)
}
declare module 'source-map-js' {
export interface SourceMapGenerator {
// SourceMapGenerator has this method but the types do not include it
toJSON(): RawSourceMap
_sources: Set<string>
_names: Set<string>
_mappings: {
add(mapping: MappingItem): void
}
}
}
declare interface String {
/**
* @deprecated Please use String.prototype.slice instead of String.prototype.substring in the repository.

View File

@ -4,6 +4,7 @@ import {
EffectScope,
computed,
effect,
effectScope,
getCurrentScope,
onScopeDispose,
reactive,
@ -13,21 +14,21 @@ import {
describe('reactivity/effect/scope', () => {
it('should run', () => {
const fnSpy = vi.fn(() => {})
new EffectScope().run(fnSpy)
effectScope().run(fnSpy)
expect(fnSpy).toHaveBeenCalledTimes(1)
})
it('should accept zero argument', () => {
const scope = new EffectScope()
const scope = effectScope()
expect(scope.effects.length).toBe(0)
})
it('should return run value', () => {
expect(new EffectScope().run(() => 1)).toBe(1)
expect(effectScope().run(() => 1)).toBe(1)
})
it('should work w/ active property', () => {
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => 1)
expect(scope.active).toBe(true)
scope.stop()
@ -35,7 +36,7 @@ describe('reactivity/effect/scope', () => {
})
it('should collect the effects', () => {
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => {
let dummy
const counter = reactive({ num: 0 })
@ -53,7 +54,7 @@ describe('reactivity/effect/scope', () => {
let dummy, doubled
const counter = reactive({ num: 0 })
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
effect(() => (doubled = counter.num * 2))
@ -77,11 +78,11 @@ describe('reactivity/effect/scope', () => {
let dummy, doubled
const counter = reactive({ num: 0 })
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
// nested scope
new EffectScope().run(() => {
effectScope().run(() => {
effect(() => (doubled = counter.num * 2))
})
})
@ -107,11 +108,11 @@ describe('reactivity/effect/scope', () => {
let dummy, doubled
const counter = reactive({ num: 0 })
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
// nested scope
new EffectScope(true).run(() => {
effectScope(true).run(() => {
effect(() => (doubled = counter.num * 2))
})
})
@ -136,7 +137,7 @@ describe('reactivity/effect/scope', () => {
let dummy, doubled
const counter = reactive({ num: 0 })
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
})
@ -160,7 +161,7 @@ describe('reactivity/effect/scope', () => {
let dummy, doubled
const counter = reactive({ num: 0 })
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
})
@ -185,7 +186,7 @@ describe('reactivity/effect/scope', () => {
it('should fire onScopeDispose hook', () => {
let dummy = 0
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => {
onScopeDispose(() => (dummy += 1))
onScopeDispose(() => (dummy += 2))
@ -203,7 +204,7 @@ describe('reactivity/effect/scope', () => {
it('should warn onScopeDispose() is called when there is no active effect scope', () => {
const spy = vi.fn()
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => {
onScopeDispose(spy)
})
@ -221,8 +222,8 @@ describe('reactivity/effect/scope', () => {
})
it('should dereference child scope from parent scope after stopping child scope (no memleaks)', () => {
const parent = new EffectScope()
const child = parent.run(() => new EffectScope())!
const parent = effectScope()
const child = parent.run(() => effectScope())!
expect(parent.scopes!.includes(child)).toBe(true)
child.stop()
expect(parent.scopes!.includes(child)).toBe(false)
@ -236,7 +237,7 @@ describe('reactivity/effect/scope', () => {
const watchEffectSpy = vi.fn()
let c: ComputedRef
const scope = new EffectScope()
const scope = effectScope()
scope.run(() => {
c = computed(() => {
computedSpy()
@ -272,12 +273,12 @@ describe('reactivity/effect/scope', () => {
})
it('getCurrentScope() stays valid when running a detached nested EffectScope', () => {
const parentScope = new EffectScope()
const parentScope = effectScope()
parentScope.run(() => {
const currentScope = getCurrentScope()
expect(currentScope).toBeDefined()
const detachedScope = new EffectScope(true)
const detachedScope = effectScope(true)
detachedScope.run(() => {})
expect(getCurrentScope()).toBe(currentScope)
@ -285,10 +286,10 @@ describe('reactivity/effect/scope', () => {
})
it('calling .off() of a detached scope inside an active scope should not break currentScope', () => {
const parentScope = new EffectScope()
const parentScope = effectScope()
parentScope.run(() => {
const childScope = new EffectScope(true)
const childScope = effectScope(true)
childScope.on()
childScope.off()
expect(getCurrentScope()).toBe(parentScope)

View File

@ -1,5 +1,14 @@
import { isRef, ref } from '../src/ref'
import { isReactive, markRaw, reactive, toRaw } from '../src/reactive'
import {
isProxy,
isReactive,
markRaw,
reactive,
readonly,
shallowReactive,
shallowReadonly,
toRaw,
} from '../src/reactive'
import { computed } from '../src/computed'
import { effect } from '../src/effect'
@ -302,4 +311,52 @@ describe('reactivity/reactive', () => {
const observed = reactive(original)
expect(isReactive(observed)).toBe(false)
})
test('hasOwnProperty edge case: Symbol values', () => {
const key = Symbol()
const obj = reactive({ [key]: 1 }) as { [key]?: 1 }
let dummy
effect(() => {
dummy = obj.hasOwnProperty(key)
})
expect(dummy).toBe(true)
delete obj[key]
expect(dummy).toBe(false)
})
test('hasOwnProperty edge case: non-string values', () => {
const key = {}
const obj = reactive({ '[object Object]': 1 }) as { '[object Object]'?: 1 }
let dummy
effect(() => {
// @ts-expect-error
dummy = obj.hasOwnProperty(key)
})
expect(dummy).toBe(true)
// @ts-expect-error
delete obj[key]
expect(dummy).toBe(false)
})
test('isProxy', () => {
const foo = {}
expect(isProxy(foo)).toBe(false)
const fooRe = reactive(foo)
expect(isProxy(fooRe)).toBe(true)
const fooSRe = shallowReactive(foo)
expect(isProxy(fooSRe)).toBe(true)
const barRl = readonly(foo)
expect(isProxy(barRl)).toBe(true)
const barSRl = shallowReadonly(foo)
expect(isProxy(barSRl)).toBe(true)
const c = computed(() => {})
expect(isProxy(c)).toBe(false)
})
})

View File

@ -100,6 +100,21 @@ describe('reactivity/reactive/Array', () => {
expect(fn).toHaveBeenCalledTimes(1)
})
test('should track hasOwnProperty call with index', () => {
const original = [1, 2, 3]
const observed = reactive(original)
let dummy
effect(() => {
dummy = observed.hasOwnProperty(0)
})
expect(dummy).toBe(true)
delete observed[0]
expect(dummy).toBe(false)
})
test('shift on Array should trigger dependency once', () => {
const arr = reactive([1, 2, 3])
const fn = vi.fn()

View File

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

View File

@ -38,10 +38,12 @@ const builtInSymbols = new Set(
.filter(isSymbol),
)
function hasOwnProperty(this: object, key: string) {
function hasOwnProperty(this: object, key: unknown) {
// #10455 hasOwnProperty may be called with non-string values
if (!isSymbol(key)) key = String(key)
const obj = toRaw(this)
track(obj, TrackOpTypes.HAS, key)
return obj.hasOwnProperty(key)
return obj.hasOwnProperty(key as string)
}
class BaseReactiveHandler implements ProxyHandler<Target> {

View File

@ -329,8 +329,8 @@ export function isShallow(value: unknown): boolean {
* @param value - The value to check.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#isproxy}
*/
export function isProxy(value: unknown): boolean {
return isReactive(value) || isReadonly(value)
export function isProxy(value: any): boolean {
return value ? !!value[ReactiveFlags.RAW] : false
}
/**
@ -409,5 +409,5 @@ export const toReactive = <T extends unknown>(value: T): T =>
*
* @param value - The value for which a readonly proxy shall be created.
*/
export const toReadonly = <T extends unknown>(value: T): T =>
isObject(value) ? readonly(value) : value
export const toReadonly = <T extends unknown>(value: T): DeepReadonly<T> =>
isObject(value) ? readonly(value) : (value as DeepReadonly<T>)

View File

@ -7,6 +7,7 @@ import {
h,
nextTick,
nodeOps,
onUnmounted,
ref,
render,
serialize,
@ -768,6 +769,42 @@ describe('BaseTransition', () => {
test('w/ KeepAlive', async () => {
await runTestWithKeepAlive(testOutIn)
})
test('w/ KeepAlive + unmount innerChild', async () => {
const unmountSpy = vi.fn()
const includeRef = ref(['TrueBranch'])
const trueComp = {
name: 'TrueBranch',
setup() {
onUnmounted(unmountSpy)
const count = ref(0)
return () => h('div', count.value)
},
}
const toggle = ref(true)
const { props } = mockProps({ mode: 'out-in' }, true /*withKeepAlive*/)
const root = nodeOps.createElement('div')
const App = {
render() {
return h(BaseTransition, props, () => {
return h(
KeepAlive,
{ include: includeRef.value },
toggle.value ? h(trueComp) : h('div'),
)
})
},
}
render(h(App), root)
// trigger toggle
toggle.value = false
includeRef.value = []
await nextTick()
expect(unmountSpy).toHaveBeenCalledTimes(1)
})
})
// #6835

View File

@ -583,5 +583,31 @@ describe('error handling', () => {
expect(handler).toHaveBeenCalledTimes(4)
})
// #9574
test('should pause tracking in error handler', async () => {
const error = new Error('error')
const x = ref(Math.random())
const handler = vi.fn(() => {
x.value
x.value = Math.random()
})
const app = createApp({
setup() {
return () => {
throw error
}
},
})
app.config.errorHandler = handler
app.mount(nodeOps.createElement('div'))
await nextTick()
expect(handler).toHaveBeenCalledWith(error, {}, 'render function')
expect(handler).toHaveBeenCalledTimes(1)
})
// native event handler handling should be tested in respective renderers
})

View File

@ -7,7 +7,10 @@ import {
Teleport,
Transition,
type VNode,
createBlock,
createCommentVNode,
createElementBlock,
createElementVNode,
createSSRApp,
createStaticVNode,
createTextVNode,
@ -17,16 +20,19 @@ import {
h,
nextTick,
onMounted,
openBlock,
ref,
renderSlot,
useCssVars,
vModelCheckbox,
vShow,
withCtx,
withDirectives,
} from '@vue/runtime-dom'
import { type SSRContext, renderToString } from '@vue/server-renderer'
import { PatchFlags } from '@vue/shared'
import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow'
import { expect } from 'vitest'
function mountWithHydration(html: string, render: () => any) {
const container = document.createElement('div')
@ -1292,6 +1298,81 @@ describe('SSR hydration', () => {
`)
})
// #10607
test('update component stable slot (prod + optimized mode)', async () => {
__DEV__ = false
const container = document.createElement('div')
container.innerHTML = `<template><div show="false"><!--[--><div><div><!----></div></div><div>0</div><!--]--></div></template>`
const Comp = {
render(this: any) {
return (
openBlock(),
createElementBlock('div', null, [renderSlot(this.$slots, 'default')])
)
},
}
const show = ref(false)
const clicked = ref(false)
const Wrapper = {
setup() {
const items = ref<number[]>([])
onMounted(() => {
items.value = [1]
})
return () => {
return (
openBlock(),
createBlock(Comp, null, {
default: withCtx(() => [
createElementVNode('div', null, [
createElementVNode('div', null, [
clicked.value
? (openBlock(),
createElementBlock('div', { key: 0 }, 'foo'))
: createCommentVNode('v-if', true),
]),
]),
createElementVNode(
'div',
null,
items.value.length,
1 /* TEXT */,
),
]),
_: 1 /* STABLE */,
})
)
}
},
}
createSSRApp({
components: { Wrapper },
data() {
return { show }
},
template: `<Wrapper :show="show"/>`,
}).mount(container)
await nextTick()
expect(container.innerHTML).toBe(
`<div show="false"><!--[--><div><div><!----></div></div><div>1</div><!--]--></div>`,
)
show.value = true
await nextTick()
expect(async () => {
clicked.value = true
await nextTick()
}).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
await nextTick()
expect(container.innerHTML).toBe(
`<div show="true"><!--[--><div><div><div>foo</div></div></div><div>1</div><!--]--></div>`,
)
__DEV__ = true
})
describe('mismatch handling', () => {
test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar')

View File

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

View File

@ -233,7 +233,7 @@ export type DefineModelOptions<T = any> = {
* Otherwise the prop name will default to "modelValue". In both cases, you
* can also pass an additional object which will be used as the prop's options.
*
* The the returned ref behaves differently depending on whether the parent
* The returned ref behaves differently depending on whether the parent
* provided the corresponding v-model props or not:
* - If yes, the returned ref's value will always be in sync with the parent
* prop.
@ -284,6 +284,9 @@ export function defineModel(): any {
}
type NotUndefined<T> = T extends undefined ? never : T
type MappedOmit<T, K extends keyof any> = {
[P in keyof T as P extends K ? never : P]: T[P]
}
type InferDefaults<T> = {
[K in keyof T]?: InferDefault<T, T[K]>
@ -299,7 +302,7 @@ type PropsWithDefaults<
T,
Defaults extends InferDefaults<T>,
BKeys extends keyof T,
> = Readonly<Omit<T, keyof Defaults>> & {
> = Readonly<MappedOmit<T, keyof Defaults>> & {
readonly [K in keyof Defaults]-?: K extends keyof T
? Defaults[K] extends undefined
? T[K]

View File

@ -427,15 +427,14 @@ function applySingletonPrototype(app: App, Ctor: Function) {
app.config.globalProperties = Object.create(Ctor.prototype)
}
let hasPrototypeAugmentations = false
const descriptors = Object.getOwnPropertyDescriptors(Ctor.prototype)
for (const key in descriptors) {
for (const key of Object.getOwnPropertyNames(Ctor.prototype)) {
if (key !== 'constructor') {
hasPrototypeAugmentations = true
if (enabled) {
Object.defineProperty(
app.config.globalProperties,
key,
descriptors[key],
Object.getOwnPropertyDescriptor(Ctor.prototype, key)!,
)
}
}

View File

@ -15,6 +15,7 @@ import {
DeprecationTypes,
assertCompatEnabled,
isCompatEnabled,
warnDeprecation,
} from './compatConfig'
import { off, on, once } from './instanceEventEmitter'
import { getCompatListeners } from './instanceListeners'
@ -121,50 +122,77 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
$children: getCompatChildren,
$listeners: getCompatListeners,
// inject additional properties into $options for compat
// e.g. vuex needs this.$options.parent
$options: i => {
if (!isCompatEnabled(DeprecationTypes.PRIVATE_APIS, i)) {
return resolveMergedOptions(i)
}
if (i.resolvedOptions) {
return i.resolvedOptions
}
const res = (i.resolvedOptions = extend({}, resolveMergedOptions(i)))
Object.defineProperties(res, {
parent: {
get() {
warnDeprecation(DeprecationTypes.PRIVATE_APIS, i, '$options.parent')
return i.proxy!.$parent
},
},
propsData: {
get() {
warnDeprecation(
DeprecationTypes.PRIVATE_APIS,
i,
'$options.propsData',
)
return i.vnode.props
},
},
})
return res
},
} as PublicPropertiesMap)
/* istanbul ignore if */
if (isCompatEnabled(DeprecationTypes.PRIVATE_APIS, null)) {
extend(map, {
// needed by many libs / render fns
$vnode: i => i.vnode,
const privateAPIs = {
// needed by many libs / render fns
$vnode: i => i.vnode,
// inject additional properties into $options for compat
// e.g. vuex needs this.$options.parent
$options: i => {
const res = extend({}, resolveMergedOptions(i))
res.parent = i.proxy!.$parent
res.propsData = i.vnode.props
return res
},
// some private properties that are likely accessed...
_self: i => i.proxy,
_uid: i => i.uid,
_data: i => i.data,
_isMounted: i => i.isMounted,
_isDestroyed: i => i.isUnmounted,
// some private properties that are likely accessed...
_self: i => i.proxy,
_uid: i => i.uid,
_data: i => i.data,
_isMounted: i => i.isMounted,
_isDestroyed: i => i.isUnmounted,
// v2 render helpers
$createElement: () => compatH,
_c: () => compatH,
_o: () => legacyMarkOnce,
_n: () => looseToNumber,
_s: () => toDisplayString,
_l: () => renderList,
_t: i => legacyRenderSlot.bind(null, i),
_q: () => looseEqual,
_i: () => looseIndexOf,
_m: i => legacyRenderStatic.bind(null, i),
_f: () => resolveFilter,
_k: i => legacyCheckKeyCodes.bind(null, i),
_b: () => legacyBindObjectProps,
_v: () => createTextVNode,
_e: () => createCommentVNode,
_u: () => legacyresolveScopedSlots,
_g: () => legacyBindObjectListeners,
_d: () => legacyBindDynamicKeys,
_p: () => legacyPrependModifier,
} as PublicPropertiesMap
// v2 render helpers
$createElement: () => compatH,
_c: () => compatH,
_o: () => legacyMarkOnce,
_n: () => looseToNumber,
_s: () => toDisplayString,
_l: () => renderList,
_t: i => legacyRenderSlot.bind(null, i),
_q: () => looseEqual,
_i: () => looseIndexOf,
_m: i => legacyRenderStatic.bind(null, i),
_f: () => resolveFilter,
_k: i => legacyCheckKeyCodes.bind(null, i),
_b: () => legacyBindObjectProps,
_v: () => createTextVNode,
_e: () => createCommentVNode,
_u: () => legacyresolveScopedSlots,
_g: () => legacyBindObjectListeners,
_d: () => legacyBindDynamicKeys,
_p: () => legacyPrependModifier,
} as PublicPropertiesMap)
for (const key in privateAPIs) {
map[key] = i => {
if (isCompatEnabled(DeprecationTypes.PRIVATE_APIS, i)) {
return privateAPIs[key](i)
}
}
}
}

View File

@ -45,6 +45,7 @@ import { type Directive, validateDirectiveName } from './directives'
import {
type ComponentOptions,
type ComputedOptions,
type MergedComponentOptions,
type MethodOptions,
applyOptions,
resolveMergedOptions,
@ -527,6 +528,12 @@ export interface ComponentInternalInstance {
* @internal
*/
getCssVars?: () => Record<string, string>
/**
* v2 compat only, for caching mutated $options
* @internal
*/
resolvedOptions?: MergedComponentOptions
}
const emptyAppContext = createAppContext()
@ -779,8 +786,7 @@ function setupStatefulComponent(
// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
@ -1009,36 +1015,28 @@ export function finishComponentSetup(
}
}
function getAttrsProxy(instance: ComponentInternalInstance): Data {
return (
instance.attrsProxy ||
(instance.attrsProxy = new Proxy(
instance.attrs,
__DEV__
? {
get(target, key: string) {
markAttrsAccessed()
track(instance, TrackOpTypes.GET, '$attrs')
return target[key]
},
set() {
warn(`setupContext.attrs is readonly.`)
return false
},
deleteProperty() {
warn(`setupContext.attrs is readonly.`)
return false
},
}
: {
get(target, key: string) {
track(instance, TrackOpTypes.GET, '$attrs')
return target[key]
},
},
))
)
}
const attrsProxyHandlers = __DEV__
? {
get(target: Data, key: string) {
markAttrsAccessed()
track(target, TrackOpTypes.GET, '')
return target[key]
},
set() {
warn(`setupContext.attrs is readonly.`)
return false
},
deleteProperty() {
warn(`setupContext.attrs is readonly.`)
return false
},
}
: {
get(target: Data, key: string) {
track(target, TrackOpTypes.GET, '')
return target[key]
},
}
/**
* Dev-only
@ -1085,9 +1083,13 @@ export function createSetupContext(
if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod)
let attrsProxy: Data
return Object.freeze({
get attrs() {
return getAttrsProxy(instance)
return (
attrsProxy ||
(attrsProxy = new Proxy(instance.attrs, attrsProxyHandlers))
)
},
get slots() {
return getSlotsProxy(instance)
@ -1099,9 +1101,7 @@ export function createSetupContext(
})
} else {
return {
get attrs() {
return getAttrsProxy(instance)
},
attrs: new Proxy(instance.attrs, attrsProxyHandlers),
slots: instance.slots,
emit: instance.emit,
expose,

View File

@ -13,7 +13,6 @@ import {
PatchFlags,
camelize,
capitalize,
def,
extend,
hasOwn,
hyphenate,
@ -34,7 +33,6 @@ import {
setCurrentInstance,
} from './component'
import { isEmitListener } from './componentEmits'
import { InternalObjectKey } from './vnode'
import type { AppContext } from './apiCreateApp'
import { createPropsDefaultThis } from './compat/props'
import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig'
@ -187,6 +185,13 @@ type NormalizedProp =
export type NormalizedProps = Record<string, NormalizedProp>
export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
/**
* Used during vnode props normalization to check if the vnode props is the
* attrs object of a component via `Object.getPrototypeOf`. This is more
* performant than defining a non-enumerable property.
*/
export const attrsProto = {}
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
@ -194,8 +199,7 @@ export function initProps(
isSSR = false,
) {
const props: Data = {}
const attrs: Data = {}
def(attrs, InternalObjectKey, 1)
const attrs: Data = Object.create(attrsProto)
instance.propsDefaults = Object.create(null)
@ -361,7 +365,7 @@ export function updateProps(
// trigger updates for $attrs in case it's used in component slots
if (hasAttrsChanged) {
trigger(instance, TriggerOpTypes.SET, '$attrs')
trigger(instance.attrs, TriggerOpTypes.SET, '')
}
if (__DEV__) {

View File

@ -23,6 +23,7 @@ import {
isString,
} from '@vue/shared'
import {
ReactiveFlags,
type ShallowUnwrapRef,
TrackOpTypes,
type UnwrapNestedRefs,
@ -306,6 +307,10 @@ const hasSetupBinding = (state: Data, key: string) =>
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
if (key === ReactiveFlags.SKIP) {
return true
}
const { ctx, setupState, data, props, accessCache, type, appContext } =
instance

View File

@ -1,6 +1,5 @@
import { type ComponentInternalInstance, currentInstance } from './component'
import {
InternalObjectKey,
type VNode,
type VNodeChild,
type VNodeNormalizedChildren,
@ -174,7 +173,7 @@ export const initSlots = (
// we should avoid the proxy object polluting the slots of the internal instance
instance.slots = toRaw(children as InternalSlots)
// make compiler marker non-enumerable
def(children as InternalSlots, '_', type)
def(instance.slots, '_', type)
} else {
normalizeObjectSlots(
children as RawSlots,
@ -188,7 +187,6 @@ export const initSlots = (
normalizeVNodeSlots(instance, children)
}
}
def(instance.slots, InternalObjectKey, 1)
}
export const updateSlots = (

View File

@ -254,7 +254,7 @@ const KeepAliveImpl: ComponentOptions = {
pendingCacheKey = null
if (!slots.default) {
return null
return (current = null)
}
const children = slots.default()

View File

@ -1,7 +1,8 @@
import { pauseTracking, resetTracking } from '@vue/reactivity'
import type { VNode } from './vnode'
import type { ComponentInternalInstance } from './component'
import { popWarningContext, pushWarningContext, warn } from './warning'
import { isFunction, isPromise } from '@vue/shared'
import { isArray, isFunction, isPromise } from '@vue/shared'
import { LifecycleHooks } from './enums'
import { BaseWatchErrorCodes } from '@vue/reactivity'
@ -82,7 +83,7 @@ export function callWithAsyncErrorHandling(
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[],
): any[] {
): any {
if (isFunction(fn)) {
const res = callWithErrorHandling(fn, instance, type, args)
if (res && isPromise(res)) {
@ -93,11 +94,17 @@ export function callWithAsyncErrorHandling(
return res
}
const values = []
for (let i = 0; i < fn.length; i++) {
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
if (isArray(fn)) {
const values = []
for (let i = 0; i < fn.length; i++) {
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
}
return values
} else if (__DEV__) {
warn(
`Invalid value type passed to callWithAsyncErrorHandling(): ${typeof fn}`,
)
}
return values
}
export function handleError(
@ -131,12 +138,14 @@ export function handleError(
// app-level handling
const appErrorHandler = instance.appContext.config.errorHandler
if (appErrorHandler) {
pauseTracking()
callWithErrorHandling(
appErrorHandler,
null,
ErrorCodes.APP_ERROR_HANDLER,
[err, exposedInstance, errorInfo],
)
resetTracking()
return
}
}

View File

@ -120,6 +120,7 @@ export function createHydrationFunctions(
slotScopeIds: string[] | null,
optimized = false,
): Node | null => {
optimized = optimized || !!vnode.dynamicChildren
const isFragmentStart = isComment(node) && node.data === '['
const onMismatch = () =>
handleMismatch(
@ -443,6 +444,7 @@ export function createHydrationFunctions(
if (props) {
if (
__DEV__ ||
__FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ||
forcePatch ||
!optimized ||
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
@ -450,7 +452,7 @@ export function createHydrationFunctions(
for (const key in props) {
// check hydration mismatch
if (
__DEV__ &&
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
propHasMismatch(el, key, props[key], vnode, parentComponent)
) {
hasMismatch = true

View File

@ -55,6 +55,7 @@ import { convertLegacyVModelProps } from './compat/componentVModel'
import { defineLegacyVNodeProperties } from './compat/renderFn'
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { ComponentPublicInstance } from './componentPublicInstance'
import { attrsProto } from './componentProps'
export const Fragment = Symbol.for('v-fgt') as any as {
__isFragment: true
@ -404,8 +405,6 @@ const createVNodeWithArgsTransform = (
)
}
export const InternalObjectKey = `__vInternal`
const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
key != null ? key : null
@ -618,7 +617,7 @@ function _createVNode(
export function guardReactiveProps(props: (Data & VNodeProps) | null) {
if (!props) return null
return isProxy(props) || InternalObjectKey in props
return isProxy(props) || Object.getPrototypeOf(props) === attrsProto
? extend({}, props)
: props
}
@ -792,7 +791,7 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
} else {
type = ShapeFlags.SLOTS_CHILDREN
const slotFlag = (children as RawSlots)._
if (!slotFlag && !(InternalObjectKey in children!)) {
if (!slotFlag) {
// if slots are not normalized, attach context instance
// (compiled / normalized slots already have context)
;(children as RawSlots)._ctx = currentRenderingInstance

View File

@ -139,6 +139,12 @@ describe('defineCustomElement', () => {
expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
expect(e.hasAttribute('foo')).toBe(false)
e.foo = undefined
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
expect(e.hasAttribute('foo')).toBe(false)
expect(e.foo).toBe(undefined)
e.bazQux = 'four'
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>four</div>')

View File

@ -1237,4 +1237,73 @@ describe('vModel', () => {
await nextTick()
expect(data.value).toEqual('使用拼音输入')
})
it('multiple select (model is number, option value is string)', async () => {
const component = defineComponent({
data() {
return {
value: [1, 2],
}
},
render() {
return [
withVModel(
h(
'select',
{
multiple: true,
'onUpdate:modelValue': setValue.bind(this),
},
[h('option', { value: '1' }), h('option', { value: '2' })],
),
this.value,
),
]
},
})
render(h(component), root)
await nextTick()
const [foo, bar] = root.querySelectorAll('option')
expect(foo.selected).toEqual(true)
expect(bar.selected).toEqual(true)
})
// #10503
test('equal value with a leading 0 should trigger update.', async () => {
const setNum = function (this: any, value: any) {
this.num = value
}
const component = defineComponent({
data() {
return { num: 0 }
},
render() {
return [
withVModel(
h('input', {
id: 'input_num1',
type: 'number',
'onUpdate:modelValue': setNum.bind(this),
}),
this.num,
),
]
},
})
render(h(component), root)
const data = root._vnode.component.data
const inputNum1 = root.querySelector('#input_num1')!
expect(inputNum1.value).toBe('0')
inputNum1.value = '01'
triggerEvent('input', inputNum1)
await nextTick()
expect(data.num).toBe(1)
expect(inputNum1.value).toBe('1')
})
})

View File

@ -118,6 +118,63 @@ describe('useCssVars', () => {
}
})
test('with v-if & async component & suspense', async () => {
const state = reactive({ color: 'red' })
const root = document.createElement('div')
const show = ref(false)
let resolveAsync: any
let asyncPromise: any
const AsyncComp = {
setup() {
useCssVars(() => state)
asyncPromise = new Promise(r => {
resolveAsync = () => {
r(() => h('p', 'default'))
}
})
return asyncPromise
},
}
const App = {
setup() {
return () =>
h(Suspense, null, {
default: h('div', {}, show.value ? h(AsyncComp) : h('p')),
})
},
}
render(h(App), root)
await nextTick()
// AsyncComp resolve
show.value = true
await nextTick()
resolveAsync()
await asyncPromise.then(() => {})
// Suspense effects flush
await nextTick()
// css vars use with default tree
for (const c of [].slice.call(root.children as any)) {
expect(
((c as any).children[0] as HTMLElement).style.getPropertyValue(
`--color`,
),
).toBe(`red`)
}
state.color = 'green'
await nextTick()
for (const c of [].slice.call(root.children as any)) {
expect(
((c as any).children[0] as HTMLElement).style.getPropertyValue(
`--color`,
),
).toBe('green')
}
})
test('with subTree changed', async () => {
const state = reactive({ color: 'red' })
const value = ref(true)

View File

@ -192,4 +192,14 @@ describe(`runtime-dom: events patching`, () => {
testElement.dispatchEvent(new CustomEvent('foobar'))
expect(fn2).toHaveBeenCalledTimes(1)
})
it('handles an unknown type', () => {
const el = document.createElement('div')
patchProp(el, 'onClick', null, 'test')
el.dispatchEvent(new Event('click'))
expect(
`Wrong type passed as event handler to onClick - did you forget @ or : ` +
`in front of your prop?\nExpected function or array of functions, received type string.`,
).toHaveBeenWarned()
})
})

View File

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

View File

@ -313,7 +313,7 @@ export class VueElement extends BaseClass {
}
protected _setAttr(key: string) {
let value = this.getAttribute(key)
let value = this.hasAttribute(key) ? this.getAttribute(key) : undefined
const camelKey = camelize(key)
if (this._numberProps && this._numberProps[camelKey]) {
value = toNumber(value)

View File

@ -112,7 +112,29 @@ const TransitionGroupImpl: ComponentOptions = {
tag = 'span'
}
prevChildren = children
prevChildren = []
if (children) {
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.el && child.el instanceof Element) {
prevChildren.push(child)
setTransitionHooks(
child,
resolveTransitionHooks(
child,
cssTransitionProps,
state,
instance,
),
)
positionMap.set(
child,
(child.el as Element).getBoundingClientRect(),
)
}
}
}
children = slots.default ? getTransitionRawChildren(slots.default()) : []
for (let i = 0; i < children.length; i++) {
@ -127,17 +149,6 @@ const TransitionGroupImpl: ComponentOptions = {
}
}
if (prevChildren) {
for (let i = 0; i < prevChildren.length; i++) {
const child = prevChildren[i]
setTransitionHooks(
child,
resolveTransitionHooks(child, cssTransitionProps, state, instance),
)
positionMap.set(child, (child.el as Element).getBoundingClientRect())
}
}
return createVNode(tag, null, children)
}
},

View File

@ -86,9 +86,10 @@ export const vModelText: ModelDirective<
el[assignKey] = getModelAssigner(vnode)
// avoid clearing unresolved text. #2302
if ((el as any).composing) return
const elValue =
number || el.type === 'number' ? looseToNumber(el.value) : el.value
(number || el.type === 'number') && !/^0\d/.test(el.value)
? looseToNumber(el.value)
: el.value
const newValue = value == null ? '' : value
if (elValue === newValue) {
@ -242,9 +243,7 @@ function setSelected(el: HTMLSelectElement, value: any, number: boolean) {
const optionType = typeof optionValue
// fast path for string / number values
if (optionType === 'string' || optionType === 'number') {
option.selected = value.includes(
number ? looseToNumber(optionValue) : optionValue,
)
option.selected = value.some(v => String(v) === String(optionValue))
} else {
option.selected = looseIndexOf(value, optionValue) > -1
}

View File

@ -42,9 +42,8 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>) {
updateTeleports(vars)
}
watchPostEffect(setVars)
onMounted(() => {
watchPostEffect(setVars)
const ob = new MutationObserver(setVars)
ob.observe(instance.subTree.el!.parentNode, { childList: true })
onUnmounted(() => ob.disconnect())

View File

@ -1,8 +1,9 @@
import { hyphenate, isArray } from '@vue/shared'
import { NOOP, hyphenate, isArray, isFunction } from '@vue/shared'
import {
type ComponentInternalInstance,
ErrorCodes,
callWithAsyncErrorHandling,
warn,
} from '@vue/runtime-core'
interface Invoker extends EventListener {
@ -36,7 +37,7 @@ export function patchEvent(
el: Element & { [veiKey]?: Record<string, Invoker | undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null,
nextValue: EventValue | unknown,
instance: ComponentInternalInstance | null = null,
) {
// vei = vue event invokers
@ -44,12 +45,19 @@ export function patchEvent(
const existingInvoker = invokers[rawName]
if (nextValue && existingInvoker) {
// patch
existingInvoker.value = nextValue
existingInvoker.value = __DEV__
? sanitizeEventValue(nextValue, rawName)
: (nextValue as EventValue)
} else {
const [name, options] = parseName(rawName)
if (nextValue) {
// add
const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
const invoker = (invokers[rawName] = createInvoker(
__DEV__
? sanitizeEventValue(nextValue, rawName)
: (nextValue as EventValue),
instance,
))
addEventListener(el, name, invoker, options)
} else if (existingInvoker) {
// remove
@ -116,6 +124,17 @@ function createInvoker(
return invoker
}
function sanitizeEventValue(value: unknown, propName: string): EventValue {
if (isFunction(value) || isArray(value)) {
return value as EventValue
}
warn(
`Wrong type passed as event handler to ${propName} - did you forget @ or : ` +
`in front of your prop?\nExpected function or array of functions, received type ${typeof value}.`,
)
return NOOP
}
function patchStopImmediatePropagation(
e: Event,
value: EventValue,
@ -126,7 +145,9 @@ function patchStopImmediatePropagation(
originalStop.call(e)
;(e as any)._stopped = true
}
return value.map(fn => (e: Event) => !(e as any)._stopped && fn && fn(e))
return (value as Function[]).map(
fn => (e: Event) => !(e as any)._stopped && fn && fn(e),
)
} else {
return value
}

View File

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

View File

@ -10,7 +10,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.1.5"
"vite": "^5.2.7"
},
"dependencies": {
"@vue/repl": "^4.1.1",

View File

@ -12,6 +12,6 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.1.5"
"vite": "^5.2.7"
}
}

View File

@ -1,3 +1,2 @@
// serve vue to the iframe sandbox during dev.
// @ts-expect-error
export * from 'vue/dist/vue.runtime.esm-browser.prod.js'

View File

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

View File

@ -172,6 +172,9 @@ export const toNumber = (val: any): any => {
return isNaN(n) ? val : n
}
// for typeof global checks without @types/node
declare var global: {}
let _globalThis: any
export const getGlobalThis = (): any => {
return (

View File

@ -54,4 +54,6 @@ const replacer = (_key: string, val: any): any => {
}
const stringifySymbol = (v: unknown, i: number | string = ''): any =>
isSymbol(v) ? `Symbol(${v.description ?? i})` : v
// Symbol.description in es2019+ so we need to cast here to pass
// the lib: es2016 check
isSymbol(v) ? `Symbol(${(v as any).description ?? i})` : v

View File

@ -12,7 +12,7 @@
},
"dependencies": {
"@vue/compiler-vapor": "workspace:^",
"monaco-editor": "^0.46.0",
"source-map-js": "^1.0.2"
"monaco-editor": "^0.47.0",
"source-map-js": "^1.2.0"
}
}

View File

@ -14,6 +14,7 @@ beforeEach(() => {
Vue.configureCompat({
MODE: 2,
GLOBAL_MOUNT: 'suppress-warning',
PRIVATE_APIS: 'suppress-warning',
})
})
@ -331,3 +332,43 @@ test('INSTANCE_ATTR_CLASS_STYLE', () => {
)('Anonymous'),
).toHaveBeenWarned()
})
test('$options mutation', () => {
const Comp = {
props: ['id'],
template: '<div/>',
data() {
return {
foo: '',
}
},
created(this: any) {
expect(this.$options.parent).toBeDefined()
expect(this.$options.test).toBeUndefined()
this.$options.test = this.id
expect(this.$options.test).toBe(this.id)
},
}
new Vue({
template: `<div><Comp id="1"/><Comp id="2"/></div>`,
components: { Comp },
}).$mount()
})
test('other private APIs', () => {
new Vue({
created() {
expect(this.$createElement).toBeTruthy()
},
})
new Vue({
compatConfig: {
PRIVATE_APIS: false,
},
created() {
expect(this.$createElement).toBeUndefined()
},
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compat",
"version": "3.4.21",
"version": "3.4.22",
"description": "Vue 3 compatibility build for Vue 2",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
@ -52,9 +52,9 @@
},
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/vue-compat#readme",
"dependencies": {
"@babel/parser": "^7.24.0",
"@babel/parser": "^7.24.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
},
"peerDependencies": {
"vue": "workspace:*"

View File

@ -508,4 +508,126 @@ describe('e2e: TransitionGroup', () => {
expect(`<TransitionGroup> children must be keyed`).toHaveBeenWarned()
})
// #5168, #7898, #9067
test(
'avoid set transition hooks for comment node',
async () => {
await page().evaluate(duration => {
const { createApp, ref, h, createCommentVNode } = (window as any).Vue
const show = ref(false)
createApp({
template: `
<div id="container">
<transition-group name="test">
<div v-for="item in items" :key="item" class="test">{{item}}</div>
<Child key="child"/>
</transition-group>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
components: {
Child: {
setup() {
return () =>
show.value
? h('div', { class: 'test' }, 'child')
: createCommentVNode('v-if', true)
},
},
},
setup: () => {
const items = ref([])
const click = () => {
items.value = ['a', 'b', 'c']
setTimeout(() => {
show.value = true
}, duration)
}
return { click, items }
},
}).mount('#app')
}, duration)
expect(await html('#container')).toBe(`<!--v-if-->`)
expect(await htmlWhenTransitionStart()).toBe(
`<div class="test test-enter-from test-enter-active">a</div>` +
`<div class="test test-enter-from test-enter-active">b</div>` +
`<div class="test test-enter-from test-enter-active">c</div>` +
`<!--v-if-->`,
)
await transitionFinish(duration)
await nextFrame()
expect(await html('#container')).toBe(
`<div class="test">a</div>` +
`<div class="test">b</div>` +
`<div class="test">c</div>` +
`<div class="test test-enter-active test-enter-to">child</div>`,
)
await transitionFinish(duration)
expect(await html('#container')).toBe(
`<div class="test">a</div>` +
`<div class="test">b</div>` +
`<div class="test">c</div>` +
`<div class="test">child</div>`,
)
},
E2E_TIMEOUT,
)
// #4621, #4622, #5153
test(
'avoid set transition hooks for text node',
async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
const app = createApp({
template: `
<div id="container">
<transition-group name="test">
<div class="test">foo</div>
<div class="test" v-if="show">bar</div>
</transition-group>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
setup: () => {
const show = ref(false)
const click = () => {
show.value = true
}
return { show, click }
},
})
app.config.compilerOptions.whitespace = 'preserve'
app.mount('#app')
})
expect(await html('#container')).toBe(`<div class="test">foo</div>` + ` `)
expect(await htmlWhenTransitionStart()).toBe(
`<div class="test">foo</div>` +
` ` +
`<div class="test test-enter-from test-enter-active">bar</div>`,
)
await nextFrame()
expect(await html('#container')).toBe(
`<div class="test">foo</div>` +
` ` +
`<div class="test test-enter-active test-enter-to">bar</div>`,
)
await transitionFinish(duration)
expect(await html('#container')).toBe(
`<div class="test">foo</div>` + ` ` + `<div class="test">bar</div>`,
)
},
E2E_TIMEOUT,
)
})

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -180,7 +180,7 @@ function createConfig(format, output, plugins = []) {
tsconfig: path.resolve(__dirname, 'tsconfig.json'),
sourceMap: output.sourcemap,
minify: false,
target: isServerRenderer || isCJSBuild ? 'es2019' : 'es2015',
target: isServerRenderer || isCJSBuild ? 'es2019' : 'es2016',
define: resolveDefine(),
}),
...resolveNodePlugins(),
@ -369,7 +369,7 @@ function createMinifiedConfig(/** @type {PackageFormat} */ format) {
terser({
module: /^esm/.test(format),
compress: {
ecma: 2015,
ecma: 2016,
pure_getters: true,
},
safari10: true,

View File

@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": [],
"declaration": true,
"emitDeclarationOnly": true,
"stripInternal": true
},
"include": [
"packages/global.d.ts",
"packages/vue/src",
"packages/vue-compat/src",
"packages/compiler-core/src",
"packages/compiler-dom/src",
"packages/runtime-core/src",
"packages/runtime-dom/src",
"packages/reactivity/src",
"packages/shared/src"
]
}

15
tsconfig.build-node.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node"],
"declaration": true,
"emitDeclarationOnly": true,
"stripInternal": true
},
"include": [
"packages/global.d.ts",
"packages/compiler-sfc/src",
"packages/compiler-ssr/src",
"packages/server-renderer/src"
]
}

View File

@ -1,16 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"stripInternal": true
},
"exclude": [
"packages/*/__tests__",
"packages/runtime-test",
"packages/template-explorer",
"packages/sfc-playground",
"packages/dts-test",
"playground"
]
}