chore: Merge branch 'main' into minor

This commit is contained in:
Evan You 2024-04-16 00:05:37 +08:00
commit bb5c31e614
No known key found for this signature in database
GPG Key ID: B9D421896CA450FB
109 changed files with 2042 additions and 886 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 }],

View File

@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v3.0.0
- name: Set node version to 18 - name: Set node version to 18
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

@ -17,7 +17,7 @@ jobs:
ref: minor ref: minor
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v3.0.0
- name: Set node version to 18 - name: Set node version to 18
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v3.0.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

@ -6,6 +6,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- minor
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
@ -20,7 +21,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v3.0.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -42,7 +43,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v3.0.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -71,7 +72,7 @@ jobs:
key: chromium-${{ hashFiles('pnpm-lock.yaml') }} key: chromium-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v3.0.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -97,7 +98,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v3.0.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -125,7 +126,7 @@ jobs:
# - uses: actions/checkout@v4 # - uses: actions/checkout@v4
# - name: Install pnpm # - name: Install pnpm
# uses: pnpm/action-setup@v2 # uses: pnpm/action-setup@v3.0.0
# - name: Install Node.js # - name: Install Node.js
# uses: actions/setup-node@v4 # uses: actions/setup-node@v4

View File

@ -7,6 +7,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- minor
permissions: permissions:
contents: read contents: read
@ -22,7 +23,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v3.0.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v3.0.0
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4

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,55 @@
## [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)
### Bug Fixes
* **runtime-dom:** avoid unset option's value ([#10416](https://github.com/vuejs/core/issues/10416)) ([b3f8b5a](https://github.com/vuejs/core/commit/b3f8b5a4e700d4c47a146b6040882287d180f6cb)), closes [#10412](https://github.com/vuejs/core/issues/10412) [#10396](https://github.com/vuejs/core/issues/10396)
* **suspense:** ensure nested suspense patching if in fallback state ([#10417](https://github.com/vuejs/core/issues/10417)) ([7c97778](https://github.com/vuejs/core/commit/7c97778aec1e3513035e5df265e1b8a7801f6106)), closes [#10415](https://github.com/vuejs/core/issues/10415)
* **warning:** stringify args in warn handler ([#10414](https://github.com/vuejs/core/issues/10414)) ([bc37258](https://github.com/vuejs/core/commit/bc37258caa2f6f67f4554ab8587aca3798d92124)), closes [#10409](https://github.com/vuejs/core/issues/10409)
## [3.4.20](https://github.com/vuejs/core/compare/v3.4.19...v3.4.20) (2024-02-26) ## [3.4.20](https://github.com/vuejs/core/compare/v3.4.19...v3.4.20) (2024-02-26)

7
FUNDING.json Normal file
View File

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

View File

@ -1,4 +1,4 @@
# vuejs/core [![npm](https://img.shields.io/npm/v/vue.svg)](https://www.npmjs.com/package/vue) [![build status](https://github.com/vuejs/core/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/vuejs/core/actions/workflows/ci.yml) # vuejs/core [![npm](https://img.shields.io/npm/v/vue.svg)](https://www.npmjs.com/package/vue) [![build status](https://github.com/vuejs/core/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/vuejs/core/actions/workflows/ci.yml) [![Download](https://img.shields.io/npm/dm/vue)](https://www.npmjs.com/package/vue)
## Getting Started ## Getting Started

View File

@ -1,12 +1,12 @@
{ {
"private": true, "private": true,
"version": "3.4.20", "version": "3.4.22",
"packageManager": "pnpm@8.15.4", "packageManager": "pnpm@8.15.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
"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 runtime-dom -f global -p --size", "size-global": "node scripts/build.js vue runtime-dom -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.23.9", "@babel/parser": "^7.24.1",
"@babel/types": "^7.23.9", "@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,15 +70,15 @@
"@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.20", "@types/node": "^20.12.5",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.0.2", "@typescript-eslint/parser": "^7.4.0",
"@vitest/coverage-istanbul": "^1.3.1", "@vitest/coverage-istanbul": "^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",
@ -89,30 +89,30 @@
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"lint-staged": "^15.2.2", "lint-staged": "^15.2.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"magic-string": "^0.30.7", "magic-string": "^0.30.8",
"markdown-table": "^3.0.3", "markdown-table": "^3.0.3",
"marked": "^12.0.0", "marked": "^12.0.1",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"npm-run-all2": "^6.1.2", "npm-run-all2": "^6.1.2",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"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.2.0", "puppeteer": "~22.6.3",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"rollup": "^4.12.0", "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.9.0", "simple-git-hooks": "^2.11.1",
"terser": "^5.28.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.4", "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.20", "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,13 +46,13 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.9", "@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.23.9" "@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]}`
@ -85,7 +124,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
@ -218,14 +257,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

@ -21,7 +21,13 @@ export {
type StructuralDirectiveTransform, type StructuralDirectiveTransform,
type DirectiveTransform, type DirectiveTransform,
} from './transform' } from './transform'
export { generate, type CodegenContext, type CodegenResult } from './codegen' export {
generate,
type CodegenContext,
type CodegenResult,
type CodegenSourceMapGenerator,
type RawSourceMap,
} from './codegen'
export { export {
ErrorCodes, ErrorCodes,
errorMessages, errorMessages,

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.20", "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

@ -1362,3 +1362,24 @@ return { get foo() { return foo } }
}" }"
`; `;
exports[`compileScript > should care about runtimeModuleName 1`] = `
"import { withAsyncContext as _withAsyncContext } from "npm:vue"
export default {
async setup(__props, { expose: __expose }) {
__expose();
let __temp, __restore
;(
([__temp,__restore] = _withAsyncContext(() => Promise.resolve(1))),
await __temp,
__restore()
)
return { }
}
}"
`;

View File

@ -1472,3 +1472,26 @@ describe('SFC genDefaultAs', () => {
}) })
}) })
}) })
describe('compileScript', () => {
test('should care about runtimeModuleName', () => {
const { content } = compile(
`
<script setup>
await Promise.resolve(1)
</script>
`,
{
templateOptions: {
compilerOptions: {
runtimeModuleName: 'npm:vue',
},
},
},
)
expect(content).toMatch(
`import { withAsyncContext as _withAsyncContext } from "npm:vue"\n`,
)
assertCode(content)
})
})

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,29 +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 } }
}
})"
`;
exports[`import namespace 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import * as Foo from './foo'
export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();
return { get Foo() { return Foo } }
} }
})" })"

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.20", "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,26 +42,26 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.9", "@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:*",
"@vue/shared": "workspace:*", "@vue/shared": "workspace:*",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.7", "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.23.9", "@babel/types": "^7.24.0",
"@vue/consolidate": "^1.0.0", "@vue/consolidate": "^1.0.0",
"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

@ -989,10 +989,15 @@ export function compileScript(
// 11. finalize Vue helper imports // 11. finalize Vue helper imports
if (ctx.helperImports.size > 0) { if (ctx.helperImports.size > 0) {
const runtimeModuleName =
options.templateOptions?.compilerOptions?.runtimeModuleName
const importSrc = runtimeModuleName
? JSON.stringify(runtimeModuleName)
: `'vue'`
ctx.s.prepend( ctx.s.prepend(
`import { ${[...ctx.helperImports] `import { ${[...ctx.helperImports]
.map(h => `${h} as _${h}`) .map(h => `${h} as _${h}`)
.join(', ')} } from 'vue'\n`, .join(', ')} } from ${importSrc}\n`,
) )
} }

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'
@ -375,7 +377,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) => {
@ -390,7 +392,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.20", "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.20", "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

@ -232,8 +232,10 @@ function createReadonlyMethod(type: TriggerOpTypes): Function {
} }
} }
type Instrumentations = Record<string | symbol, Function | number>
function createInstrumentations() { function createInstrumentations() {
const mutableInstrumentations: Record<string, Function | number> = { const mutableInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) { get(this: MapTypes, key: unknown) {
return get(this, key) return get(this, key)
}, },
@ -248,7 +250,7 @@ function createInstrumentations() {
forEach: createForEach(false, false), forEach: createForEach(false, false),
} }
const shallowInstrumentations: Record<string, Function | number> = { const shallowInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) { get(this: MapTypes, key: unknown) {
return get(this, key, false, true) return get(this, key, false, true)
}, },
@ -263,7 +265,7 @@ function createInstrumentations() {
forEach: createForEach(false, true), forEach: createForEach(false, true),
} }
const readonlyInstrumentations: Record<string, Function | number> = { const readonlyInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) { get(this: MapTypes, key: unknown) {
return get(this, key, true) return get(this, key, true)
}, },
@ -280,7 +282,7 @@ function createInstrumentations() {
forEach: createForEach(true, false), forEach: createForEach(true, false),
} }
const shallowReadonlyInstrumentations: Record<string, Function | number> = { const shallowReadonlyInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) { get(this: MapTypes, key: unknown) {
return get(this, key, true, true) return get(this, key, true, true)
}, },
@ -297,24 +299,18 @@ function createInstrumentations() {
forEach: createForEach(true, true), forEach: createForEach(true, true),
} }
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator] const iteratorMethods = [
'keys',
'values',
'entries',
Symbol.iterator,
] as const
iteratorMethods.forEach(method => { iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod( mutableInstrumentations[method] = createIterableMethod(method, false, false)
method, readonlyInstrumentations[method] = createIterableMethod(method, true, false)
false, shallowInstrumentations[method] = createIterableMethod(method, false, true)
false, shallowReadonlyInstrumentations[method] = createIterableMethod(
)
readonlyInstrumentations[method as string] = createIterableMethod(
method,
true,
false,
)
shallowInstrumentations[method as string] = createIterableMethod(
method,
false,
true,
)
shallowReadonlyInstrumentations[method as string] = createIterableMethod(
method, method,
true, true,
true, true,

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

@ -54,6 +54,18 @@ describe('Suspense', () => {
} }
} }
const RouterView = {
setup(_: any, { slots }: any) {
const route = inject('route') as any
const depth = inject('depth', 0)
provide('depth', depth + 1)
return () => {
const current = route.value[depth]
return slots.default({ Component: current })[0]
}
},
}
test('fallback content', async () => { test('fallback content', async () => {
const Async = defineAsyncComponent({ const Async = defineAsyncComponent({
render() { render() {
@ -1041,18 +1053,6 @@ describe('Suspense', () => {
// #10098 // #10098
test('switching branches w/ nested suspense', async () => { test('switching branches w/ nested suspense', async () => {
const RouterView = {
setup(_: any, { slots }: any) {
const route = inject('route') as any
const depth = inject('depth', 0)
provide('depth', depth + 1)
return () => {
const current = route.value[depth]
return slots.default({ Component: current })[0]
}
},
}
const OuterB = defineAsyncComponent({ const OuterB = defineAsyncComponent({
setup: () => { setup: () => {
return () => return () =>
@ -1132,6 +1132,121 @@ describe('Suspense', () => {
expect(serializeInner(root)).toBe(`<div>innerA</div>`) expect(serializeInner(root)).toBe(`<div>innerA</div>`)
}) })
// #10415
test('nested suspense (w/ suspensible) switch several times before parent suspense resolve', async () => {
const OuterA = defineAsyncComponent({
setup: () => {
return () =>
h(RouterView, null, {
default: ({ Component }: any) => [
h(Suspense, null, {
default: () => h(Component),
}),
],
})
},
})
const InnerA = defineAsyncComponent({
setup: () => {
return () => h('div', 'innerA')
},
})
const route = shallowRef([OuterA, InnerA])
const InnerB = defineAsyncComponent(
{
setup: () => {
return () => h('div', 'innerB')
},
},
5,
)
const InnerB1 = defineAsyncComponent(
{
setup: () => {
return () => h('div', 'innerB1')
},
},
5,
)
const InnerB2 = defineAsyncComponent(
{
setup: () => {
return () => h('div', 'innerB2')
},
},
5,
)
const OuterB = defineAsyncComponent(
{
setup() {
nextTick(async () => {
await new Promise(resolve => setTimeout(resolve, 1))
route.value = [OuterB, InnerB1]
})
nextTick(async () => {
await new Promise(resolve => setTimeout(resolve, 1))
route.value = [OuterB, InnerB2]
})
return () =>
h(RouterView, null, {
default: ({ Component }: any) => [
h(
Suspense,
{ suspensible: true },
{
default: () => h(Component),
},
),
],
})
},
},
5,
)
const Comp = {
setup() {
provide('route', route)
return () =>
h(RouterView, null, {
default: ({ Component }: any) => [
h(Suspense, null, {
default: () => h(Component),
}),
],
})
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<!---->`)
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>innerA</div>`)
deps.length = 0
route.value = [OuterB, InnerB]
await nextTick()
await Promise.all(deps)
await Promise.all(deps)
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>innerB2</div>`)
})
test('branch switch to 3rd branch before resolve', async () => { test('branch switch to 3rd branch before resolve', async () => {
const calls: string[] = [] const calls: string[] = []

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.20", "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,
@ -528,6 +529,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()
@ -780,8 +787,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)
} }
@ -1010,16 +1016,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() {
@ -1032,14 +1033,11 @@ 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]
}, },
}, }
))
)
}
/** /**
* Dev-only * Dev-only
@ -1086,9 +1084,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)
@ -1100,9 +1102,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

@ -12,7 +12,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

@ -99,7 +99,11 @@ export const SuspenseImpl = {
// 2. mounting along with the pendingBranch of parentSuspense // 2. mounting along with the pendingBranch of parentSuspense
// it is necessary to skip the current patch to avoid multiple mounts // it is necessary to skip the current patch to avoid multiple mounts
// of inner components. // of inner components.
if (parentSuspense && parentSuspense.deps > 0) { if (
parentSuspense &&
parentSuspense.deps > 0 &&
!n1.suspense!.isInFallback
) {
n2.suspense = n1.suspense! n2.suspense = n1.suspense!
n2.suspense.vnode = n2 n2.suspense.vnode = n2
n2.el = n1.el n2.el = n1.el

View File

@ -123,6 +123,7 @@ export const devtoolsComponentRemoved = (
} }
} }
/*! #__NO_SIDE_EFFECTS__ */
function createDevtoolsComponentHook(hook: DevtoolsHooks) { function createDevtoolsComponentHook(hook: DevtoolsHooks) {
return (component: ComponentInternalInstance) => { return (component: ComponentInternalInstance) => {
emit( emit(

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'
// contexts where user provided function may be executed, in addition to // contexts where user provided function may be executed, in addition to
@ -78,7 +79,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)) {
@ -89,11 +90,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(
@ -127,12 +134,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

@ -45,7 +45,7 @@ export function warn(msg: string, ...args: any[]) {
instance, instance,
ErrorCodes.APP_WARN_HANDLER, ErrorCodes.APP_WARN_HANDLER,
[ [
msg + args.join(''), msg + args.map(a => a.toString?.() ?? JSON.stringify(a)).join(''),
instance && instance.proxy, instance && instance.proxy,
trace trace
.map( .map(

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

@ -291,6 +291,18 @@ describe('runtime-dom: props patching', () => {
expect(el.value).toBe('baz') expect(el.value).toBe('baz')
}) })
test('init empty value for option', () => {
const root = document.createElement('div')
render(
h('select', { value: 'foo' }, [h('option', { value: '' }, 'foo')]),
root,
)
const select = root.children[0] as HTMLSelectElement
const option = select.children[0] as HTMLOptionElement
expect(select.value).toBe('')
expect(option.value).toBe('')
})
// #8780 // #8780
test('embedded tag with width and height', () => { test('embedded tag with width and height', () => {
// Width and height of some embedded element such as img、video、source、canvas // Width and height of some embedded element such as img、video、source、canvas

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/runtime-dom", "name": "@vue/runtime-dom",
"version": "3.4.20", "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

@ -1348,8 +1348,9 @@ export interface Events {
// selection events // selection events
onSelect: Event onSelect: Event
// UI events // scroll events
onScroll: UIEvent onScroll: Event
onScrollend: Event
// touch events // touch events
onTouchcancel: TouchEvent onTouchcancel: TouchEvent

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

@ -34,20 +34,20 @@ export function patchDOMProp(
// custom elements may use _value internally // custom elements may use _value internally
!tag.includes('-') !tag.includes('-')
) { ) {
// store value as _value as well since
// non-string values will be stringified.
el._value = value
// #4956: <option> value will fallback to its text content so we need to // #4956: <option> value will fallback to its text content so we need to
// compare against its attribute value instead. // compare against its attribute value instead.
const oldValue = const oldValue =
tag === 'OPTION' ? el.getAttribute('value') || '' : el.value tag === 'OPTION' ? el.getAttribute('value') || '' : el.value
const newValue = value == null ? '' : value const newValue = value == null ? '' : value
if (oldValue !== newValue) { if (oldValue !== newValue || !('_value' in el)) {
el.value = newValue el.value = newValue
} }
if (value == null) { if (value == null) {
el.removeAttribute(key) el.removeAttribute(key)
} }
// store value as _value as well since
// non-string values will be stringified.
el._value = value
return return
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@vue/server-renderer", "name": "@vue/server-renderer",
"version": "3.4.20", "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

@ -17,7 +17,7 @@ import {
} from '@vue/shared' } from '@vue/shared'
// leading comma for empty string "" // leading comma for empty string ""
const shouldIgnoreProp = makeMap( const shouldIgnoreProp = /*#__PURE__*/ makeMap(
`,key,ref,innerHTML,textContent,ref_key,ref_for`, `,key,ref,innerHTML,textContent,ref_key,ref_for`,
) )

View File

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

View File

@ -27,7 +27,7 @@ export async function downloadProject(store: ReplStore) {
const files = store.getFiles() const files = store.getFiles()
for (const file in files) { for (const file in files) {
if (file !== 'import-map.json') { if (file !== 'import-map.json' && file !== 'tsconfig.json') {
src.file(file, files[file]) src.file(file, files[file])
} else { } else {
zip.file(file, files[file]) zip.file(file, files[file])

View File

@ -1,6 +1,6 @@
# Vite Vue Starter # Vite Vue Starter
This is a project template using [Vite](https://vitejs.dev/). It requires [Node.js](https://nodejs.org) v12+. This is a project template using [Vite](https://vitejs.dev/). It requires [Node.js](https://nodejs.org) version 18+, 20+.
To start: To start:

View File

@ -12,6 +12,6 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.1.4" "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.20", "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

@ -165,6 +165,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

@ -5,6 +5,8 @@
* \/\*#\_\_PURE\_\_\*\/ * \/\*#\_\_PURE\_\_\*\/
* So that rollup can tree-shake them if necessary. * So that rollup can tree-shake them if necessary.
*/ */
/*! #__NO_SIDE_EFFECTS__ */
export function makeMap( export function makeMap(
str: string, str: string,
expectsLowerCase?: boolean, expectsLowerCase?: boolean,

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

@ -11,7 +11,7 @@
"enableNonBrowserBranches": true "enableNonBrowserBranches": true
}, },
"dependencies": { "dependencies": {
"monaco-editor": "^0.46.0", "monaco-editor": "^0.47.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.0"
} }
} }

Some files were not shown because too many files have changed in this diff Show More