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': [ 'no-restricted-syntax': [
'error', 'error',
banConstEnum, 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. selector: 'ObjectPattern > RestElement',
'ObjectPattern > RestElement', message:
// tsc compiles assignment spread into Object.assign() calls, but esbuild 'Our output target is ES2016, and object rest spread results in ' +
// still generates verbose helpers, so spread assignment is also prohiboted 'verbose helpers and should be avoided.',
'ObjectExpression > SpreadElement', },
'AwaitExpression', {
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 }], 'sort-imports': ['error', { ignoreDeclarationSort: true }],

1
.gitignore vendored
View File

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

23
.vscode/launch.json vendored
View File

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

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

View File

@ -2070,6 +2070,16 @@ describe('compiler: parse', () => {
baseParse(`<Foo>`, { parseMode: 'sfc', onError() {} }) baseParse(`<Foo>`, { parseMode: 'sfc', onError() {} })
expect(() => baseParse(`{ foo }`)).not.toThrow() 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', () => { describe('decodeEntities option', () => {
@ -2166,7 +2176,7 @@ describe('compiler: parse', () => {
}) })
test('should remove leading newline character immediately following the pre element start tag', () => { 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', isPreTag: tag => tag === 'pre',
}) })
expect(ast.children).toHaveLength(1) 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', () => { 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', isPreTag: tag => tag === 'pre',
}) })
const preElement = ast.children[0] as ElementNode const preElement = ast.children[0] as ElementNode
@ -2187,7 +2197,7 @@ describe('compiler: parse', () => {
}) })
test('self-closing pre tag', () => { 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', isPreTag: tag => tag === 'pre',
}) })
const elementAfterPre = ast.children[1] as ElementNode const elementAfterPre = ast.children[1] as ElementNode
@ -2196,7 +2206,7 @@ describe('compiler: parse', () => {
}) })
test('should NOT condense whitespaces in RCDATA text mode', () => { 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', parseMode: 'html',
}) })
const preElement = ast.children[0] as ElementNode 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', () => { test('is casting', () => {
const { node, root } = parseWithBind(`<div is="vue:foo" />`) const { node, root } = parseWithBind(`<div is="vue:foo" />`)
expect(root.helpers).toContain(RESOLVE_COMPONENT) expect(root.helpers).toContain(RESOLVE_COMPONENT)

View File

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

View File

@ -28,7 +28,7 @@ import {
getVNodeHelper, getVNodeHelper,
locStub, locStub,
} from './ast' } from './ast'
import { type RawSourceMap, SourceMapGenerator } from 'source-map-js' import { SourceMapGenerator } from 'source-map-js'
import { import {
advancePositionWithMutation, advancePositionWithMutation,
assert, assert,
@ -56,6 +56,45 @@ import {
} from './runtimeHelpers' } from './runtimeHelpers'
import type { ImportItem } from './transform' 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 PURE_ANNOTATION = `/*#__PURE__*/`
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}` const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
@ -101,7 +140,7 @@ export interface CodegenContext
offset: number offset: number
indentLevel: number indentLevel: number
pure: boolean pure: boolean
map?: SourceMapGenerator map?: CodegenSourceMapGenerator
helper(key: symbol): string helper(key: symbol): string
push(code: string, newlineIndex?: number, node?: CodegenNode): void push(code: string, newlineIndex?: number, node?: CodegenNode): void
indent(): void indent(): void
@ -234,14 +273,14 @@ function createCodegenContext(
generatedLine: context.line, generatedLine: context.line,
generatedColumn: context.column - 1, generatedColumn: context.column - 1,
source: filename, source: filename,
// @ts-expect-error it is possible to be null
name, name,
}) })
} }
if (!__BROWSER__ && sourceMap) { if (!__BROWSER__ && sourceMap) {
// lazy require source-map implementation, only in non-browser builds // 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.setSourceContent(filename, context.source)
context.map._sources.add(filename) context.map._sources.add(filename)
} }

View File

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

View File

@ -74,6 +74,7 @@ export interface ParserOptions
delimiters?: [string, string] delimiters?: [string, string]
/** /**
* Whitespace handling strategy * Whitespace handling strategy
* @default 'condense'
*/ */
whitespace?: 'preserve' | '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 // implied close, end should be backtracked to close
setLocEnd(el.loc, backTrack(end, CharCodes.Lt)) setLocEnd(el.loc, backTrack(end, CharCodes.Lt))
} else { } else {
setLocEnd(el.loc, end + 1) setLocEnd(el.loc, lookAhead(end, CharCodes.Gt) + 1)
} }
if (tokenizer.inSFCRoot) { 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) { function backTrack(index: number, c: number) {
let i = index let i = index
while (currentInput.charCodeAt(i) !== c && i >= 0) i-- while (currentInput.charCodeAt(i) !== c && i >= 0) i--

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/compiler-dom", "name": "@vue/compiler-dom",
"version": "3.4.21", "version": "3.4.22",
"description": "@vue/compiler-dom", "description": "@vue/compiler-dom",
"main": "index.js", "main": "index.js",
"module": "dist/compiler-dom.esm-bundler.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`] = ` exports[`defineModel() > w/ array props 1`] = `
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue' "import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'

View File

@ -66,14 +66,14 @@ return { get vMyDir() { return vMyDir } }
exports[`dynamic arguments 1`] = ` exports[`dynamic arguments 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "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({ export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __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) assertCode(content)
expect(content).toMatch(`set: (v) => { return v + __props.x }`) 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', () => { test('dynamic arguments', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
import { FooBar, foo, bar, unused, baz } from './x' import { FooBar, foo, bar, unused, baz, msg } from './x'
</script> </script>
<template> <template>
<FooBar #[foo.slotName] /> <FooBar #[foo.slotName] />
@ -53,11 +53,12 @@ test('dynamic arguments', () => {
<div :[bar.attrName]="15"></div> <div :[bar.attrName]="15"></div>
<div unused="unused"></div> <div unused="unused"></div>
<div #[\`item:\${baz.key}\`]="{ value }"></div> <div #[\`item:\${baz.key}\`]="{ value }"></div>
<FooBar :msg />
</template> </template>
`) `)
expect(content).toMatch( expect(content).toMatch(
`return { get FooBar() { return FooBar }, get foo() { return foo }, ` + `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) assertCode(content)
}) })

View File

@ -561,6 +561,27 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(Object.keys(files)) 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', () => { test.runIf(process.platform === 'win32')('relative ts on Windows', () => {
const files = { const files = {
'C:\\Test\\FolderA\\foo.ts': 'export type P = { foo: number }', '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', () => { test('media query', () => {
expect(compileScoped(`@media print { .foo { color: red }}`)) expect(compileScoped(`@media print { .foo { color: red }}`))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
@ -390,4 +429,23 @@ describe('SFC style preprocessors', () => {
expect(res.errors.length).toBe(0) 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", "name": "@vue/compiler-sfc",
"version": "3.4.21", "version": "3.4.22",
"description": "@vue/compiler-sfc", "description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js", "main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.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", "homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-sfc#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.24.0", "@babel/parser": "^7.24.1",
"@vue/compiler-core": "workspace:*", "@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*", "@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*", "@vue/compiler-ssr": "workspace:*",
@ -50,8 +50,8 @@
"@vue/shared": "workspace:*", "@vue/shared": "workspace:*",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.8", "magic-string": "^0.30.8",
"postcss": "^8.4.35", "postcss": "^8.4.38",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.24.0", "@babel/types": "^7.24.0",
@ -59,10 +59,10 @@
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"lru-cache": "10.1.0", "lru-cache": "10.1.0",
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"minimatch": "^9.0.3", "minimatch": "^9.0.4",
"postcss-modules": "^6.0.0", "postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.0.15", "postcss-selector-parser": "^6.0.16",
"pug": "^3.0.2", "pug": "^3.0.2",
"sass": "^1.71.1" "sass": "^1.74.1"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -60,6 +60,9 @@ function resolveTemplateUsedIdentifiers(sfc: SFCDescriptor): Set<string> {
extractIdentifiers(ids, prop.forParseResult!.source) extractIdentifiers(ids, prop.forParseResult!.source)
} else if (prop.exp) { } else if (prop.exp) {
extractIdentifiers(ids, 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 ( if (

View File

@ -956,8 +956,10 @@ function resolveExt(filename: string, fs: FS) {
return ( return (
tryResolve(filename) || tryResolve(filename) ||
tryResolve(filename + `.ts`) || tryResolve(filename + `.ts`) ||
tryResolve(filename + `.tsx`) ||
tryResolve(filename + `.d.ts`) || tryResolve(filename + `.d.ts`) ||
tryResolve(joinPaths(filename, `index.ts`)) || tryResolve(joinPaths(filename, `index.ts`)) ||
tryResolve(joinPaths(filename, `index.tsx`)) ||
tryResolve(joinPaths(filename, `index.d.ts`)) 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 ( if (
(n.type !== 'pseudo' && n.type !== 'combinator') || (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 node = n
} }

View File

@ -1,5 +1,5 @@
import merge from 'merge-source-map' 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 type { SFCStyleCompileOptions } from '../compileStyle'
import { isFunction } from '@vue/shared' import { isFunction } from '@vue/shared'

View File

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

View File

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

View File

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

View File

@ -87,6 +87,13 @@ export function ssrProcessTransitionGroup(
* by disabling nested fragment wrappers from being generated. * by disabling nested fragment wrappers from being generated.
*/ */
true, 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(`</`)
context.pushStringPart(tag.exp!) context.pushStringPart(tag.exp!)
@ -106,6 +113,6 @@ export function ssrProcessTransitionGroup(
} }
} else { } else {
// fragment // fragment
processChildren(node, context, true, true) processChildren(node, context, true, true, true)
} }
} }

View File

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

View File

@ -2,7 +2,7 @@
"name": "@vue/dts-built-test", "name": "@vue/dts-built-test",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"types": "dist/dts-built-test.d.ts", "types": "dist/index.d.ts",
"dependencies": { "dependencies": {
"@vue/shared": "workspace:*", "@vue/shared": "workspace:*",
"@vue/reactivity": "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 describe('defineProps w/ generic type declaration + withDefaults', <T extends
number, TA extends { number, TA extends {
a: string 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 { declare interface String {
/** /**
* @deprecated Please use String.prototype.slice instead of String.prototype.substring in the repository. * @deprecated Please use String.prototype.slice instead of String.prototype.substring in the repository.

View File

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

View File

@ -1,5 +1,14 @@
import { isRef, ref } from '../src/ref' 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 { computed } from '../src/computed'
import { effect } from '../src/effect' import { effect } from '../src/effect'
@ -302,4 +311,52 @@ describe('reactivity/reactive', () => {
const observed = reactive(original) const observed = reactive(original)
expect(isReactive(observed)).toBe(false) 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) 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', () => { test('shift on Array should trigger dependency once', () => {
const arr = reactive([1, 2, 3]) const arr = reactive([1, 2, 3])
const fn = vi.fn() const fn = vi.fn()

View File

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

View File

@ -38,10 +38,12 @@ const builtInSymbols = new Set(
.filter(isSymbol), .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) const obj = toRaw(this)
track(obj, TrackOpTypes.HAS, key) track(obj, TrackOpTypes.HAS, key)
return obj.hasOwnProperty(key) return obj.hasOwnProperty(key as string)
} }
class BaseReactiveHandler implements ProxyHandler<Target> { class BaseReactiveHandler implements ProxyHandler<Target> {

View File

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

View File

@ -7,6 +7,7 @@ import {
h, h,
nextTick, nextTick,
nodeOps, nodeOps,
onUnmounted,
ref, ref,
render, render,
serialize, serialize,
@ -768,6 +769,42 @@ describe('BaseTransition', () => {
test('w/ KeepAlive', async () => { test('w/ KeepAlive', async () => {
await runTestWithKeepAlive(testOutIn) 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 // #6835

View File

@ -583,5 +583,31 @@ describe('error handling', () => {
expect(handler).toHaveBeenCalledTimes(4) 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 // native event handler handling should be tested in respective renderers
}) })

View File

@ -7,7 +7,10 @@ import {
Teleport, Teleport,
Transition, Transition,
type VNode, type VNode,
createBlock,
createCommentVNode, createCommentVNode,
createElementBlock,
createElementVNode,
createSSRApp, createSSRApp,
createStaticVNode, createStaticVNode,
createTextVNode, createTextVNode,
@ -17,16 +20,19 @@ import {
h, h,
nextTick, nextTick,
onMounted, onMounted,
openBlock,
ref, ref,
renderSlot, renderSlot,
useCssVars, useCssVars,
vModelCheckbox, vModelCheckbox,
vShow, vShow,
withCtx,
withDirectives, withDirectives,
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { type SSRContext, renderToString } from '@vue/server-renderer' import { type SSRContext, renderToString } from '@vue/server-renderer'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow' import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow'
import { expect } from 'vitest'
function mountWithHydration(html: string, render: () => any) { function mountWithHydration(html: string, render: () => any) {
const container = document.createElement('div') 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', () => { describe('mismatch handling', () => {
test('text node', () => { test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar') const { container } = mountWithHydration(`foo`, () => 'bar')

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/runtime-core", "name": "@vue/runtime-core",
"version": "3.4.21", "version": "3.4.22",
"description": "@vue/runtime-core", "description": "@vue/runtime-core",
"main": "index.js", "main": "index.js",
"module": "dist/runtime-core.esm-bundler.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 * 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. * 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: * provided the corresponding v-model props or not:
* - If yes, the returned ref's value will always be in sync with the parent * - If yes, the returned ref's value will always be in sync with the parent
* prop. * prop.
@ -284,6 +284,9 @@ export function defineModel(): any {
} }
type NotUndefined<T> = T extends undefined ? never : T 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> = { type InferDefaults<T> = {
[K in keyof T]?: InferDefault<T, T[K]> [K in keyof T]?: InferDefault<T, T[K]>
@ -299,7 +302,7 @@ type PropsWithDefaults<
T, T,
Defaults extends InferDefaults<T>, Defaults extends InferDefaults<T>,
BKeys extends keyof T, BKeys extends keyof T,
> = Readonly<Omit<T, keyof Defaults>> & { > = Readonly<MappedOmit<T, keyof Defaults>> & {
readonly [K in keyof Defaults]-?: K extends keyof T readonly [K in keyof Defaults]-?: K extends keyof T
? Defaults[K] extends undefined ? Defaults[K] extends undefined
? T[K] ? T[K]

View File

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

View File

@ -15,6 +15,7 @@ import {
DeprecationTypes, DeprecationTypes,
assertCompatEnabled, assertCompatEnabled,
isCompatEnabled, isCompatEnabled,
warnDeprecation,
} from './compatConfig' } from './compatConfig'
import { off, on, once } from './instanceEventEmitter' import { off, on, once } from './instanceEventEmitter'
import { getCompatListeners } from './instanceListeners' import { getCompatListeners } from './instanceListeners'
@ -121,22 +122,42 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
$children: getCompatChildren, $children: getCompatChildren,
$listeners: getCompatListeners, $listeners: getCompatListeners,
} as PublicPropertiesMap)
/* istanbul ignore if */
if (isCompatEnabled(DeprecationTypes.PRIVATE_APIS, null)) {
extend(map, {
// needed by many libs / render fns
$vnode: i => i.vnode,
// inject additional properties into $options for compat // inject additional properties into $options for compat
// e.g. vuex needs this.$options.parent // e.g. vuex needs this.$options.parent
$options: i => { $options: i => {
const res = extend({}, resolveMergedOptions(i)) if (!isCompatEnabled(DeprecationTypes.PRIVATE_APIS, i)) {
res.parent = i.proxy!.$parent return resolveMergedOptions(i)
res.propsData = i.vnode.props }
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 return res
}, },
} as PublicPropertiesMap)
const privateAPIs = {
// needed by many libs / render fns
$vnode: i => i.vnode,
// some private properties that are likely accessed... // some private properties that are likely accessed...
_self: i => i.proxy, _self: i => i.proxy,
@ -165,6 +186,13 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
_g: () => legacyBindObjectListeners, _g: () => legacyBindObjectListeners,
_d: () => legacyBindDynamicKeys, _d: () => legacyBindDynamicKeys,
_p: () => legacyPrependModifier, _p: () => legacyPrependModifier,
} as PublicPropertiesMap) } 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 { import {
type ComponentOptions, type ComponentOptions,
type ComputedOptions, type ComputedOptions,
type MergedComponentOptions,
type MethodOptions, type MethodOptions,
applyOptions, applyOptions,
resolveMergedOptions, resolveMergedOptions,
@ -527,6 +528,12 @@ export interface ComponentInternalInstance {
* @internal * @internal
*/ */
getCssVars?: () => Record<string, string> getCssVars?: () => Record<string, string>
/**
* v2 compat only, for caching mutated $options
* @internal
*/
resolvedOptions?: MergedComponentOptions
} }
const emptyAppContext = createAppContext() const emptyAppContext = createAppContext()
@ -779,8 +786,7 @@ function setupStatefulComponent(
// 0. create render proxy property access cache // 0. create render proxy property access cache
instance.accessCache = Object.create(null) instance.accessCache = Object.create(null)
// 1. create public instance / render proxy // 1. create public instance / render proxy
// also mark it raw so it's never observed instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
if (__DEV__) { if (__DEV__) {
exposePropsOnRenderContext(instance) exposePropsOnRenderContext(instance)
} }
@ -1009,16 +1015,11 @@ export function finishComponentSetup(
} }
} }
function getAttrsProxy(instance: ComponentInternalInstance): Data { const attrsProxyHandlers = __DEV__
return (
instance.attrsProxy ||
(instance.attrsProxy = new Proxy(
instance.attrs,
__DEV__
? { ? {
get(target, key: string) { get(target: Data, key: string) {
markAttrsAccessed() markAttrsAccessed()
track(instance, TrackOpTypes.GET, '$attrs') track(target, TrackOpTypes.GET, '')
return target[key] return target[key]
}, },
set() { set() {
@ -1031,13 +1032,10 @@ function getAttrsProxy(instance: ComponentInternalInstance): Data {
}, },
} }
: { : {
get(target, key: string) { get(target: Data, key: string) {
track(instance, TrackOpTypes.GET, '$attrs') track(target, TrackOpTypes.GET, '')
return target[key] return target[key]
}, },
},
))
)
} }
/** /**
@ -1085,9 +1083,13 @@ export function createSetupContext(
if (__DEV__) { if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance // We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod) // properties (overwrites should not be done in prod)
let attrsProxy: Data
return Object.freeze({ return Object.freeze({
get attrs() { get attrs() {
return getAttrsProxy(instance) return (
attrsProxy ||
(attrsProxy = new Proxy(instance.attrs, attrsProxyHandlers))
)
}, },
get slots() { get slots() {
return getSlotsProxy(instance) return getSlotsProxy(instance)
@ -1099,9 +1101,7 @@ export function createSetupContext(
}) })
} else { } else {
return { return {
get attrs() { attrs: new Proxy(instance.attrs, attrsProxyHandlers),
return getAttrsProxy(instance)
},
slots: instance.slots, slots: instance.slots,
emit: instance.emit, emit: instance.emit,
expose, expose,

View File

@ -13,7 +13,6 @@ import {
PatchFlags, PatchFlags,
camelize, camelize,
capitalize, capitalize,
def,
extend, extend,
hasOwn, hasOwn,
hyphenate, hyphenate,
@ -34,7 +33,6 @@ import {
setCurrentInstance, setCurrentInstance,
} from './component' } from './component'
import { isEmitListener } from './componentEmits' import { isEmitListener } from './componentEmits'
import { InternalObjectKey } from './vnode'
import type { AppContext } from './apiCreateApp' import type { AppContext } from './apiCreateApp'
import { createPropsDefaultThis } from './compat/props' import { createPropsDefaultThis } from './compat/props'
import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig' import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig'
@ -187,6 +185,13 @@ type NormalizedProp =
export type NormalizedProps = Record<string, NormalizedProp> export type NormalizedProps = Record<string, NormalizedProp>
export type NormalizedPropsOptions = [NormalizedProps, string[]] | [] 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( export function initProps(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
rawProps: Data | null, rawProps: Data | null,
@ -194,8 +199,7 @@ export function initProps(
isSSR = false, isSSR = false,
) { ) {
const props: Data = {} const props: Data = {}
const attrs: Data = {} const attrs: Data = Object.create(attrsProto)
def(attrs, InternalObjectKey, 1)
instance.propsDefaults = Object.create(null) instance.propsDefaults = Object.create(null)
@ -361,7 +365,7 @@ export function updateProps(
// trigger updates for $attrs in case it's used in component slots // trigger updates for $attrs in case it's used in component slots
if (hasAttrsChanged) { if (hasAttrsChanged) {
trigger(instance, TriggerOpTypes.SET, '$attrs') trigger(instance.attrs, TriggerOpTypes.SET, '')
} }
if (__DEV__) { if (__DEV__) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1237,4 +1237,73 @@ describe('vModel', () => {
await nextTick() await nextTick()
expect(data.value).toEqual('使用拼音输入') 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 () => { test('with subTree changed', async () => {
const state = reactive({ color: 'red' }) const state = reactive({ color: 'red' })
const value = ref(true) const value = ref(true)

View File

@ -192,4 +192,14 @@ describe(`runtime-dom: events patching`, () => {
testElement.dispatchEvent(new CustomEvent('foobar')) testElement.dispatchEvent(new CustomEvent('foobar'))
expect(fn2).toHaveBeenCalledTimes(1) 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", "name": "@vue/runtime-dom",
"version": "3.4.21", "version": "3.4.22",
"description": "@vue/runtime-dom", "description": "@vue/runtime-dom",
"main": "index.js", "main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js", "module": "dist/runtime-dom.esm-bundler.js",

View File

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

View File

@ -112,7 +112,29 @@ const TransitionGroupImpl: ComponentOptions = {
tag = 'span' 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()) : [] children = slots.default ? getTransitionRawChildren(slots.default()) : []
for (let i = 0; i < children.length; i++) { 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) return createVNode(tag, null, children)
} }
}, },

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import { hyphenate, isArray } from '@vue/shared' import { NOOP, hyphenate, isArray, isFunction } from '@vue/shared'
import { import {
type ComponentInternalInstance, type ComponentInternalInstance,
ErrorCodes, ErrorCodes,
callWithAsyncErrorHandling, callWithAsyncErrorHandling,
warn,
} from '@vue/runtime-core' } from '@vue/runtime-core'
interface Invoker extends EventListener { interface Invoker extends EventListener {
@ -36,7 +37,7 @@ export function patchEvent(
el: Element & { [veiKey]?: Record<string, Invoker | undefined> }, el: Element & { [veiKey]?: Record<string, Invoker | undefined> },
rawName: string, rawName: string,
prevValue: EventValue | null, prevValue: EventValue | null,
nextValue: EventValue | null, nextValue: EventValue | unknown,
instance: ComponentInternalInstance | null = null, instance: ComponentInternalInstance | null = null,
) { ) {
// vei = vue event invokers // vei = vue event invokers
@ -44,12 +45,19 @@ export function patchEvent(
const existingInvoker = invokers[rawName] const existingInvoker = invokers[rawName]
if (nextValue && existingInvoker) { if (nextValue && existingInvoker) {
// patch // patch
existingInvoker.value = nextValue existingInvoker.value = __DEV__
? sanitizeEventValue(nextValue, rawName)
: (nextValue as EventValue)
} else { } else {
const [name, options] = parseName(rawName) const [name, options] = parseName(rawName)
if (nextValue) { if (nextValue) {
// add // 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) addEventListener(el, name, invoker, options)
} else if (existingInvoker) { } else if (existingInvoker) {
// remove // remove
@ -116,6 +124,17 @@ function createInvoker(
return invoker 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( function patchStopImmediatePropagation(
e: Event, e: Event,
value: EventValue, value: EventValue,
@ -126,7 +145,9 @@ function patchStopImmediatePropagation(
originalStop.call(e) originalStop.call(e)
;(e as any)._stopped = true ;(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 { } else {
return value return value
} }

View File

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

View File

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

View File

@ -12,6 +12,6 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.0.4", "@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. // serve vue to the iframe sandbox during dev.
// @ts-expect-error
export * from 'vue/dist/vue.runtime.esm-browser.prod.js' export * from 'vue/dist/vue.runtime.esm-browser.prod.js'

View File

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

View File

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

View File

@ -54,4 +54,6 @@ const replacer = (_key: string, val: any): any => {
} }
const stringifySymbol = (v: unknown, i: number | string = ''): 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": { "dependencies": {
"@vue/compiler-vapor": "workspace:^", "@vue/compiler-vapor": "workspace:^",
"monaco-editor": "^0.46.0", "monaco-editor": "^0.47.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.0"
} }
} }

View File

@ -14,6 +14,7 @@ beforeEach(() => {
Vue.configureCompat({ Vue.configureCompat({
MODE: 2, MODE: 2,
GLOBAL_MOUNT: 'suppress-warning', GLOBAL_MOUNT: 'suppress-warning',
PRIVATE_APIS: 'suppress-warning',
}) })
}) })
@ -331,3 +332,43 @@ test('INSTANCE_ATTR_CLASS_STYLE', () => {
)('Anonymous'), )('Anonymous'),
).toHaveBeenWarned() ).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", "name": "@vue/compat",
"version": "3.4.21", "version": "3.4.22",
"description": "Vue 3 compatibility build for Vue 2", "description": "Vue 3 compatibility build for Vue 2",
"main": "index.js", "main": "index.js",
"module": "dist/vue.runtime.esm-bundler.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", "homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/vue-compat#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.24.0", "@babel/parser": "^7.24.1",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.0"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "workspace:*" "vue": "workspace:*"

View File

@ -508,4 +508,126 @@ describe('e2e: TransitionGroup', () => {
expect(`<TransitionGroup> children must be keyed`).toHaveBeenWarned() 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", "name": "vue",
"version": "3.4.21", "version": "3.4.22",
"description": "The progressive JavaScript framework for building modern web UI.", "description": "The progressive JavaScript framework for building modern web UI.",
"main": "index.js", "main": "index.js",
"module": "dist/vue.runtime.esm-bundler.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'), tsconfig: path.resolve(__dirname, 'tsconfig.json'),
sourceMap: output.sourcemap, sourceMap: output.sourcemap,
minify: false, minify: false,
target: isServerRenderer || isCJSBuild ? 'es2019' : 'es2015', target: isServerRenderer || isCJSBuild ? 'es2019' : 'es2016',
define: resolveDefine(), define: resolveDefine(),
}), }),
...resolveNodePlugins(), ...resolveNodePlugins(),
@ -369,7 +369,7 @@ function createMinifiedConfig(/** @type {PackageFormat} */ format) {
terser({ terser({
module: /^esm/.test(format), module: /^esm/.test(format),
compress: { compress: {
ecma: 2015, ecma: 2016,
pure_getters: true, pure_getters: true,
}, },
safari10: 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"
]
}