Merge remote-tracking branch 'upstream/minor'

This commit is contained in:
三咲智子 Kevin Deng 2023-12-16 16:12:55 +08:00
commit 597eae423b
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
105 changed files with 3448 additions and 914 deletions

View File

@ -60,3 +60,28 @@ jobs:
- name: Run type declaration tests
run: pnpm run test-dts
# benchmarks:
# runs-on: ubuntu-latest
# if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
# env:
# PUPPETEER_SKIP_DOWNLOAD: 'true'
# steps:
# - uses: actions/checkout@v4
# - name: Install pnpm
# uses: pnpm/action-setup@v2
# - name: Install Node.js
# uses: actions/setup-node@v4
# with:
# node-version-file: '.node-version'
# cache: 'pnpm'
# - run: pnpm install
# - name: Run benchmarks
# uses: CodSpeedHQ/action@v2
# with:
# run: pnpm vitest bench --run
# token: ${{ secrets.CODSPEED_TOKEN }}

View File

@ -1,3 +1,103 @@
# [3.4.0-beta.3](https://github.com/vuejs/core/compare/v3.3.12...v3.4.0-beta.3) (2023-12-16)
### Bug Fixes
* **compiler-core:** vnode hooks error message ([#9842](https://github.com/vuejs/core/issues/9842)) ([7bc3c9e](https://github.com/vuejs/core/commit/7bc3c9e205c5158230772d9fcd25bf300809342e))
* **defineModel:** ensure trigger effect when prop changed ([#9841](https://github.com/vuejs/core/issues/9841)) ([eb12f21](https://github.com/vuejs/core/commit/eb12f211b8e312fd64d91ef1a58b2c2db618bdee)), closes [#9838](https://github.com/vuejs/core/issues/9838)
* **mathml:** update known mathML tags ([#9829](https://github.com/vuejs/core/issues/9829)) ([ebd78d2](https://github.com/vuejs/core/commit/ebd78d2c99d9587307e444e6b7baa7bc920d42e7))
* **Suspense:** fix edge case of Suspense being patched during async HOC child remount ([f0f6f7c](https://github.com/vuejs/core/commit/f0f6f7cea6e16650181e71dcfccbee405a1db503))
## [3.3.12](https://github.com/vuejs/core/compare/v3.3.11...v3.3.12) (2023-12-16)
### Bug Fixes
* **hydration:** handle appear transition before patch props ([#9837](https://github.com/vuejs/core/issues/9837)) ([e70f4c4](https://github.com/vuejs/core/commit/e70f4c47c553b6e16d8fad70743271ca23802fe7)), closes [#9832](https://github.com/vuejs/core/issues/9832)
* **sfc/cssVars:** fix loss of CSS v-bind variables when setting inline style with string value ([#9824](https://github.com/vuejs/core/issues/9824)) ([0a387df](https://github.com/vuejs/core/commit/0a387dfb1d04afb6eae4296b6da76dfdaca77af4)), closes [#9821](https://github.com/vuejs/core/issues/9821)
* **ssr:** fix suspense hydration of fallback content ([#7188](https://github.com/vuejs/core/issues/7188)) ([60415b5](https://github.com/vuejs/core/commit/60415b5d67df55f1fd6b176615299c08640fa142))
* **types:** add `xmlns:xlink` to `SVGAttributes` ([#9300](https://github.com/vuejs/core/issues/9300)) ([0d61b42](https://github.com/vuejs/core/commit/0d61b429ecf63591d31e09702058fa4c7132e1a7)), closes [#9299](https://github.com/vuejs/core/issues/9299)
* **types:** fix `shallowRef` type error ([#9839](https://github.com/vuejs/core/issues/9839)) ([9a57158](https://github.com/vuejs/core/commit/9a571582b53220270e498d8712ea59312c0bef3a))
* **types:** support for generic keyof slots ([#8374](https://github.com/vuejs/core/issues/8374)) ([213eba4](https://github.com/vuejs/core/commit/213eba479ce080efc1053fe636f6be4a4c889b44))
# [3.4.0-beta.2](https://github.com/vuejs/core/compare/v3.4.0-beta.1...v3.4.0-beta.2) (2023-12-14)
### Features
* **types:** remove default jsx global registration ([92b8d9c](https://github.com/vuejs/core/commit/92b8d9cef69146540db2bf7f2a5632ab5d38f672))
### BREAKING CHANGES
* **types:** Vue no longer registers the global `JSX` namespace by
default.
# [3.4.0-beta.1](https://github.com/vuejs/core/compare/v3.3.11...v3.4.0-beta.1) (2023-12-13)
### Bug Fixes
* **compiler-core:** use the same resolved options for all compile stages ([#9760](https://github.com/vuejs/core/issues/9760)) ([0dc875d](https://github.com/vuejs/core/commit/0dc875d53e5d869b44d0c1a70736ec859337b58f))
* **hydration:** should not warn mismatch for nullish prop ([33159a5](https://github.com/vuejs/core/commit/33159a5916bf7686fe53517befa59b450b34e974))
* **hydration:** swap client/server labels for hydration mismatch warnings ([f41fd86](https://github.com/vuejs/core/commit/f41fd86d5f26bd0009b4ca285ddc3cefaafa9f7c)), closes [#9098](https://github.com/vuejs/core/issues/9098) [#5953](https://github.com/vuejs/core/issues/5953)
* **runtime-core:** fix suspense crash when patching non-resolved async setup component ([#7290](https://github.com/vuejs/core/issues/7290)) ([bb0c889](https://github.com/vuejs/core/commit/bb0c8899cadd03af22e23c0383aaab363635c5b4)), closes [#5993](https://github.com/vuejs/core/issues/5993) [#6463](https://github.com/vuejs/core/issues/6463) [#6949](https://github.com/vuejs/core/issues/6949) [#6095](https://github.com/vuejs/core/issues/6095) [#8121](https://github.com/vuejs/core/issues/8121)
* **runtime-core:** properly pop warning context when mounting components with async setup ([69a2acc](https://github.com/vuejs/core/commit/69a2acc6ea159da8300a68ecc8953f19932c251b))
* **ssr:** fix suspense hydration of fallback content ([#7188](https://github.com/vuejs/core/issues/7188)) ([60415b5](https://github.com/vuejs/core/commit/60415b5d67df55f1fd6b176615299c08640fa142))
* **ssr:** make isInSSRComponentSetup state sharable across copies of Vue ([e04d821](https://github.com/vuejs/core/commit/e04d821422102446704e223c03e50d26cbb1fe69))
* **Suspense:** handle switching away from kept-alive component before resolve ([aa0c13f](https://github.com/vuejs/core/commit/aa0c13f637df7eb27faa2545ee731f543c0813ec)), closes [#6416](https://github.com/vuejs/core/issues/6416) [#6467](https://github.com/vuejs/core/issues/6467)
* **Suspense:** properly fix [#6416](https://github.com/vuejs/core/issues/6416) ([0db336f](https://github.com/vuejs/core/commit/0db336ff6c640fb9d3e48943c69f4c1737412be4))
* **types:** add `xmlns:xlink` to `SVGAttributes` ([#9300](https://github.com/vuejs/core/issues/9300)) ([0d61b42](https://github.com/vuejs/core/commit/0d61b429ecf63591d31e09702058fa4c7132e1a7)), closes [#9299](https://github.com/vuejs/core/issues/9299)
* **types:** support for generic keyof slots ([#8374](https://github.com/vuejs/core/issues/8374)) ([213eba4](https://github.com/vuejs/core/commit/213eba479ce080efc1053fe636f6be4a4c889b44))
### Features
* **compiler-core:** add current filename to TransformContext ([#8950](https://github.com/vuejs/core/issues/8950)) ([638f1ab](https://github.com/vuejs/core/commit/638f1abbb632000553e2b7d75e87c95d8ca192d6))
* **compiler-sfc:** promote defineModel stable ([#9598](https://github.com/vuejs/core/issues/9598)) ([ef688ba](https://github.com/vuejs/core/commit/ef688ba92bfccbc8b7ea3997eb297665d13e5249))
* **compiler-sfc:** support import attributes and `using` syntax ([#8786](https://github.com/vuejs/core/issues/8786)) ([5b2bd1d](https://github.com/vuejs/core/commit/5b2bd1df78e8ff524c3a184adaa284681aba6574))
* **defineModel:** support local mutation by default, remove local option ([f74785b](https://github.com/vuejs/core/commit/f74785bc4ad351102dde17fdfd2c7276b823111f)), closes [/github.com/vuejs/rfcs/discussions/503#discussioncomment-7566278](https://github.com//github.com/vuejs/rfcs/discussions/503/issues/discussioncomment-7566278)
* MathML support ([#7836](https://github.com/vuejs/core/issues/7836)) ([d42b6ba](https://github.com/vuejs/core/commit/d42b6ba3f530746eb1221eb7a4be0f44eb56f7d3)), closes [#7820](https://github.com/vuejs/core/issues/7820)
* **runtime-core:** provide full props to props validator functions ([#3258](https://github.com/vuejs/core/issues/3258)) ([8e27692](https://github.com/vuejs/core/commit/8e27692029a4645cd54287f776c0420f2b82740b))
* **ssr:** add `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__` feature flag ([#9550](https://github.com/vuejs/core/issues/9550)) ([bc7698d](https://github.com/vuejs/core/commit/bc7698dbfed9b5327a93565f9df336ae5a94d605))
* **ssr:** improve ssr hydration mismatch checks ([#5953](https://github.com/vuejs/core/issues/5953)) ([2ffc1e8](https://github.com/vuejs/core/commit/2ffc1e8cfdc6ec9c45c4a4dd8e3081b2aa138f1e)), closes [#5063](https://github.com/vuejs/core/issues/5063)
* **types:** add emits and slots type to `FunctionalComponent` ([#8644](https://github.com/vuejs/core/issues/8644)) ([927ab17](https://github.com/vuejs/core/commit/927ab17cfc645e82d061fdf227c34689491268e1))
* **types:** export AriaAttributes type ([#8909](https://github.com/vuejs/core/issues/8909)) ([fd0b6ba](https://github.com/vuejs/core/commit/fd0b6ba01660499fa07b0cf360eefaac8cca8287))
* **types:** export ObjectPlugin and FunctionPlugin types ([#8946](https://github.com/vuejs/core/issues/8946)) ([fa4969e](https://github.com/vuejs/core/commit/fa4969e7a3aefa6863203f9294fc5e769ddf6d8f)), closes [#8577](https://github.com/vuejs/core/issues/8577)
* **types:** expose `DefineProps` type ([096ba81](https://github.com/vuejs/core/commit/096ba81817b7da15f61bc55fc1a93f72ac9586e0))
* **types:** expose `PublicProps` type ([#2403](https://github.com/vuejs/core/issues/2403)) ([44135dc](https://github.com/vuejs/core/commit/44135dc95fb8fea26b84d1433839d28b8c21f708))
* **types:** improve event type inference when using `h` with native elements ([#9756](https://github.com/vuejs/core/issues/9756)) ([a625376](https://github.com/vuejs/core/commit/a625376ac8901eea81bf3c66cb531f2157f073ef))
* **types:** provide ComponentInstance type ([#5408](https://github.com/vuejs/core/issues/5408)) ([bfb8565](https://github.com/vuejs/core/commit/bfb856565d3105db4b18991ae9e404e7cc989b25))
* **types:** support passing generics when registering global directives ([#9660](https://github.com/vuejs/core/issues/9660)) ([a41409e](https://github.com/vuejs/core/commit/a41409ed02a8c7220e637f56caf6813edeb077f8))
### Performance Improvements
* use sync watcher for defineModel local mode ([7e60d10](https://github.com/vuejs/core/commit/7e60d1058ff06e3d37c8608f3449453321220edc)), closes [/github.com/vuejs/rfcs/discussions/503#discussioncomment-7566278](https://github.com//github.com/vuejs/rfcs/discussions/503/issues/discussioncomment-7566278)
## [3.3.11](https://github.com/vuejs/core/compare/v3.3.10...v3.3.11) (2023-12-08)
### Bug Fixes
* **custom-element:** correctly handle number type props in prod ([#8989](https://github.com/vuejs/core/issues/8989)) ([d74d364](https://github.com/vuejs/core/commit/d74d364d62db8e48881af6b5a75ce4fb5f36cc35))
* **reactivity:** fix mutation on user proxy of reactive Array ([6ecbd5c](https://github.com/vuejs/core/commit/6ecbd5ce2a7f59314a8326a1d193874b87f4d8c8)), closes [#9742](https://github.com/vuejs/core/issues/9742) [#9751](https://github.com/vuejs/core/issues/9751) [#9750](https://github.com/vuejs/core/issues/9750)
* **runtime-dom:** fix width and height prop check condition ([5b00286](https://github.com/vuejs/core/commit/5b002869c533220706f9788b496b8ca8d8e98609)), closes [#9762](https://github.com/vuejs/core/issues/9762)
* **shared:** handle Map with symbol keys in toDisplayString ([#9731](https://github.com/vuejs/core/issues/9731)) ([364821d](https://github.com/vuejs/core/commit/364821d6bdb1775e2f55a69bcfb9f40f7acf1506)), closes [#9727](https://github.com/vuejs/core/issues/9727)
* **shared:** handle more Symbol cases in toDisplayString ([983d45d](https://github.com/vuejs/core/commit/983d45d4f8eb766b5a16b7ea93b86d3c51618fa6))
* **Suspense:** properly get anchor when mount fallback vnode ([#9770](https://github.com/vuejs/core/issues/9770)) ([b700328](https://github.com/vuejs/core/commit/b700328342e17dc16b19316c2e134a26107139d2)), closes [#9769](https://github.com/vuejs/core/issues/9769)
* **types:** ref() return type should not be any when initial value is any ([#9768](https://github.com/vuejs/core/issues/9768)) ([cdac121](https://github.com/vuejs/core/commit/cdac12161ec27b45ded48854c3d749664b6d4a6d))
* **watch:** should not fire pre watcher on child component unmount ([#7181](https://github.com/vuejs/core/issues/7181)) ([6784f0b](https://github.com/vuejs/core/commit/6784f0b1f8501746ea70d87d18ed63a62cf6b76d)), closes [#7030](https://github.com/vuejs/core/issues/7030)
# [3.4.0-alpha.4](https://github.com/vuejs/core/compare/v3.3.10...v3.4.0-alpha.4) (2023-12-04)
@ -19,74 +119,6 @@
# [3.4.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.0-alpha.2...v3.4.0-alpha.3) (2023-11-28)
### Bug Fixes
* **parser:** directive arg should be undefined on shorthands with no arg ([e49dffc](https://github.com/vuejs/core/commit/e49dffc9ece86bddf094b9ad4ad15eb4856d6277))
### Features
* **dx:** link errors to docs in prod build ([#9165](https://github.com/vuejs/core/issues/9165)) ([9f8ba98](https://github.com/vuejs/core/commit/9f8ba9821fe166f77e63fa940e9e7e13ec3344fa))
# [3.4.0-alpha.2](https://github.com/vuejs/core/compare/v3.3.9...v3.4.0-alpha.2) (2023-11-27)
### Bug Fixes
* avoid confusing breakage in @vitejs/plugin-vue ([ceec69c](https://github.com/vuejs/core/commit/ceec69c8ccb96c433a4a506ad2e85e276998bade))
* **compiler-core:** fix line/column tracking when fast forwarding ([2e65ea4](https://github.com/vuejs/core/commit/2e65ea481f74db8649df8110a031cbdc98f98c84))
* **compiler-sfc:** fix ast reuse for ssr ([fb619cf](https://github.com/vuejs/core/commit/fb619cf9a440239f0ba88e327d10001a6a3c8171))
* **compiler-sfc:** support `:is` and `:where` selector in scoped css rewrite ([#8929](https://github.com/vuejs/core/issues/8929)) ([c6083dc](https://github.com/vuejs/core/commit/c6083dcad31f3e9292c687fada9e32f287e2317f))
* **compiler-sfc:** use correct compiler when re-parsing in ssr mode ([678378a](https://github.com/vuejs/core/commit/678378afd559481badb486b243722b6287862e09))
* feat!: remove reactivity transform (#9321) ([79b8a09](https://github.com/vuejs/core/commit/79b8a0905bf363bf82edd2096fef10c3db6d9c3c)), closes [#9321](https://github.com/vuejs/core/issues/9321)
### Features
* **compiler-core:** support specifying root namespace when parsing ([40f72d5](https://github.com/vuejs/core/commit/40f72d5e50b389cb11b7ca13461aa2a75ddacdb4))
* **compiler-core:** support v-bind shorthand for key and value with the same name ([#9451](https://github.com/vuejs/core/issues/9451)) ([26399aa](https://github.com/vuejs/core/commit/26399aa6fac1596b294ffeba06bb498d86f5508c))
* **compiler:** improve parsing tolerance for language-tools ([41ff68e](https://github.com/vuejs/core/commit/41ff68ea579d933333392146625560359acb728a))
* **reactivity:** expose last result for computed getter ([#9497](https://github.com/vuejs/core/issues/9497)) ([48b47a1](https://github.com/vuejs/core/commit/48b47a1ab63577e2dbd91947eea544e3ef185b85))
### Performance Improvements
* avoid sfc source map unnecessary serialization and parsing ([f15d2f6](https://github.com/vuejs/core/commit/f15d2f6cf69c0c39f8dfb5c33122790c68bf92e2))
* **codegen:** optimize line / column calculation during codegen ([3be53d9](https://github.com/vuejs/core/commit/3be53d9b974dae1a10eb795cade71ae765e17574))
* **codegen:** optimize source map generation ([c11002f](https://github.com/vuejs/core/commit/c11002f16afd243a2b15b546816e73882eea9e4d))
* **compiler-sfc:** remove magic-string trim on script ([e8e3ec6](https://github.com/vuejs/core/commit/e8e3ec6ca7392e43975c75b56eaaa711d5ea9410))
* **compiler-sfc:** use faster source map addMapping ([50cde7c](https://github.com/vuejs/core/commit/50cde7cfbcc49022ba88f5f69fa9b930b483c282))
* optimize away isBuiltInType ([66c0ed0](https://github.com/vuejs/core/commit/66c0ed0a3c1c6f37dafc6b1c52b75c6bf60e3136))
* optimize makeMap ([ae6fba9](https://github.com/vuejs/core/commit/ae6fba94954bac6430902f77b0d1113a98a75b18))
* optimize position cloning ([2073236](https://github.com/vuejs/core/commit/20732366b9b3530d33b842cf1fc985919afb9317))
### BREAKING CHANGES
* Reactivity Transform was marked deprecated in 3.3 and is now removed in 3.4. This change does not require a major due to the feature being experimental. Users who wish to continue using the feature can do so via the external plugin at https://vue-macros.dev/features/reactivity-transform.html
# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28)
### Features
* **compiler-core:** export error message ([#8729](https://github.com/vuejs/core/issues/8729)) ([f7e80ee](https://github.com/vuejs/core/commit/f7e80ee4a065a9eaba98720abf415d9e87756cbd))
* **compiler-sfc:** expose resolve type-based props and emits ([#8874](https://github.com/vuejs/core/issues/8874)) ([9e77580](https://github.com/vuejs/core/commit/9e77580c0c2f0d977bd0031a1d43cc334769d433))
* export runtime error strings ([#9301](https://github.com/vuejs/core/issues/9301)) ([feb2f2e](https://github.com/vuejs/core/commit/feb2f2edce2d91218a5e9a52c81e322e4033296b))
* **reactivity:** more efficient reactivity system ([#5912](https://github.com/vuejs/core/issues/5912)) ([16e06ca](https://github.com/vuejs/core/commit/16e06ca08f5a1e2af3fc7fb35de153dbe0c3087d)), closes [#311](https://github.com/vuejs/core/issues/311) [#1811](https://github.com/vuejs/core/issues/1811) [#6018](https://github.com/vuejs/core/issues/6018) [#7160](https://github.com/vuejs/core/issues/7160) [#8714](https://github.com/vuejs/core/issues/8714) [#9149](https://github.com/vuejs/core/issues/9149) [#9419](https://github.com/vuejs/core/issues/9419) [#9464](https://github.com/vuejs/core/issues/9464)
* **runtime-core:** add `once` option to watch ([#9034](https://github.com/vuejs/core/issues/9034)) ([a645e7a](https://github.com/vuejs/core/commit/a645e7aa51006516ba668b3a4365d296eb92ee7d))
## [3.3.10](https://github.com/vuejs/core/compare/v3.3.9...v3.3.10) (2023-12-04)

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "0.0.0-vapor",
"packageManager": "pnpm@8.11.0",
"packageManager": "pnpm@8.12.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js vue vue-vapor",
@ -22,6 +22,7 @@
"test-dts": "run-s build-dts test-dts-only",
"test-dts-only": "tsc -p ./packages/dts-test/tsconfig.test.json",
"test-coverage": "vitest -c vitest.unit.config.ts --coverage",
"test-bench": "vitest bench",
"release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
@ -61,6 +62,7 @@
"devDependencies": {
"@babel/parser": "^7.23.5",
"@babel/types": "^7.23.5",
"@codspeed/vitest-plugin": "^2.3.1",
"@rollup/plugin-alias": "^5.0.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.0.1",
@ -69,33 +71,33 @@
"@rollup/plugin-terser": "^0.4.4",
"@types/hash-sum": "^1.0.2",
"@types/minimist": "^1.2.5",
"@types/node": "^20.10.3",
"@types/node": "^20.10.4",
"@types/semver": "^7.5.5",
"@typescript-eslint/parser": "^6.13.0",
"@vitest/coverage-istanbul": "^0.34.6",
"@typescript-eslint/parser": "^6.13.2",
"@vitest/coverage-istanbul": "^1.0.4",
"@vue/consolidate": "0.17.3",
"conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.4.1",
"esbuild": "^0.19.5",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^8.54.0",
"eslint": "^8.55.0",
"eslint-define-config": "^1.24.1",
"eslint-plugin-jest": "^27.6.0",
"estree-walker": "^2.0.2",
"execa": "^8.0.1",
"jsdom": "^22.1.0",
"lint-staged": "^15.1.0",
"jsdom": "^23.0.1",
"lint-staged": "^15.2.0",
"lodash": "^4.17.21",
"magic-string": "^0.30.5",
"markdown-table": "^3.0.3",
"marked": "^9.1.6",
"marked": "^11.0.1",
"minimist": "^1.2.8",
"npm-run-all": "^4.1.5",
"picocolors": "^1.0.0",
"prettier": "^3.1.0",
"prettier": "^3.1.1",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.2",
"puppeteer": "~21.5.2",
"puppeteer": "~21.6.0",
"rimraf": "^5.0.5",
"rollup": "^4.1.4",
"rollup-plugin-dts": "^6.1.0",
@ -109,7 +111,7 @@
"tslib": "^2.6.2",
"tsx": "^4.6.2",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vitest": "^1.0.0"
"vite": "^5.0.5",
"vitest": "^1.0.4"
}
}

View File

@ -201,6 +201,26 @@ describe('compiler: transform', () => {
expect((ast as any).children[1].props[0].exp.content).toBe(`_hoisted_2`)
})
test('context.filename and selfName', () => {
const ast = baseParse(`<div />`)
const calls: any[] = []
const plugin: NodeTransform = (node, context) => {
calls.push({ ...context })
}
transform(ast, {
filename: '/the/fileName.vue',
nodeTransforms: [plugin]
})
expect(calls.length).toBe(2)
expect(calls[1]).toMatchObject({
filename: '/the/fileName.vue',
selfName: 'FileName'
})
})
test('onError option', () => {
const ast = baseParse(`<div/>`)
const loc = ast.children[0].loc

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
"version": "3.4.0-alpha.4",
"version": "3.4.0-beta.3",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",

View File

@ -441,3 +441,11 @@ export const TS_NODE_TYPES = [
'TSInstantiationExpression', // foo<string>
'TSSatisfiesExpression' // foo satisfies T
]
export function unwrapTSNode(node: Node): Node {
if (TS_NODE_TYPES.includes(node.type)) {
return unwrapTSNode((node as any).expression)
} else {
return node
}
}

View File

@ -90,13 +90,13 @@ export enum ErrorCodes {
X_V_MODEL_ON_PROPS,
X_INVALID_EXPRESSION,
X_KEEP_ALIVE_INVALID_CHILDREN,
X_VNODE_HOOKS,
// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
X_MODULE_MODE_NOT_SUPPORTED,
X_CACHE_HANDLER_NOT_SUPPORTED,
X_SCOPE_ID_NOT_SUPPORTED,
X_VNODE_HOOKS,
// Special value for higher-order compilers to pick up the last code
// to avoid collision of error codes. This should always be kept as the last
@ -173,7 +173,7 @@ export const errorMessages: Record<ErrorCodes, string> = {
[ErrorCodes.X_V_MODEL_ON_PROPS]: `v-model cannot be used on a prop, because local prop bindings are not writable.\nUse a v-bind binding combined with a v-on listener that emits update:x event instead.`,
[ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> expects exactly one child component.`,
[ErrorCodes.X_VNODE_HOOKS]: `@vnode-* hooks in templates are deprecated. Use the vue: prefix instead. For example, @vnode-mounted should be changed to @vue:mounted. @vnode-* hooks support will be removed in 3.4.`,
[ErrorCodes.X_VNODE_HOOKS]: `@vnode-* hooks in templates are no longer supported. Use the vue: prefix instead. For example, @vnode-mounted should be changed to @vue:mounted. @vnode-* hooks support has been removed in 3.4.`,
// generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,

View File

@ -83,9 +83,7 @@ export interface ImportItem {
}
export interface TransformContext
extends Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
>,
extends Required<Omit<TransformOptions, keyof CompilerCompatOptions>>,
CompilerCompatOptions {
selfName: string | null
root: RootNode
@ -153,6 +151,7 @@ export function createTransformContext(
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
const context: TransformContext = {
// options
filename,
selfName: nameMatch && capitalize(camelize(nameMatch[1])),
prefixIdentifiers,
hoistStatic,

View File

@ -40,6 +40,7 @@ import { isString, isObject, NOOP } from '@vue/shared'
import { PropsExpression } from './transforms/transformElement'
import { parseExpression } from '@babel/parser'
import { Expression } from '@babel/types'
import { unwrapTSNode } from './babelUtils'
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
@ -164,9 +165,7 @@ export const isMemberExpressionNode = __BROWSER__
let ret: Expression = parseExpression(path, {
plugins: options.expressionPlugins
})
if (ret.type === 'TSAsExpression' || ret.type === 'TSTypeAssertion') {
ret = ret.expression
}
ret = unwrapTSNode(ret) as Expression
return (
ret.type === 'MemberExpression' ||
ret.type === 'OptionalMemberExpression' ||

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
"version": "3.4.0-alpha.4",
"version": "3.4.0-beta.3",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",

View File

@ -1,12 +1,12 @@
import { ParserOptions, NodeTypes, Namespaces } from '@vue/compiler-core'
import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { isVoidTag, isHTMLTag, isSVGTag, isMathMLTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtmlBrowser } from './decodeHtmlBrowser'
export const parserOptions: ParserOptions = {
parseMode: 'html',
isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag) || isMathMLTag(tag),
isPreTag: tag => tag === 'pre',
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,

View File

@ -1483,3 +1483,31 @@ _sfc_.setup = __setup__
: __injectCSSVars__
"
`;
exports[`SFC genDefaultAs > parser plugins > import attributes (user override for deprecated syntax) 1`] = `
"import { foo } from './foo.js' assert { type: 'foobar' }
export default {
setup(__props, { expose: __expose }) {
__expose();
return { get foo() { return foo } }
}
}"
`;
exports[`SFC genDefaultAs > parser plugins > import attributes 1`] = `
"import { foo } from './foo.js' with { type: 'foobar' }
export default {
setup(__props, { expose: __expose }) {
__expose();
return { get foo() { return foo } }
}
}"
`;

View File

@ -1600,4 +1600,38 @@ describe('SFC genDefaultAs', () => {
foo: BindingTypes.SETUP_REF
})
})
describe('parser plugins', () => {
test('import attributes', () => {
const { content } = compile(`
<script setup>
import { foo } from './foo.js' with { type: 'foobar' }
</script>
`)
assertCode(content)
expect(() =>
compile(`
<script setup>
import { foo } from './foo.js' assert { type: 'foobar' }
</script>`)
).toThrow()
})
test('import attributes (user override for deprecated syntax)', () => {
const { content } = compile(
`
<script setup>
import { foo } from './foo.js' assert { type: 'foobar' }
</script>
`,
{
babelParserPlugins: [
['importAttributes', { deprecatedAssertSyntax: true }]
]
}
)
assertCode(content)
})
})
})

View File

@ -18,6 +18,47 @@ return { props, bar }
}"
`;
exports[`defineProps > custom element retains the props type & default value & production mode 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
interface Props {
foo?: number;
}
export default /*#__PURE__*/_defineComponent({
__name: 'app.ce',
props: {
foo: { default: 5.5, type: Number }
},
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
return { props }
}
})"
`;
exports[`defineProps > custom element retains the props type & production mode 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
__name: 'app.ce',
props: {
foo: {type: Number}
},
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props
return { props }
}
})"
`;
exports[`defineProps > defineProps w/ runtime options 1`] = `
"import { defineComponent as _defineComponent } from 'vue'

View File

@ -10,8 +10,7 @@ describe('defineModel()', () => {
const c = defineModel('count')
const toString = defineModel('toString', { type: Function })
</script>
`,
{ defineModel: true }
`
)
assertCode(content)
expect(content).toMatch('props: {')
@ -44,8 +43,7 @@ describe('defineModel()', () => {
defineEmits(['change'])
const count = defineModel({ default: 0 })
</script>
`,
{ defineModel: true }
`
)
assertCode(content)
expect(content).toMatch(`props: /*#__PURE__*/_mergeModels({ foo: String }`)
@ -66,8 +64,7 @@ describe('defineModel()', () => {
defineProps(['foo', 'bar'])
const count = defineModel('count')
</script>
`,
{ defineModel: true }
`
)
assertCode(content)
expect(content).toMatch(`props: /*#__PURE__*/_mergeModels(['foo', 'bar'], {
@ -94,8 +91,7 @@ describe('defineModel()', () => {
const local = true
const hoist = defineModel('hoist', { local })
</script>`,
{ defineModel: true }
</script>`
)
assertCode(content)
expect(content).toMatch(`_useModel(__props, "modelValue", { local: true })`)
@ -115,8 +111,7 @@ describe('defineModel()', () => {
const disabled = defineModel<number>('disabled', { required: false })
const any = defineModel<any | boolean>('any')
</script>
`,
{ defineModel: true }
`
)
assertCode(content)
expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
@ -155,7 +150,7 @@ describe('defineModel()', () => {
const optional = defineModel<string>('optional', { required: false })
</script>
`,
{ defineModel: true, isProd: true }
{ isProd: true }
)
assertCode(content)
expect(content).toMatch('"modelValue": { type: Boolean }')

View File

@ -710,4 +710,35 @@ const props = defineProps({ foo: String })
'da-sh': BindingTypes.PROPS
})
})
// #8989
test('custom element retains the props type & production mode', () => {
const { content } = compile(
`<script setup lang="ts">
const props = defineProps<{ foo: number}>()
</script>`,
{ isProd: true, customElement: filename => /\.ce\.vue$/.test(filename) },
{ filename: 'app.ce.vue' }
)
expect(content).toMatch(`foo: {type: Number}`)
assertCode(content)
})
test('custom element retains the props type & default value & production mode', () => {
const { content } = compile(
`<script setup lang="ts">
interface Props {
foo?: number;
}
const props = withDefaults(defineProps<Props>(), {
foo: 5.5,
});
</script>`,
{ isProd: true, customElement: filename => /\.ce\.vue$/.test(filename) },
{ filename: 'app.ce.vue' }
)
expect(content).toMatch(`foo: { default: 5.5, type: Number }`)
assertCode(content)
})
})

View File

@ -28,7 +28,10 @@ export function assertCode(code: string) {
try {
babelParse(code, {
sourceType: 'module',
plugins: ['typescript']
plugins: [
'typescript',
['importAttributes', { deprecatedAssertSyntax: true }]
]
})
} catch (e: any) {
console.log(code)

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.4.0-alpha.4",
"version": "3.4.0-beta.3",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",

View File

@ -2,7 +2,8 @@ import {
BindingTypes,
UNREF,
isFunctionType,
walkIdentifiers
walkIdentifiers,
unwrapTSNode
} from '@vue/compiler-dom'
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
import { ParserPlugin } from '@babel/parser'
@ -43,12 +44,7 @@ import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose'
import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
import { processDefineSlots } from './script/defineSlots'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
import {
isLiteralNode,
unwrapTSNode,
isCallOf,
getImportedName
} from './script/utils'
import { isLiteralNode, isCallOf, getImportedName } from './script/utils'
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait'
@ -102,11 +98,6 @@ export interface SFCScriptCompileOptions {
* @default true
*/
hoistStatic?: boolean
/**
* (**Experimental**) Enable macro `defineModel`
* @default false
*/
defineModel?: boolean
/**
* (**Experimental**) Enable reactive destructure for `defineProps`
* @default false
@ -121,6 +112,10 @@ export interface SFCScriptCompileOptions {
fileExists(file: string): boolean
readFile(file: string): string | undefined
}
/**
* Transform Vue SFCs into custom elements.
*/
customElement?: boolean | ((filename: string) => boolean)
}
export interface ImportBinding {

View File

@ -2,6 +2,7 @@ import { parse } from '@babel/parser'
import MagicString from 'magic-string'
import type { ParserPlugin } from '@babel/parser'
import type { Identifier, Statement } from '@babel/types'
import { resolveParserPlugins } from './script/context'
export function rewriteDefault(
input: string,
@ -10,7 +11,7 @@ export function rewriteDefault(
): string {
const ast = parse(input, {
sourceType: 'module',
plugins: parserPlugins
plugins: resolveParserPlugins('js', parserPlugins)
}).program.body
const s = new MagicString(input)

View File

@ -1,6 +1,6 @@
import { CallExpression, Node, ObjectPattern, Program } from '@babel/types'
import { SFCDescriptor } from '../parse'
import { generateCodeFrame } from '@vue/shared'
import { generateCodeFrame, isArray } from '@vue/shared'
import { parse as babelParse, ParserPlugin } from '@babel/parser'
import { ImportBinding, SFCScriptCompileOptions } from '../compileScript'
import { PropsDestructureBindings } from './defineProps'
@ -12,6 +12,7 @@ import { TypeScope } from './resolveType'
export class ScriptCompileContext {
isJS: boolean
isTS: boolean
isCE = false
scriptAst: Program | null
scriptSetupAst: Program | null
@ -95,6 +96,14 @@ export class ScriptCompileContext {
scriptSetupLang === 'ts' ||
scriptSetupLang === 'tsx'
const customElement = options.customElement
const filename = this.descriptor.filename
if (customElement) {
this.isCE =
typeof customElement === 'boolean'
? customElement
: customElement(filename)
}
// resolve parser plugins
const plugins: ParserPlugin[] = resolveParserPlugins(
(scriptLang || scriptSetupLang)!,
@ -155,6 +164,17 @@ export function resolveParserPlugins(
dts = false
) {
const plugins: ParserPlugin[] = []
if (
!userPlugins ||
!userPlugins.some(
p =>
p === 'importAssertions' ||
p === 'importAttributes' ||
(isArray(p) && p[0] === 'importAttributes')
)
) {
plugins.push('importAttributes')
}
if (lang === 'jsx' || lang === 'tsx') {
plugins.push('jsx')
} else if (userPlugins) {
@ -163,7 +183,7 @@ export function resolveParserPlugins(
userPlugins = userPlugins.filter(p => p !== 'jsx')
}
if (lang === 'ts' || lang === 'tsx') {
plugins.push(['typescript', { dts }])
plugins.push(['typescript', { dts }], 'explicitResourceManagement')
if (!userPlugins || !userPlugins.includes('decorators')) {
plugins.push('decorators-legacy')
}

View File

@ -5,11 +5,9 @@ import {
UNKNOWN_TYPE,
concatStrings,
isCallOf,
toRuntimeTypeString,
unwrapTSNode
toRuntimeTypeString
} from './utils'
import { BindingTypes } from '@vue/compiler-dom'
import { warnOnce } from '../warn'
import { BindingTypes, unwrapTSNode } from '@vue/compiler-dom'
export const DEFINE_MODEL = 'defineModel'
@ -28,21 +26,6 @@ export function processDefineModel(
return false
}
if (!ctx.options.defineModel) {
warnOnce(
`defineModel() is an experimental feature and disabled by default.\n` +
`To enable it, follow the RFC at https://github.com/vuejs/rfcs/discussions/503.`
)
return false
}
warnOnce(
`This project is using defineModel(), which is an experimental ` +
`feature. It may receive breaking changes or be removed in the future, so ` +
`use at your own risk.\n` +
`To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/503.`
)
ctx.hasDefineModelCall = true
const type =

View File

@ -1,6 +1,7 @@
import { Node } from '@babel/types'
import { unwrapTSNode } from '@vue/compiler-dom'
import { ScriptCompileContext } from './context'
import { isCallOf, unwrapTSNode } from './utils'
import { isCallOf } from './utils'
import { DEFINE_PROPS } from './defineProps'
import { DEFINE_EMITS } from './defineEmits'
import { DEFINE_EXPOSE } from './defineExpose'

View File

@ -6,7 +6,7 @@ import {
ObjectExpression,
Expression
} from '@babel/types'
import { BindingTypes, isFunctionType } from '@vue/compiler-dom'
import { BindingTypes, isFunctionType, unwrapTSNode } from '@vue/compiler-dom'
import { ScriptCompileContext } from './context'
import {
TypeResolveContext,
@ -19,7 +19,6 @@ import {
concatStrings,
isLiteralNode,
isCallOf,
unwrapTSNode,
toRuntimeTypeString,
getEscapedPropName
} from './utils'
@ -282,6 +281,17 @@ function genRuntimePropFromType(
defaultString
])} }`
} else {
// #8989 for custom element, should keep the type
if (ctx.isCE) {
if (defaultString) {
return `${finalKey}: ${`{ ${defaultString}, type: ${toRuntimeTypeString(
type
)} }`}`
} else {
return `${finalKey}: {type: ${toRuntimeTypeString(type)}}`
}
}
// production: checks are useless
return `${finalKey}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
}

View File

@ -15,10 +15,11 @@ import {
isInDestructureAssignment,
isReferencedIdentifier,
isStaticProperty,
walkFunctionParams
walkFunctionParams,
unwrapTSNode
} from '@vue/compiler-dom'
import { genPropsAccessExp } from '@vue/shared'
import { isCallOf, resolveObjectKey, unwrapTSNode } from './utils'
import { isCallOf, resolveObjectKey } from './utils'
import { ScriptCompileContext } from './context'
import { DEFINE_PROPS } from './defineProps'
import { warnOnce } from '../warn'

View File

@ -83,6 +83,9 @@ export type SimpleTypeResolveContext = Pick<
// emits
| 'emitsTypeDecl'
// customElement
| 'isCE'
> &
Partial<
Pick<ScriptCompileContext, 'scope' | 'globalScopes' | 'deps' | 'fs'>
@ -1475,6 +1478,7 @@ export function inferRuntimeType(
scope
)
}
break
case 'TSMethodSignature':
case 'TSFunctionType':
return ['Function']

View File

@ -9,7 +9,6 @@ import {
StringLiteral
} from '@babel/types'
import path from 'path'
import { TS_NODE_TYPES } from '@vue/compiler-dom'
export const UNKNOWN_TYPE = 'Unknown'
@ -32,14 +31,6 @@ export function isLiteralNode(node: Node) {
return node.type.endsWith('Literal')
}
export function unwrapTSNode(node: Node): Node {
if (TS_NODE_TYPES.includes(node.type)) {
return unwrapTSNode((node as any).expression)
} else {
return node
}
}
export function isCallOf(
node: Node | null | undefined,
test: string | ((id: string) => boolean) | null | undefined

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
"version": "3.4.0-alpha.4",
"version": "3.4.0-beta.3",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",

View File

@ -0,0 +1,14 @@
import { createApp } from 'vue'
import { expectType } from './utils'
const app = createApp({})
app.directive<HTMLElement, string>('custom', {
mounted(el, binding) {
expectType<HTMLElement>(el)
expectType<string>(binding.value)
// @ts-expect-error not any
expectType<number>(binding.value)
}
})

View File

@ -8,14 +8,22 @@ import {
FunctionalComponent,
ComponentPublicInstance,
toRefs,
SetupContext
SetupContext,
EmitsOptions
} from 'vue'
import { describe, expectAssignable, expectType, IsAny } from './utils'
declare function extractComponentOptions<Props, RawBindings>(
obj: Component<Props, RawBindings>
declare function extractComponentOptions<
Props,
RawBindings,
Emits extends EmitsOptions | Record<string, any[]>,
Slots extends Record<string, any>
>(
obj: Component<Props, RawBindings, any, any, any, Emits, Slots>
): {
props: Props
emits: Emits
slots: Slots
rawBindings: RawBindings
setup: ShallowUnwrapRef<RawBindings>
}
@ -455,11 +463,27 @@ describe('functional', () => {
})
describe('typed', () => {
const MyComponent: FunctionalComponent<{ foo: number }> = (_, _2) => {}
type Props = { foo: number }
type Emits = { change: [value: string]; inc: [value: number] }
type Slots = { default: (scope: { foo: string }) => any }
const { props } = extractComponentOptions(MyComponent)
const MyComponent: FunctionalComponent<Props, Emits, Slots> = (
props,
{ emit, slots }
) => {
expectType<Props>(props)
expectType<{
(event: 'change', value: string): void
(event: 'inc', value: number): void
}>(emit)
expectType<Slots>(slots)
}
expectType<number>(props.foo)
const { props, emits, slots } = extractComponentOptions(MyComponent)
expectType<Props>(props)
expectType<Emits>(emits)
expectType<Slots>(slots)
})
})
@ -481,4 +505,18 @@ describe('SetupContext', () => {
expectAssignable<SetupContext<{ b: () => true }>>(wider)
})
describe('short emits', () => {
const {
emit
}: SetupContext<{
a: [val: string]
b: [val: number]
}> = {} as any
expectType<{
(event: 'a', val: string): void
(event: 'b', val: number): void
}>(emit)
})
})

View File

@ -0,0 +1,139 @@
import {
defineComponent,
FunctionalComponent,
ComponentPublicInstance,
ComponentInstance,
ref
} from 'vue'
import { expectType, describe } from './utils'
describe('defineComponent', () => {
const CompSetup = defineComponent({
props: {
test: String
},
setup() {
return {
a: 1
}
}
})
const compSetup: ComponentInstance<typeof CompSetup> = {} as any
expectType<string | undefined>(compSetup.test)
expectType<number>(compSetup.a)
expectType<ComponentPublicInstance>(compSetup)
})
describe('functional component', () => {
// Functional
const CompFunctional: FunctionalComponent<{ test?: string }> = {} as any
const compFunctional: ComponentInstance<typeof CompFunctional> = {} as any
expectType<string | undefined>(compFunctional.test)
expectType<ComponentPublicInstance>(compFunctional)
const CompFunction: (props: { test?: string }) => any = {} as any
const compFunction: ComponentInstance<typeof CompFunction> = {} as any
expectType<string | undefined>(compFunction.test)
expectType<ComponentPublicInstance>(compFunction)
})
describe('options component', () => {
// Options
const CompOptions = defineComponent({
props: {
test: String
},
data() {
return {
a: 1
}
},
computed: {
b() {
return 'test'
}
},
methods: {
func(a: string) {
return true
}
}
})
const compOptions: ComponentInstance<typeof CompOptions> = {} as any
expectType<string | undefined>(compOptions.test)
expectType<number>(compOptions.a)
expectType<(a: string) => boolean>(compOptions.func)
expectType<ComponentPublicInstance>(compOptions)
})
describe('object no defineComponent', () => {
// object - no defineComponent
const CompObjectSetup = {
props: {
test: String
},
setup() {
return {
a: 1
}
}
}
const compObjectSetup: ComponentInstance<typeof CompObjectSetup> = {} as any
expectType<string | undefined>(compObjectSetup.test)
expectType<number>(compObjectSetup.a)
expectType<ComponentPublicInstance>(compObjectSetup)
const CompObjectData = {
props: {
test: String
},
data() {
return {
a: 1
}
}
}
const compObjectData: ComponentInstance<typeof CompObjectData> = {} as any
expectType<string | undefined>(compObjectData.test)
expectType<number>(compObjectData.a)
expectType<ComponentPublicInstance>(compObjectData)
const CompObjectNoProps = {
data() {
return {
a: 1
}
}
}
const compObjectNoProps: ComponentInstance<typeof CompObjectNoProps> =
{} as any
expectType<string | undefined>(compObjectNoProps.test)
expectType<number>(compObjectNoProps.a)
expectType<ComponentPublicInstance>(compObjectNoProps)
})
describe('Generic component', () => {
const Comp = defineComponent(
// TODO: babel plugin to auto infer runtime props options from type
// similar to defineProps<{...}>()
<T extends string | number>(props: { msg: T; list: T[] }) => {
// use Composition API here like in <script setup>
const count = ref(0)
return () => (
// return a render function (both JSX and h() works)
<div>
{props.msg} {count.value}
</div>
)
}
)
// defaults to known types since types are resolved on instantiation
const comp: ComponentInstance<typeof Comp> = {} as any
expectType<string | number>(comp.msg)
expectType<Array<string | number>>(comp.list)
})

View File

@ -9,7 +9,7 @@ import {
Component,
resolveComponent
} from 'vue'
import { describe, expectAssignable } from './utils'
import { describe, expectAssignable, expectType } from './utils'
describe('h inference w/ element', () => {
// key
@ -32,6 +32,17 @@ describe('h inference w/ element', () => {
// slots
const slots = { default: () => {} } // RawSlots
h('div', {}, slots)
// events
h('div', {
onClick: e => {
expectType<MouseEvent>(e)
}
})
h('input', {
onFocus(e) {
expectType<FocusEvent>(e)
}
})
})
describe('h inference w/ Fragment', () => {

View File

@ -18,7 +18,7 @@ import {
computed,
ShallowRef
} from 'vue'
import { expectType, describe, IsUnion } from './utils'
import { expectType, describe, IsUnion, IsAny } from './utils'
function plainType(arg: number | Ref<number>) {
// ref coercing
@ -79,6 +79,10 @@ function plainType(arg: number | Ref<number>) {
// should still unwrap in objects nested in arrays
const arr2 = ref([{ a: ref(1) }]).value
expectType<number>(arr2[0].a)
// any value should return Ref<any>, not any
const a = ref(1 as any)
expectType<IsAny<typeof a>>(false)
}
plainType(1)
@ -159,6 +163,17 @@ const state = reactive({
expectType<string>(state.foo.label)
describe('ref with generic', <T extends { name: string }>() => {
const r = {} as T
const s = ref(r)
expectType<string>(s.value.name)
const rr = {} as MaybeRef<T>
// should at least allow casting
const ss = ref(rr) as Ref<T>
expectType<string>(ss.value.name)
})
// shallowRef
type Status = 'initial' | 'ready' | 'invalidating'
const shallowStatus = shallowRef<Status>('initial')
@ -191,11 +206,34 @@ if (refStatus.value === 'initial') {
expectType<IsUnion<typeof shallowUnionAsCast>>(false)
}
describe('shallowRef with generic', <T>() => {
const r = ref({}) as MaybeRef<T>
expectType<ShallowRef<T> | Ref<T>>(shallowRef(r))
{
// any value should return Ref<any>, not any
const a = shallowRef(1 as any)
expectType<IsAny<typeof a>>(false)
}
describe('shallowRef with generic', <T extends { name: string }>() => {
const r = {} as T
const s = shallowRef(r)
expectType<string>(s.value.name)
expectType<ShallowRef<T>>(shallowRef(r))
const rr = {} as MaybeRef<T>
// should at least allow casting
const ss = shallowRef(rr) as Ref<T> | ShallowRef<T>
expectType<string>(ss.value.name)
})
{
// should return ShallowRef<T> | Ref<T>, not ShallowRef<T | Ref<T>>
expectType<ShallowRef<{ name: string }> | Ref<{ name: string }>>(
shallowRef({} as MaybeRef<{ name: string }>)
)
expectType<ShallowRef<number> | Ref<string[]> | ShallowRef<string>>(
shallowRef('' as Ref<string[]> | string | number)
)
}
// proxyRefs: should return `reactive` directly
const r1 = reactive({
k: 'v'

View File

@ -260,6 +260,30 @@ describe('defineSlots', () => {
expectType<Slots>(slotsUntype)
})
describe('defineSlots generic', <T extends Record<string, any>>() => {
const props = defineProps<{
item: T
}>()
const slots = defineSlots<
{
[K in keyof T as `slot-${K & string}`]?: (props: { item: T }) => any
} & {
label?: (props: { item: T }) => any
}
>()
for (const key of Object.keys(props.item) as (keyof T & string)[]) {
slots[`slot-${String(key)}`]?.({
item: props.item
})
}
slots.label?.({ item: props.item })
// @ts-expect-error calling wrong slot
slots.foo({})
})
describe('defineModel', () => {
// overload 1
const modelValueRequired = defineModel<boolean>({ required: true })
@ -294,10 +318,6 @@ describe('defineModel', () => {
defineModel<string>({ default: 123 })
// @ts-expect-error unknown props option
defineModel({ foo: 123 })
// accept defineModel-only options
defineModel({ local: true })
defineModel('foo', { local: true })
})
describe('useModel', () => {
@ -336,6 +356,78 @@ describe('useSlots', () => {
expectType<Slots>(slots)
})
describe('defineSlots generic', <T extends Record<string, any>>() => {
const props = defineProps<{
item: T
}>()
const slots = defineSlots<
{
[K in keyof T as `slot-${K & string}`]?: (props: { item: T }) => any
} & {
label?: (props: { item: T }) => any
}
>()
// @ts-expect-error slots should be readonly
slots.label = () => {}
// @ts-expect-error non existing slot
slots['foo-asdas']?.({
item: props.item
})
for (const key in props.item) {
slots[`slot-${String(key)}`]?.({
item: props.item
})
slots[`slot-${String(key as keyof T)}`]?.({
item: props.item
})
}
for (const key of Object.keys(props.item) as (keyof T)[]) {
slots[`slot-${String(key)}`]?.({
item: props.item
})
}
slots.label?.({ item: props.item })
// @ts-expect-error calling wrong slot
slots.foo({})
})
describe('defineSlots generic strict', <T extends {
foo: 'foo'
bar: 'bar'
}>() => {
const props = defineProps<{
item: T
}>()
const slots = defineSlots<
{
[K in keyof T as `slot-${K & string}`]?: (props: { item: T }) => any
} & {
label?: (props: { item: T }) => any
}
>()
// slot-bar/foo should be automatically inferred
slots['slot-bar']?.({ item: props.item })
slots['slot-foo']?.({ item: props.item })
slots.label?.({ item: props.item })
// @ts-expect-error not part of the extends
slots['slot-RANDOM']?.({ item: props.item })
// @ts-expect-error slots should be readonly
slots.label = () => {}
// @ts-expect-error calling wrong slot
slots.foo({})
})
// #6420
describe('toRefs w/ type declaration', () => {
const props = defineProps<{

View File

@ -112,3 +112,11 @@ expectType<JSX.Element>(
)
// @ts-expect-error
;<Suspense onResolve={123} />
// svg
expectType<JSX.Element>(
<svg
xmlnsXlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
/>
)

View File

@ -17,6 +17,7 @@ declare var __COMPAT__: boolean
declare var __FEATURE_OPTIONS_API__: boolean
declare var __FEATURE_PROD_DEVTOOLS__: boolean
declare var __FEATURE_SUSPENSE__: boolean
declare var __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: boolean
// for tests
declare namespace jest {

View File

@ -0,0 +1,126 @@
import { describe, bench } from 'vitest'
import { ComputedRef, Ref, computed, ref } from '../src/index'
describe('computed', () => {
bench('create computed', () => {
computed(() => 100)
})
{
let i = 0
const o = ref(100)
bench('write independent ref dep', () => {
o.value = i++
})
}
{
const v = ref(100)
computed(() => v.value * 2)
let i = 0
bench("write ref, don't read computed (never invoked)", () => {
v.value = i++
})
}
{
const v = ref(100)
computed(() => {
return v.value * 2
})
let i = 0
bench("write ref, don't read computed (never invoked)", () => {
v.value = i++
})
}
{
const v = ref(100)
const c = computed(() => {
return v.value * 2
})
c.value
let i = 0
bench("write ref, don't read computed (invoked)", () => {
v.value = i++
})
}
{
const v = ref(100)
const c = computed(() => {
return v.value * 2
})
let i = 0
bench('write ref, read computed', () => {
v.value = i++
c.value
})
}
{
const v = ref(100)
const computeds = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return v.value * 2
})
computeds.push(c)
}
let i = 0
bench("write ref, don't read 1000 computeds (never invoked)", () => {
v.value = i++
})
}
{
const v = ref(100)
const computeds = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return v.value * 2
})
c.value
computeds.push(c)
}
let i = 0
bench("write ref, don't read 1000 computeds (invoked)", () => {
v.value = i++
})
}
{
const v = ref(100)
const computeds: ComputedRef<number>[] = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return v.value * 2
})
c.value
computeds.push(c)
}
let i = 0
bench('write ref, read 1000 computeds', () => {
v.value = i++
computeds.forEach(c => c.value)
})
}
{
const refs: Ref<number>[] = []
for (let i = 0, n = 1000; i < n; i++) {
refs.push(ref(i))
}
const c = computed(() => {
let total = 0
refs.forEach(ref => (total += ref.value))
return total
})
let i = 0
const n = refs.length
bench('1000 refs, 1 computed', () => {
refs[i++ % n].value++
c.value
})
}
})

View File

@ -158,6 +158,21 @@ describe('reactivity/reactive', () => {
expect(original.bar).toBe(original2)
})
// #1246
test('mutation on objects using reactive as prototype should not trigger', () => {
const observed = reactive({ foo: 1 })
const original = Object.create(observed)
let dummy
effect(() => (dummy = original.foo))
expect(dummy).toBe(1)
observed.foo = 2
expect(dummy).toBe(2)
original.foo = 3
expect(dummy).toBe(2)
original.foo = 4
expect(dummy).toBe(2)
})
test('toRaw', () => {
const original = { foo: 1 }
const observed = reactive(original)
@ -166,11 +181,18 @@ describe('reactivity/reactive', () => {
})
test('toRaw on object using reactive as prototype', () => {
const original = reactive({})
const obj = Object.create(original)
const original = { foo: 1 }
const observed = reactive(original)
const inherted = Object.create(observed)
expect(toRaw(inherted)).toBe(inherted)
})
test('toRaw on user Proxy wrapping reactive', () => {
const original = {}
const re = reactive(original)
const obj = new Proxy(re, {})
const raw = toRaw(obj)
expect(raw).toBe(obj)
expect(raw).not.toBe(toRaw(original))
expect(raw).toBe(original)
})
test('should not unwrap Ref<T>', () => {

View File

@ -0,0 +1,92 @@
import { bench } from 'vitest'
import { computed, reactive, readonly, shallowRef, triggerRef } from '../src'
for (let amount = 1e1; amount < 1e4; amount *= 10) {
{
const rawArray = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
const r = reactive(rawArray)
const c = computed(() => {
return r.reduce((v, a) => a + v, 0)
})
bench(`reduce *reactive* array, ${amount} elements`, () => {
for (let i = 0, n = r.length; i < n; i++) {
r[i]++
}
c.value
})
}
{
const rawArray = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
const r = reactive(rawArray)
const c = computed(() => {
return r.reduce((v, a) => a + v, 0)
})
bench(
`reduce *reactive* array, ${amount} elements, only change first value`,
() => {
r[0]++
c.value
}
)
}
{
const rawArray = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
const r = reactive({ arr: readonly(rawArray) })
const c = computed(() => {
return r.arr.reduce((v, a) => a + v, 0)
})
bench(`reduce *readonly* array, ${amount} elements`, () => {
r.arr = r.arr.map(v => v + 1)
c.value
})
}
{
const rawArray = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
const r = shallowRef(rawArray)
const c = computed(() => {
return r.value.reduce((v, a) => a + v, 0)
})
bench(`reduce *raw* array, copied, ${amount} elements`, () => {
r.value = r.value.map(v => v + 1)
c.value
})
}
{
const rawArray: number[] = []
for (let i = 0, n = amount; i < n; i++) {
rawArray.push(i)
}
const r = shallowRef(rawArray)
const c = computed(() => {
return r.value.reduce((v, a) => a + v, 0)
})
bench(`reduce *raw* array, manually triggered, ${amount} elements`, () => {
for (let i = 0, n = rawArray.length; i < n; i++) {
rawArray[i]++
}
triggerRef(r)
c.value
})
}
}

View File

@ -175,6 +175,15 @@ describe('reactivity/reactive/Array', () => {
expect(length).toBe('01')
})
// #9742
test('mutation on user proxy of reactive Array', () => {
const array = reactive<number[]>([])
const proxy = new Proxy(array, {})
proxy.push(1)
expect(array).toHaveLength(1)
expect(proxy).toHaveLength(1)
})
describe('Array methods w/ refs', () => {
let original: any[]
beforeEach(() => {

View File

@ -0,0 +1,143 @@
import { bench } from 'vitest'
import { reactive, computed, ComputedRef } from '../src'
function createMap(obj: Record<string, any>) {
const map = new Map()
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
map.set(key, obj[key])
}
}
return map
}
bench('create reactive map', () => {
reactive(createMap({ a: 1 }))
})
{
let i = 0
const r = reactive(createMap({ a: 1 }))
bench('write reactive map property', () => {
r.set('a', i++)
})
}
{
const r = reactive(createMap({ a: 1 }))
computed(() => {
return r.get('a') * 2
})
let i = 0
bench("write reactive map, don't read computed (never invoked)", () => {
r.set('a', i++)
})
}
{
const r = reactive(createMap({ a: 1 }))
const c = computed(() => {
return r.get('a') * 2
})
c.value
let i = 0
bench("write reactive map, don't read computed (invoked)", () => {
r.set('a', i++)
})
}
{
const r = reactive(createMap({ a: 1 }))
const c = computed(() => {
return r.get('a') * 2
})
let i = 0
bench('write reactive map, read computed', () => {
r.set('a', i++)
c.value
})
}
{
const _m = new Map()
for (let i = 0; i < 10000; i++) {
_m.set(i, i)
}
const r = reactive(_m)
const c = computed(() => {
let total = 0
r.forEach((value, key) => {
total += value
})
return total
})
bench("write reactive map (10'000 items), read computed", () => {
r.set(5000, r.get(5000) + 1)
c.value
})
}
{
const r = reactive(createMap({ a: 1 }))
const computeds = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return r.get('a') * 2
})
computeds.push(c)
}
let i = 0
bench("write reactive map, don't read 1000 computeds (never invoked)", () => {
r.set('a', i++)
})
}
{
const r = reactive(createMap({ a: 1 }))
const computeds = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return r.get('a') * 2
})
c.value
computeds.push(c)
}
let i = 0
bench("write reactive map, don't read 1000 computeds (invoked)", () => {
r.set('a', i++)
})
}
{
const r = reactive(createMap({ a: 1 }))
const computeds: ComputedRef<number>[] = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return r.get('a') * 2
})
computeds.push(c)
}
let i = 0
bench('write reactive map, read 1000 computeds', () => {
r.set('a', i++)
computeds.forEach(c => c.value)
})
}
{
const reactives: Map<any, any>[] = []
for (let i = 0, n = 1000; i < n; i++) {
reactives.push(reactive(createMap({ a: i })))
}
const c = computed(() => {
let total = 0
reactives.forEach(r => (total += r.get('a')))
return total
})
let i = 0
const n = reactives.length
bench('1000 reactive maps, 1 computed', () => {
reactives[i++ % n].set('a', reactives[i++ % n].get('a') + 1)
c.value
})
}

View File

@ -0,0 +1,114 @@
import { bench } from 'vitest'
import { ComputedRef, computed, reactive } from '../src'
bench('create reactive obj', () => {
reactive({ a: 1 })
})
{
let i = 0
const r = reactive({ a: 1 })
bench('write reactive obj property', () => {
r.a = i++
})
}
{
const r = reactive({ a: 1 })
computed(() => {
return r.a * 2
})
let i = 0
bench("write reactive obj, don't read computed (never invoked)", () => {
r.a = i++
})
}
{
const r = reactive({ a: 1 })
const c = computed(() => {
return r.a * 2
})
c.value
let i = 0
bench("write reactive obj, don't read computed (invoked)", () => {
r.a = i++
})
}
{
const r = reactive({ a: 1 })
const c = computed(() => {
return r.a * 2
})
let i = 0
bench('write reactive obj, read computed', () => {
r.a = i++
c.value
})
}
{
const r = reactive({ a: 1 })
const computeds = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return r.a * 2
})
computeds.push(c)
}
let i = 0
bench("write reactive obj, don't read 1000 computeds (never invoked)", () => {
r.a = i++
})
}
{
const r = reactive({ a: 1 })
const computeds = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return r.a * 2
})
c.value
computeds.push(c)
}
let i = 0
bench("write reactive obj, don't read 1000 computeds (invoked)", () => {
r.a = i++
})
}
{
const r = reactive({ a: 1 })
const computeds: ComputedRef<number>[] = []
for (let i = 0, n = 1000; i < n; i++) {
const c = computed(() => {
return r.a * 2
})
computeds.push(c)
}
let i = 0
bench('write reactive obj, read 1000 computeds', () => {
r.a = i++
computeds.forEach(c => c.value)
})
}
{
const reactives: Record<string, number>[] = []
for (let i = 0, n = 1000; i < n; i++) {
reactives.push(reactive({ a: i }))
}
const c = computed(() => {
let total = 0
reactives.forEach(r => (total += r.a))
return total
})
let i = 0
const n = reactives.length
bench('1000 reactive objs, 1 computed', () => {
reactives[i++ % n].a++
c.value
})
}

View File

@ -0,0 +1,33 @@
import { describe, bench } from 'vitest'
import { ref } from '../src/index'
describe('ref', () => {
bench('create ref', () => {
ref(100)
})
{
let i = 0
const v = ref(100)
bench('write ref', () => {
v.value = i++
})
}
{
const v = ref(100)
bench('read ref', () => {
v.value
})
}
{
let i = 0
const v = ref(100)
bench('write/read ref', () => {
v.value = i++
v.value
})
}
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
"version": "3.4.0-alpha.4",
"version": "3.4.0-beta.3",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",

View File

@ -101,19 +101,25 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
} else if (key === ReactiveFlags.RAW) {
if (
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target) ||
// receiver is not the reactive proxy, but has the same prototype
// this means the reciever is a user proxy of the reactive proxy
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
) {
return target
}
// early return undefined
return
}
const targetIsArray = isArray(target)
@ -169,17 +175,19 @@ class MutableReactiveHandler extends BaseReactiveHandler {
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!this._shallow) {
const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
if (isOldValueReadonly) {
return false
} else {
oldValue.value = value
return true
}
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not

View File

@ -100,7 +100,6 @@ export function isRef(r: any): r is Ref {
* @param value - The object to wrap in the ref.
* @see {@link https://vuejs.org/api/reactivity-core.html#ref}
*/
export function ref<T extends Ref>(value: T): T
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
@ -128,9 +127,13 @@ export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }
* @param value - The "inner value" for the shallow ref.
* @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowref}
*/
export function shallowRef<T>(value: MaybeRef<T>): Ref<T> | ShallowRef<T>
export function shallowRef<T extends Ref>(value: T): T
export function shallowRef<T>(value: T): ShallowRef<T>
export function shallowRef<T>(
value: T
): Ref extends T
? T extends Ref
? IfAny<T, ShallowRef<T>, T>
: ShallowRef<T>
: ShallowRef<T>
export function shallowRef<T = any>(): ShallowRef<T | undefined>
export function shallowRef(value?: unknown) {
return createRef(value, true)

View File

@ -14,7 +14,15 @@ import {
ComputedRef,
shallowReactive,
nextTick,
ref
ref,
Ref,
watch,
openBlock,
createVNode,
createElementVNode,
createBlock,
createElementBlock,
Fragment
} from '@vue/runtime-test'
import {
defineEmits,
@ -184,13 +192,17 @@ describe('SFC <script setup> helpers', () => {
foo.value = 'bar'
}
const compRender = vi.fn()
const Comp = defineComponent({
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props) {
foo = useModel(props, 'modelValue')
},
render() {}
return () => {
compRender()
return foo.value
}
}
})
const msg = ref('')
@ -206,6 +218,8 @@ describe('SFC <script setup> helpers', () => {
expect(foo.value).toBe('')
expect(msg.value).toBe('')
expect(setValue).not.toBeCalled()
expect(compRender).toBeCalledTimes(1)
expect(serializeInner(root)).toBe('')
// update from child
update()
@ -214,42 +228,55 @@ describe('SFC <script setup> helpers', () => {
expect(msg.value).toBe('bar')
expect(foo.value).toBe('bar')
expect(setValue).toBeCalledTimes(1)
expect(compRender).toBeCalledTimes(2)
expect(serializeInner(root)).toBe('bar')
// update from parent
msg.value = 'qux'
expect(msg.value).toBe('qux')
await nextTick()
expect(msg.value).toBe('qux')
expect(foo.value).toBe('qux')
expect(setValue).toBeCalledTimes(1)
expect(compRender).toBeCalledTimes(3)
expect(serializeInner(root)).toBe('qux')
})
test('local', async () => {
test('without parent value (local mutation)', async () => {
let foo: any
const update = () => {
foo.value = 'bar'
}
const compRender = vi.fn()
const Comp = defineComponent({
props: ['foo'],
emits: ['update:foo'],
setup(props) {
foo = useModel(props, 'foo', { local: true })
},
render() {}
foo = useModel(props, 'foo')
return () => {
compRender()
return foo.value
}
}
})
const root = nodeOps.createElement('div')
const updateFoo = vi.fn()
render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
expect(compRender).toBeCalledTimes(1)
expect(serializeInner(root)).toBe('<!---->')
expect(foo.value).toBeUndefined()
update()
// when parent didn't provide value, local mutation is enabled
expect(foo.value).toBe('bar')
await nextTick()
expect(updateFoo).toBeCalledTimes(1)
expect(compRender).toBeCalledTimes(2)
expect(serializeInner(root)).toBe('bar')
})
test('default value', async () => {
@ -257,25 +284,234 @@ describe('SFC <script setup> helpers', () => {
const inc = () => {
count.value++
}
const compRender = vi.fn()
const Comp = defineComponent({
props: { count: { default: 0 } },
emits: ['update:count'],
setup(props) {
count = useModel(props, 'count', { local: true })
},
render() {}
count = useModel(props, 'count')
return () => {
compRender()
return count.value
}
}
})
const root = nodeOps.createElement('div')
const updateCount = vi.fn()
render(h(Comp, { 'onUpdate:count': updateCount }), root)
expect(compRender).toBeCalledTimes(1)
expect(serializeInner(root)).toBe('0')
expect(count.value).toBe(0)
inc()
// when parent didn't provide value, local mutation is enabled
expect(count.value).toBe(1)
await nextTick()
expect(updateCount).toBeCalledTimes(1)
expect(compRender).toBeCalledTimes(2)
expect(serializeInner(root)).toBe('1')
})
test('parent limiting child value', async () => {
let childCount: Ref<number>
const compRender = vi.fn()
const Comp = defineComponent({
props: ['count'],
emits: ['update:count'],
setup(props) {
childCount = useModel(props, 'count')
return () => {
compRender()
return childCount.value
}
}
})
const Parent = defineComponent({
setup() {
const count = ref(0)
watch(count, () => {
if (count.value < 0) {
count.value = 0
}
})
return () =>
h(Comp, {
count: count.value,
'onUpdate:count': val => {
count.value = val
}
})
}
})
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(serializeInner(root)).toBe('0')
// child update
childCount!.value = 1
// not yet updated
expect(childCount!.value).toBe(0)
await nextTick()
expect(childCount!.value).toBe(1)
expect(serializeInner(root)).toBe('1')
// child update to invalid value
childCount!.value = -1
// not yet updated
expect(childCount!.value).toBe(1)
await nextTick()
// limited to 0 by parent
expect(childCount!.value).toBe(0)
expect(serializeInner(root)).toBe('0')
})
test('has parent value -> no parent value', async () => {
let childCount: Ref<number>
const compRender = vi.fn()
const Comp = defineComponent({
props: ['count'],
emits: ['update:count'],
setup(props) {
childCount = useModel(props, 'count')
return () => {
compRender()
return childCount.value
}
}
})
const toggle = ref(true)
const Parent = defineComponent({
setup() {
const count = ref(0)
return () =>
toggle.value
? h(Comp, {
count: count.value,
'onUpdate:count': val => {
count.value = val
}
})
: h(Comp)
}
})
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(serializeInner(root)).toBe('0')
// child update
childCount!.value = 1
// not yet updated
expect(childCount!.value).toBe(0)
await nextTick()
expect(childCount!.value).toBe(1)
expect(serializeInner(root)).toBe('1')
// parent change
toggle.value = false
await nextTick()
// localValue should be reset
expect(childCount!.value).toBeUndefined()
expect(serializeInner(root)).toBe('<!---->')
// child local mutation should continue to work
childCount!.value = 2
expect(childCount!.value).toBe(2)
await nextTick()
expect(serializeInner(root)).toBe('2')
})
// #9838
test('pass modelValue to slot (optimized mode) ', async () => {
let foo: any
const update = () => {
foo.value = 'bar'
}
const Comp = {
render(this: any) {
return this.$slots.default()
}
}
const childRender = vi.fn()
const slotRender = vi.fn()
const Child = defineComponent({
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props) {
foo = useModel(props, 'modelValue')
return () => {
childRender()
return (
openBlock(),
createElementBlock(Fragment, null, [
createVNode(Comp, null, {
default: () => {
slotRender()
return createElementVNode('div', null, foo.value)
},
_: 1 /* STABLE */
})
])
)
}
}
})
const msg = ref('')
const setValue = vi.fn(v => (msg.value = v))
const root = nodeOps.createElement('div')
createApp({
render() {
return (
openBlock(),
createBlock(
Child,
{
modelValue: msg.value,
'onUpdate:modelValue': setValue
},
null,
8 /* PROPS */,
['modelValue']
)
)
}
}).mount(root)
expect(foo.value).toBe('')
expect(msg.value).toBe('')
expect(setValue).not.toBeCalled()
expect(childRender).toBeCalledTimes(1)
expect(slotRender).toBeCalledTimes(1)
expect(serializeInner(root)).toBe('<div></div>')
// update from child
update()
await nextTick()
expect(msg.value).toBe('bar')
expect(foo.value).toBe('bar')
expect(setValue).toBeCalledTimes(1)
expect(childRender).toBeCalledTimes(2)
expect(slotRender).toBeCalledTimes(2)
expect(serializeInner(root)).toBe('<div>bar</div>')
})
})

View File

@ -0,0 +1,58 @@
import { nextTick, ref, watch, watchEffect } from '../src'
import { bench } from 'vitest'
bench('create watcher', () => {
const v = ref(100)
watch(v, v => {})
})
{
const v = ref(100)
watch(v, v => {})
let i = 0
bench('update ref to trigger watcher (scheduled but not executed)', () => {
v.value = i++
})
}
{
const v = ref(100)
watch(v, v => {})
let i = 0
bench('update ref to trigger watcher (executed)', async () => {
v.value = i++
return nextTick()
})
}
{
bench('create watchEffect', () => {
watchEffect(() => {})
})
}
{
const v = ref(100)
watchEffect(() => {
v.value
})
let i = 0
bench(
'update ref to trigger watchEffect (scheduled but not executed)',
() => {
v.value = i++
}
)
}
{
const v = ref(100)
watchEffect(() => {
v.value
})
let i = 0
bench('update ref to trigger watchEffect (executed)', async () => {
v.value = i++
await nextTick()
})
}

View File

@ -549,6 +549,98 @@ describe('api: watch', () => {
expect(cb).not.toHaveBeenCalled()
})
// #7030
it('should not fire on child component unmount w/ flush: pre', async () => {
const visible = ref(true)
const cb = vi.fn()
const Parent = defineComponent({
props: ['visible'],
render() {
return visible.value ? h(Comp) : null
}
})
const Comp = {
setup() {
watch(visible, cb, { flush: 'pre' })
},
render() {}
}
const App = {
render() {
return h(Parent, {
visible: visible.value
})
}
}
render(h(App), nodeOps.createElement('div'))
expect(cb).not.toHaveBeenCalled()
visible.value = false
await nextTick()
expect(cb).not.toHaveBeenCalled()
})
// #7030
it('flush: pre watcher in child component should not fire before parent update', async () => {
const b = ref(0)
const calls: string[] = []
const Comp = {
setup() {
watch(
() => b.value,
val => {
calls.push('watcher child')
},
{ flush: 'pre' }
)
return () => {
b.value
calls.push('render child')
}
}
}
const Parent = {
props: ['a'],
setup() {
watch(
() => b.value,
val => {
calls.push('watcher parent')
},
{ flush: 'pre' }
)
return () => {
b.value
calls.push('render parent')
return h(Comp)
}
}
}
const App = {
render() {
return h(Parent, {
a: b.value
})
}
}
render(h(App), nodeOps.createElement('div'))
expect(calls).toEqual(['render parent', 'render child'])
b.value++
await nextTick()
expect(calls).toEqual([
'render parent',
'render child',
'watcher parent',
'render parent',
'watcher child',
'render child'
])
})
// #1763
it('flush: pre watcher watching props should fire before child update', async () => {
const a = ref(0)

View File

@ -19,7 +19,8 @@ import {
shallowRef,
SuspenseProps,
resolveDynamicComponent,
Fragment
Fragment,
KeepAlive
} from '@vue/runtime-test'
import { createApp, defineComponent } from 'vue'
import { type RawSlots } from 'packages/runtime-core/src/componentSlots'
@ -537,6 +538,51 @@ describe('Suspense', () => {
expect(unmounted).not.toHaveBeenCalled()
})
// vuetifyjs/vuetify#15207
test('update prop of async element before suspense resolve', async () => {
let resolve: () => void
const mounted = new Promise<void>(r => {
resolve = r
})
const Async = {
async setup() {
onMounted(() => {
resolve()
})
const p = new Promise(r => setTimeout(r, 1))
await p
return () => h('div', 'async')
}
}
const Comp: ComponentOptions<{ data: string }> = {
props: ['data'],
setup(props) {
return () => h(Async, { 'data-test': props.data })
}
}
const Root = {
setup() {
const data = ref('1')
onMounted(() => {
data.value = '2'
})
return () =>
h(Suspense, null, {
default: h(Comp, { data: data.value }),
fallback: h('div', 'fallback')
})
}
}
const root = nodeOps.createElement('div')
render(h(Root), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
await mounted
expect(serializeInner(root)).toBe(`<div data-test="2">async</div>`)
})
test('nested suspense (parent resolves first)', async () => {
const calls: string[] = []
@ -1185,6 +1231,72 @@ describe('Suspense', () => {
expect(calls).toEqual([`one mounted`, `one unmounted`, `two mounted`])
})
test('mount the fallback content is in the correct position', async () => {
const makeComp = (name: string, delay = 0) =>
defineAsyncComponent(
{
setup() {
return () => h('div', [name])
}
},
delay
)
const One = makeComp('one')
const Two = makeComp('two', 20)
const view = shallowRef(One)
const Comp = {
setup() {
return () =>
h('div', [
h(
Suspense,
{
timeout: 10
},
{
default: h(view.value),
fallback: h('div', 'fallback')
}
),
h('div', 'three')
])
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(
`<div><div>fallback</div><div>three</div></div>`
)
await deps[0]
await nextTick()
expect(serializeInner(root)).toBe(
`<div><div>one</div><div>three</div></div>`
)
view.value = Two
await nextTick()
expect(serializeInner(root)).toBe(
`<div><div>one</div><div>three</div></div>`
)
await new Promise(r => setTimeout(r, 10))
await nextTick()
expect(serializeInner(root)).toBe(
`<div><div>fallback</div><div>three</div></div>`
)
await deps[1]
await nextTick()
expect(serializeInner(root)).toBe(
`<div><div>two</div><div>three</div></div>`
)
})
// #2214
// Since suspense renders its own root like a component, it should not patch
// its content in optimized mode.
@ -1527,6 +1639,97 @@ describe('Suspense', () => {
expect(serializeInner(root)).toBe(expected)
})
// #6416
test('KeepAlive with Suspense', async () => {
const Async = defineAsyncComponent({
render() {
return h('div', 'async')
}
})
const Sync = {
render() {
return h('div', 'sync')
}
}
const components = [Async, Sync]
const viewRef = ref(0)
const root = nodeOps.createElement('div')
const App = {
render() {
return h(KeepAlive, null, {
default: () => {
return h(Suspense, null, {
default: h(components[viewRef.value]),
fallback: h('div', 'Loading-dynamic-components')
})
}
})
}
}
render(h(App), root)
expect(serializeInner(root)).toBe(`<div>Loading-dynamic-components</div>`)
viewRef.value = 1
await nextTick()
expect(serializeInner(root)).toBe(`<div>sync</div>`)
viewRef.value = 0
await nextTick()
expect(serializeInner(root)).toBe('<!---->')
await Promise.all(deps)
await nextTick()
// when async resolve,it should be <div>async</div>
expect(serializeInner(root)).toBe('<div>async</div>')
viewRef.value = 1
await nextTick()
// TypeError: Cannot read properties of null (reading 'parentNode')
// This has been fixed
expect(serializeInner(root)).toBe(`<div>sync</div>`)
})
// #6416 follow up
test('Suspense patched during HOC async component re-mount', async () => {
const key = ref('k')
const data = ref('data')
const Async = defineAsyncComponent({
render() {
return h('div', 'async')
}
})
const Comp = {
render() {
return h(Async, { key: key.value })
}
}
const root = nodeOps.createElement('div')
const App = {
render() {
return h(Suspense, null, {
default: h(Comp, { data: data.value })
})
}
}
render(h(App), root)
expect(serializeInner(root)).toBe(`<!---->`)
await Promise.all(deps)
// async mounted, but key change causing a new async comp to be loaded
key.value = 'k1'
await nextTick()
// patch the Suspense
// should not throw error due to Suspense vnode.el being null
data.value = 'data2'
await Promise.all(deps)
})
describe('warnings', () => {
// base function to check if a combination of slots warns or not
function baseCheckWarn(

View File

@ -812,17 +812,17 @@ describe('SSR hydration', () => {
})
)
const bol = ref(true)
const toggle = ref(true)
const App = {
setup() {
onMounted(() => {
// change state, this makes updateComponent(AsyncComp) execute before
// the async component is resolved
bol.value = false
toggle.value = false
})
return () => {
return [bol.value ? 'hello' : 'world', h(AsyncComp)]
return [toggle.value ? 'hello' : 'world', h(AsyncComp)]
}
}
}
@ -859,6 +859,147 @@ describe('SSR hydration', () => {
)
})
test('hydrate safely when property used by async setup changed before render', async () => {
const toggle = ref(true)
const AsyncComp = {
async setup() {
await new Promise<void>(r => setTimeout(r, 10))
return () => h('h1', 'Async component')
}
}
const AsyncWrapper = {
render() {
return h(AsyncComp)
}
}
const SiblingComp = {
setup() {
toggle.value = false
return () => h('span')
}
}
const App = {
setup() {
return () =>
h(
Suspense,
{},
{
default: () => [
h('main', {}, [
h(AsyncWrapper, {
prop: toggle.value ? 'hello' : 'world'
}),
h(SiblingComp)
])
]
}
)
}
}
// server render
const html = await renderToString(h(App))
expect(html).toMatchInlineSnapshot(
`"<main><h1 prop="hello">Async component</h1><span></span></main>"`
)
expect(toggle.value).toBe(false)
// hydration
// reset the value
toggle.value = true
expect(toggle.value).toBe(true)
const container = document.createElement('div')
container.innerHTML = html
createSSRApp(App).mount(container)
await new Promise(r => setTimeout(r, 10))
expect(toggle.value).toBe(false)
// should be hydrated now
expect(container.innerHTML).toMatchInlineSnapshot(
`"<main><h1 prop="world">Async component</h1><span></span></main>"`
)
})
test('hydrate safely when property used by deep nested async setup changed before render', async () => {
const toggle = ref(true)
const AsyncComp = {
async setup() {
await new Promise<void>(r => setTimeout(r, 10))
return () => h('h1', 'Async component')
}
}
const AsyncWrapper = { render: () => h(AsyncComp) }
const AsyncWrapperWrapper = { render: () => h(AsyncWrapper) }
const SiblingComp = {
setup() {
toggle.value = false
return () => h('span')
}
}
const App = {
setup() {
return () =>
h(
Suspense,
{},
{
default: () => [
h('main', {}, [
h(AsyncWrapperWrapper, {
prop: toggle.value ? 'hello' : 'world'
}),
h(SiblingComp)
])
]
}
)
}
}
// server render
const html = await renderToString(h(App))
expect(html).toMatchInlineSnapshot(
`"<main><h1 prop="hello">Async component</h1><span></span></main>"`
)
expect(toggle.value).toBe(false)
// hydration
// reset the value
toggle.value = true
expect(toggle.value).toBe(true)
const container = document.createElement('div')
container.innerHTML = html
createSSRApp(App).mount(container)
await new Promise(r => setTimeout(r, 10))
expect(toggle.value).toBe(false)
// should be hydrated now
expect(container.innerHTML).toMatchInlineSnapshot(
`"<main><h1 prop="world">Async component</h1><span></span></main>"`
)
})
// #3787
test('unmount async wrapper before load', async () => {
let resolve: any
@ -981,7 +1122,7 @@ describe('SSR hydration', () => {
test('force hydrate select option with non-string value bindings', () => {
const { container } = mountWithHydration(
'<select><option :value="true">ok</option></select>',
'<select><option value="true">ok</option></select>',
() =>
h('select', [
// hoisted because bound value is a constant...
@ -1114,6 +1255,41 @@ describe('SSR hydration', () => {
expect(`mismatch`).not.toHaveBeenWarned()
})
test('transition appear w/ event listener', async () => {
const container = document.createElement('div')
container.innerHTML = `<template><button>0</button></template>`
createSSRApp({
data() {
return {
count: 0
}
},
template: `
<Transition appear>
<button @click="count++">{{count}}</button>
</Transition>
`
}).mount(container)
expect(container.firstChild).toMatchInlineSnapshot(`
<button
class="v-enter-from v-enter-active"
>
0
</button>
`)
triggerEvent('click', container.querySelector('button')!)
await nextTick()
expect(container.firstChild).toMatchInlineSnapshot(`
<button
class="v-enter-from v-enter-active"
>
1
</button>
`)
})
describe('mismatch handling', () => {
test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar')
@ -1126,7 +1302,7 @@ describe('SSR hydration', () => {
h('div', 'bar')
)
expect(container.innerHTML).toBe('<div>bar</div>')
expect(`Hydration text content mismatch in <div>`).toHaveBeenWarned()
expect(`Hydration text content mismatch`).toHaveBeenWarned()
})
test('not enough children', () => {
@ -1136,7 +1312,7 @@ describe('SSR hydration', () => {
expect(container.innerHTML).toBe(
'<div><span>foo</span><span>bar</span></div>'
)
expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
expect(`Hydration children mismatch`).toHaveBeenWarned()
})
test('too many children', () => {
@ -1145,7 +1321,7 @@ describe('SSR hydration', () => {
() => h('div', [h('span', 'foo')])
)
expect(container.innerHTML).toBe('<div><span>foo</span></div>')
expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
expect(`Hydration children mismatch`).toHaveBeenWarned()
})
test('complete mismatch', () => {
@ -1219,5 +1395,58 @@ describe('SSR hydration', () => {
expect(container.innerHTML).toBe('<div><!--hi--></div>')
expect(`Hydration node mismatch`).toHaveBeenWarned()
})
test('class mismatch', () => {
mountWithHydration(`<div class="foo bar"></div>`, () =>
h('div', { class: ['foo', 'bar'] })
)
mountWithHydration(`<div class="foo bar"></div>`, () =>
h('div', { class: { foo: true, bar: true } })
)
mountWithHydration(`<div class="foo bar"></div>`, () =>
h('div', { class: 'foo bar' })
)
expect(`Hydration class mismatch`).not.toHaveBeenWarned()
mountWithHydration(`<div class="foo bar"></div>`, () =>
h('div', { class: 'foo' })
)
expect(`Hydration class mismatch`).toHaveBeenWarned()
})
test('style mismatch', () => {
mountWithHydration(`<div style="color:red;"></div>`, () =>
h('div', { style: { color: 'red' } })
)
mountWithHydration(`<div style="color:red;"></div>`, () =>
h('div', { style: `color:red;` })
)
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
mountWithHydration(`<div style="color:red;"></div>`, () =>
h('div', { style: { color: 'green' } })
)
expect(`Hydration style mismatch`).toHaveBeenWarned()
})
test('attr mismatch', () => {
mountWithHydration(`<div id="foo"></div>`, () => h('div', { id: 'foo' }))
mountWithHydration(`<div spellcheck></div>`, () =>
h('div', { spellcheck: '' })
)
mountWithHydration(`<div></div>`, () => h('div', { id: undefined }))
// boolean
mountWithHydration(`<select multiple></div>`, () =>
h('select', { multiple: true })
)
mountWithHydration(`<select multiple></div>`, () =>
h('select', { multiple: 'multiple' })
)
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))
expect(`Hydration attribute mismatch`).toHaveBeenWarned()
mountWithHydration(`<div id="bar"></div>`, () => h('div', { id: 'foo' }))
expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
})
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-core",
"version": "3.4.0-alpha.4",
"version": "3.4.0-beta.3",
"description": "@vue/runtime-core",
"main": "index.js",
"module": "dist/runtime-core.esm-bundler.js",

View File

@ -15,7 +15,7 @@ import {
ComponentPublicInstance
} from './componentPublicInstance'
import { Directive, validateDirectiveName } from './directives'
import { RootRenderFunction } from './renderer'
import { ElementNamespace, RootRenderFunction } from './renderer'
import { InjectionKey } from './apiInject'
import { warn } from './warning'
import { createVNode, cloneVNode, VNode } from './vnode'
@ -41,12 +41,12 @@ export interface App<HostElement = any> {
mixin(mixin: ComponentOptions): this
component(name: string): Component | undefined
component(name: string, component: Component | DefineComponent): this
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): this
directive<T = any, V = any>(name: string): Directive<T, V> | undefined
directive<T = any, V = any>(name: string, directive: Directive<T, V>): this
mount(
rootContainer: HostElement | string,
isHydrate?: boolean,
isSVG?: boolean
namespace?: boolean | ElementNamespace
): ComponentPublicInstance
unmount(): void
provide<T>(key: InjectionKey<T> | string, value: T): this
@ -148,17 +148,19 @@ export interface AppContext {
filters?: Record<string, Function>
}
type PluginInstallFunction<Options> = Options extends unknown[]
type PluginInstallFunction<Options = any[]> = Options extends unknown[]
? (app: App, ...options: Options) => any
: (app: App, options: Options) => any
export type ObjectPlugin<Options = any[]> = {
install: PluginInstallFunction<Options>
}
export type FunctionPlugin<Options = any[]> = PluginInstallFunction<Options> &
Partial<ObjectPlugin<Options>>
export type Plugin<Options = any[]> =
| (PluginInstallFunction<Options> & {
install?: PluginInstallFunction<Options>
})
| {
install: PluginInstallFunction<Options>
}
| FunctionPlugin<Options>
| ObjectPlugin<Options>
export function createAppContext(): AppContext {
return {
@ -296,7 +298,7 @@ export function createAppAPI<HostElement>(
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
namespace?: boolean | ElementNamespace
): any {
if (!isMounted) {
// #5571
@ -312,17 +314,29 @@ export function createAppAPI<HostElement>(
// this will be set on the root instance on initial mount.
vnode.appContext = context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
// casting to ElementNamespace because TS doesn't guarantee type narrowing
// over function boundaries
render(
cloneVNode(vnode),
rootContainer,
namespace as ElementNamespace
)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
render(vnode, rootContainer, namespace)
}
isMounted = true
app._container = rootContainer

View File

@ -5,7 +5,8 @@ import {
Prettify,
UnionToIntersection,
extend,
LooseRequired
LooseRequired,
hasChanged
} from '@vue/shared'
import {
getCurrentInstance,
@ -30,8 +31,8 @@ import {
} from './componentProps'
import { warn } from './warning'
import { SlotsType, StrictUnwrapSlotsType } from './componentSlots'
import { Ref, ref } from '@vue/reactivity'
import { watch } from './apiWatch'
import { Ref, customRef, ref } from '@vue/reactivity'
import { watchSyncEffect } from '.'
// dev only
const warnRuntimeUsage = (method: string) =>
@ -66,9 +67,9 @@ const warnRuntimeUsage = (method: string) =>
* foo?: string
* bar: number
* }>()
* ```
*
* @see {@link https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits}
* ```
*
* This is only usable inside `<script setup>`, is compiled away in the
* output and should **not** be actually called at runtime.
@ -94,7 +95,7 @@ export function defineProps() {
return null as any
}
type DefineProps<T, BKeys extends keyof T> = Readonly<T> & {
export type DefineProps<T, BKeys extends keyof T> = Readonly<T> & {
readonly [K in BKeys]-?: boolean
}
@ -116,8 +117,9 @@ type BooleanKey<T, K extends keyof T = keyof T> = K extends any
* Example type-based declaration:
* ```ts
* const emit = defineEmits<{
* (event: 'change'): void
* (event: 'update', id: number): void
* // <eventName>: <expected arguments>
* change: []
* update: [value: string] // named tuple syntax
* }>()
*
* emit('change')
@ -217,7 +219,7 @@ export function defineSlots<
}
/**
* (**Experimental**) Vue `<script setup>` compiler macro for declaring a
* Vue `<script setup>` compiler macro for declaring a
* two-way binding prop that can be consumed via `v-model` from the parent
* component. This will declare a prop with the same name and a corresponding
* `update:propName` event.
@ -226,9 +228,11 @@ export function defineSlots<
* Otherwise the prop name will default to "modelValue". In both cases, you
* can also pass an additional object which will be used as the prop's options.
*
* The options object can also specify an additional option, `local`. When set
* to `true`, the ref can be locally mutated even if the parent did not pass
* the matching `v-model`.
* The the returned ref behaves differently depending on whether the parent
* provided the corresponding v-model props or not:
* - If yes, the returned ref's value will always be in sync with the parent
* prop.
* - If not, the returned ref will behave like a normal local ref.
*
* @example
* ```ts
@ -245,32 +249,26 @@ export function defineSlots<
*
* // with specified name and default value
* const count = defineModel<number>('count', { default: 0 })
*
* // local mutable model, can be mutated locally
* // even if the parent did not pass the matching `v-model`.
* const count = defineModel<number>('count', { local: true, default: 0 })
* ```
*/
export function defineModel<T>(
options: { required: true } & PropOptions<T> & DefineModelOptions
options: { required: true } & PropOptions<T>
): Ref<T>
export function defineModel<T>(
options: { default: any } & PropOptions<T> & DefineModelOptions
options: { default: any } & PropOptions<T>
): Ref<T>
export function defineModel<T>(
options?: PropOptions<T> & DefineModelOptions
): Ref<T | undefined>
export function defineModel<T>(options?: PropOptions<T>): Ref<T | undefined>
export function defineModel<T>(
name: string,
options: { required: true } & PropOptions<T> & DefineModelOptions
options: { required: true } & PropOptions<T>
): Ref<T>
export function defineModel<T>(
name: string,
options: { default: any } & PropOptions<T> & DefineModelOptions
options: { default: any } & PropOptions<T>
): Ref<T>
export function defineModel<T>(
name: string,
options?: PropOptions<T> & DefineModelOptions
options?: PropOptions<T>
): Ref<T | undefined>
export function defineModel(): any {
if (__DEV__) {
@ -278,10 +276,6 @@ export function defineModel(): any {
}
}
interface DefineModelOptions {
local?: boolean
}
type NotUndefined<T> = T extends undefined ? never : T
type InferDefaults<T> = {
@ -356,14 +350,9 @@ export function useAttrs(): SetupContext['attrs'] {
export function useModel<T extends Record<string, any>, K extends keyof T>(
props: T,
name: K,
options?: { local?: boolean }
name: K
): Ref<T[K]>
export function useModel(
props: Record<string, any>,
name: string,
options?: { local?: boolean }
): Ref {
export function useModel(props: Record<string, any>, name: string): Ref {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useModel() called without active instance.`)
@ -375,32 +364,30 @@ export function useModel(
return ref() as any
}
if (options && options.local) {
const proxy = ref<any>(props[name])
watch(
() => props[name],
v => (proxy.value = v)
)
watch(proxy, value => {
if (value !== props[name]) {
i.emit(`update:${name}`, value)
return customRef((track, trigger) => {
let localValue: any
watchSyncEffect(() => {
const propValue = props[name]
if (hasChanged(localValue, propValue)) {
localValue = propValue
trigger()
}
})
return proxy
} else {
return {
__v_isRef: true,
get value() {
return props[name]
get() {
track()
return localValue
},
set value(value) {
set(value) {
const rawProps = i.vnode!.props
if (!(rawProps && name in rawProps) && hasChanged(value, localValue)) {
localValue = value
trigger()
}
i.emit(`update:${name}`, value)
}
} as any
}
}
})
}
function getContext(): SetupContext {

View File

@ -17,7 +17,7 @@ import {
} from '@vue/shared'
import { warn } from '../warning'
import { cloneVNode, createVNode } from '../vnode'
import { RootRenderFunction } from '../renderer'
import { ElementNamespace, RootRenderFunction } from '../renderer'
import {
App,
AppConfig,
@ -82,8 +82,11 @@ export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
component(name: string): Component | undefined
component(name: string, component: Component): CompatVue
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): CompatVue
directive<T = any, V = any>(name: string): Directive<T, V> | undefined
directive<T = any, V = any>(
name: string,
directive: Directive<T, V>
): CompatVue
compile(template: string): RenderFunction
@ -503,7 +506,13 @@ function installCompatMount(
container = selectorOrEl || document.createElement('div')
}
const isSVG = container instanceof SVGElement
let namespace: ElementNamespace
if (container instanceof SVGElement) namespace = 'svg'
else if (
typeof MathMLElement === 'function' &&
container instanceof MathMLElement
)
namespace = 'mathml'
// HMR root reload
if (__DEV__) {
@ -511,7 +520,7 @@ function installCompatMount(
const cloned = cloneVNode(vnode)
// compat mode will use instance if not reset to null
cloned.component = null
render(cloned, container, isSVG)
render(cloned, container, namespace)
}
}
@ -538,7 +547,7 @@ function installCompatMount(
container.innerHTML = ''
// TODO hydration
render(vnode, container, isSVG)
render(vnode, container, namespace)
if (container instanceof Element) {
container.removeAttribute('v-cloak')

View File

@ -51,7 +51,8 @@ import {
EmitFn,
emit,
normalizeEmitsOptions,
EmitsToProps
EmitsToProps,
ShortEmitsToObject
} from './componentEmits'
import {
EMPTY_OBJ,
@ -82,6 +83,39 @@ import {
import { SchedulerJob } from './scheduler'
import { LifecycleHooks } from './enums'
/**
* Public utility type for extracting the instance type of a component.
* Works with all valid component definition types. This is intended to replace
* the usage of `InstanceType<typeof Comp>` which only works for
* constructor-based component definition types.
*
* Exmaple:
* ```ts
* const MyComp = { ... }
* declare const instance: ComponentInstance<typeof MyComp>
* ```
*/
export type ComponentInstance<T> = T extends { new (): ComponentPublicInstance }
? InstanceType<T>
: T extends FunctionalComponent<infer Props, infer Emits>
? ComponentPublicInstance<Props, {}, {}, {}, {}, ShortEmitsToObject<Emits>>
: T extends Component<
infer Props,
infer RawBindings,
infer D,
infer C,
infer M
>
? // NOTE we override Props/RawBindings/D to make sure is not `unknown`
ComponentPublicInstance<
unknown extends Props ? {} : Props,
unknown extends RawBindings ? {} : RawBindings,
unknown extends D ? {} : D,
C,
M
>
: never // not a vue Component
/**
* For extending allowed non-declared props on components in TSX
*/
@ -126,16 +160,17 @@ export interface ComponentInternalOptions {
export interface FunctionalComponent<
P = {},
E extends EmitsOptions = {},
S extends Record<string, any> = any
E extends EmitsOptions | Record<string, any[]> = {},
S extends Record<string, any> = any,
EE extends EmitsOptions = ShortEmitsToObject<E>
> extends ComponentInternalOptions {
// use of any here is intentional so it can be a valid JSX Element constructor
(
props: P & EmitsToProps<E>,
ctx: Omit<SetupContext<E, IfAny<S, {}, SlotsType<S>>>, 'expose'>
props: P & EmitsToProps<EE>,
ctx: Omit<SetupContext<EE, IfAny<S, {}, SlotsType<S>>>, 'expose'>
): any
props?: ComponentPropsOptions<P>
emits?: E | (keyof E)[]
emits?: EE | (keyof EE)[]
slots?: IfAny<S, Slots, SlotsType<S>>
inheritAttrs?: boolean
displayName?: string
@ -158,10 +193,12 @@ export type ConcreteComponent<
RawBindings = any,
D = any,
C extends ComputedOptions = ComputedOptions,
M extends MethodOptions = MethodOptions
M extends MethodOptions = MethodOptions,
E extends EmitsOptions | Record<string, any[]> = {},
S extends Record<string, any> = any
> =
| ComponentOptions<Props, RawBindings, D, C, M>
| FunctionalComponent<Props, any>
| FunctionalComponent<Props, E, S>
/**
* A type used in public APIs where a component type is expected.
@ -172,9 +209,11 @@ export type Component<
RawBindings = any,
D = any,
C extends ComputedOptions = ComputedOptions,
M extends MethodOptions = MethodOptions
M extends MethodOptions = MethodOptions,
E extends EmitsOptions | Record<string, any[]> = {},
S extends Record<string, any> = any
> =
| ConcreteComponent<Props, RawBindings, D, C, M>
| ConcreteComponent<Props, RawBindings, D, C, M, E, S>
| ComponentPublicInstanceConstructor<Props>
export type { ComponentOptions }
@ -590,13 +629,10 @@ export let currentInstance: ComponentInternalInstance | null = null
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance || currentRenderingInstance
type GlobalInstanceSetter = ((
let internalSetCurrentInstance: (
instance: ComponentInternalInstance | null
) => void) & { version?: string }
let internalSetCurrentInstance: GlobalInstanceSetter
let globalCurrentInstanceSetters: GlobalInstanceSetter[]
let settersKey = '__VUE_INSTANCE_SETTERS__'
) => void
let setInSSRSetupState: (state: boolean) => void
/**
* The following makes getCurrentInstance() usage across multiple copies of Vue
@ -611,21 +647,36 @@ let settersKey = '__VUE_INSTANCE_SETTERS__'
* found during browser execution.
*/
if (__SSR__) {
if (!(globalCurrentInstanceSetters = getGlobalThis()[settersKey])) {
globalCurrentInstanceSetters = getGlobalThis()[settersKey] = []
}
globalCurrentInstanceSetters.push(i => (currentInstance = i))
internalSetCurrentInstance = instance => {
if (globalCurrentInstanceSetters.length > 1) {
globalCurrentInstanceSetters.forEach(s => s(instance))
} else {
globalCurrentInstanceSetters[0](instance)
type Setter = (v: any) => void
const g = getGlobalThis()
const registerGlobalSetter = (key: string, setter: Setter) => {
let setters: Setter[]
if (!(setters = g[key])) setters = g[key] = []
setters.push(setter)
return (v: any) => {
if (setters.length > 1) setters.forEach(set => set(v))
else setters[0](v)
}
}
internalSetCurrentInstance = registerGlobalSetter(
`__VUE_INSTANCE_SETTERS__`,
v => (currentInstance = v)
)
// also make `isInSSRComponentSetup` sharable across copies of Vue.
// this is needed in the SFC playground when SSRing async components, since
// we have to load both the runtime and the server-renderer from CDNs, they
// contain duplicated copies of Vue runtime code.
setInSSRSetupState = registerGlobalSetter(
`__VUE_SSR_SETTERS__`,
v => (isInSSRComponentSetup = v)
)
} else {
internalSetCurrentInstance = i => {
currentInstance = i
}
setInSSRSetupState = v => {
isInSSRComponentSetup = v
}
}
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
@ -659,7 +710,7 @@ export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
isSSR && setInSSRSetupState(isSSR)
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
@ -669,7 +720,8 @@ export function setupComponent(
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
isSSR && setInSSRSetupState(false)
return setupResult
}

View File

@ -38,23 +38,28 @@ export type EmitsOptions = ObjectEmitsOptions | string[]
export type EmitsToProps<T extends EmitsOptions> = T extends string[]
? {
[K in string & `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
[K in `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
}
: T extends ObjectEmitsOptions
? {
[K in string &
`on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
? T[Uncapitalize<C>] extends null
? (...args: any[]) => any
: (
...args: T[Uncapitalize<C>] extends (...args: infer P) => any
? P
[K in `on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
? (
...args: T[Uncapitalize<C>] extends (...args: infer P) => any
? P
: T[Uncapitalize<C>] extends null
? any[]
: never
) => any
) => any
: never
}
: {}
export type ShortEmitsToObject<E> = E extends Record<string, any[]>
? {
[K in keyof E]: (...args: E[K]) => any
}
: E
export type EmitFn<
Options = ObjectEmitsOptions,
Event extends keyof Options = keyof Options
@ -66,7 +71,9 @@ export type EmitFn<
{
[key in Event]: Options[key] extends (...args: infer Args) => any
? (event: key, ...args: Args) => void
: (event: key, ...args: any[]) => void
: Options[key] extends any[]
? (event: key, ...args: Options[key]) => void
: (event: key, ...args: any[]) => void
}[Event]
>

View File

@ -433,8 +433,17 @@ export function updateHOCHostEl(
{ vnode, parent }: ComponentInternalInstance,
el: typeof vnode.el // HostNode
) {
while (parent && parent.subTree === vnode) {
;(vnode = parent.vnode).el = el
parent = parent.parent
if (!el) return
while (parent) {
const root = parent.subTree
if (root.suspense && root.suspense.activeBranch === vnode) {
root.el = vnode.el
}
if (root === vnode) {
;(vnode = parent.vnode).el = el
parent = parent.parent
} else {
break
}
}
}

View File

@ -44,7 +44,7 @@ export type SlotsType<T extends Record<string, any> = Record<string, any>> = {
export type StrictUnwrapSlotsType<
S extends SlotsType,
T = NonNullable<S[typeof SlotSymbol]>
> = [keyof S] extends [never] ? Slots : Readonly<T>
> = [keyof S] extends [never] ? Slots : Readonly<T> & T
export type UnwrapSlotsType<
S extends SlotsType,

View File

@ -37,7 +37,8 @@ import {
queuePostRenderEffect,
MoveType,
RendererElement,
RendererNode
RendererNode,
ElementNamespace
} from '../renderer'
import { setTransitionHooks } from './BaseTransition'
import { ComponentRenderContext } from '../componentPublicInstance'
@ -64,7 +65,7 @@ export interface KeepAliveContext extends ComponentRenderContext {
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
isSVG: boolean,
namespace: ElementNamespace,
optimized: boolean
) => void
deactivate: (vnode: VNode) => void
@ -125,7 +126,13 @@ const KeepAliveImpl: ComponentOptions = {
} = sharedContext
const storageContainer = createElement('div')
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
sharedContext.activate = (
vnode,
container,
anchor,
namespace,
optimized
) => {
const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed
@ -136,7 +143,7 @@ const KeepAliveImpl: ComponentOptions = {
anchor,
instance,
parentSuspense,
isSVG,
namespace,
vnode.slotScopeIds,
optimized
)

View File

@ -18,7 +18,8 @@ import {
MoveType,
SetupRenderEffectFn,
RendererNode,
RendererElement
RendererElement,
ElementNamespace
} from '../renderer'
import { queuePostFlushCb } from '../scheduler'
import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
@ -46,6 +47,9 @@ export interface SuspenseProps {
export const isSuspense = (type: any): boolean => type.__isSuspense
// incrementing unique id for every pending branch
let suspenseId = 0
// Suspense exposes a component-like API, and is treated like a component
// in the compiler, but internally it's a special built-in type that hooks
// directly into the renderer.
@ -63,7 +67,7 @@ export const SuspenseImpl = {
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
// platform-specific impl passed from renderer
@ -76,7 +80,7 @@ export const SuspenseImpl = {
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized,
rendererInternals
@ -88,7 +92,7 @@ export const SuspenseImpl = {
container,
anchor,
parentComponent,
isSVG,
namespace,
slotScopeIds,
optimized,
rendererInternals
@ -130,7 +134,7 @@ function mountSuspense(
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals
@ -147,7 +151,7 @@ function mountSuspense(
container,
hiddenContainer,
anchor,
isSVG,
namespace,
slotScopeIds,
optimized,
rendererInternals
@ -161,7 +165,7 @@ function mountSuspense(
null,
parentComponent,
suspense,
isSVG,
namespace,
slotScopeIds
)
// now check if we have encountered any async deps
@ -179,7 +183,7 @@ function mountSuspense(
anchor,
parentComponent,
null, // fallback tree will not have suspense context
isSVG,
namespace,
slotScopeIds
)
setActiveBranch(suspense, vnode.ssFallback!)
@ -195,7 +199,7 @@ function patchSuspense(
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
{ p: patch, um: unmount, o: { createElement } }: RendererInternals
@ -218,29 +222,38 @@ function patchSuspense(
null,
parentComponent,
suspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
if (suspense.deps <= 0) {
suspense.resolve()
} else if (isInFallback) {
patch(
activeBranch,
newFallback,
container,
anchor,
parentComponent,
null, // fallback tree will not have suspense context
isSVG,
slotScopeIds,
optimized
)
setActiveBranch(suspense, newFallback)
// It's possible that the app is in hydrating state when patching the
// suspense instance. If someone updates the dependency during component
// setup in children of suspense boundary, that would be problemtic
// because we aren't actually showing a fallback content when
// patchSuspense is called. In such case, patch of fallback content
// should be no op
if (!isHydrating) {
patch(
activeBranch,
newFallback,
container,
anchor,
parentComponent,
null, // fallback tree will not have suspense context
namespace,
slotScopeIds,
optimized
)
setActiveBranch(suspense, newFallback)
}
}
} else {
// toggled before pending tree is resolved
suspense.pendingId++
// increment pending ID. this is used to invalidate async callbacks
suspense.pendingId = suspenseId++
if (isHydrating) {
// if toggled before hydration is finished, the current DOM tree is
// no longer valid. set it as the active branch so it will be unmounted
@ -250,7 +263,6 @@ function patchSuspense(
} else {
unmount(pendingBranch, parentComponent, suspense)
}
// increment pending ID. this is used to invalidate async callbacks
// reset suspense state
suspense.deps = 0
// discard effects from pending branch
@ -267,7 +279,7 @@ function patchSuspense(
null,
parentComponent,
suspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -281,7 +293,7 @@ function patchSuspense(
anchor,
parentComponent,
null, // fallback tree will not have suspense context
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -296,7 +308,7 @@ function patchSuspense(
anchor,
parentComponent,
suspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -311,7 +323,7 @@ function patchSuspense(
null,
parentComponent,
suspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -330,7 +342,7 @@ function patchSuspense(
anchor,
parentComponent,
suspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -341,7 +353,11 @@ function patchSuspense(
triggerEvent(n2, 'onPending')
// mount pending branch in off-dom container
suspense.pendingBranch = newBranch
suspense.pendingId++
if (newBranch.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
suspense.pendingId = newBranch.component!.suspenseId!
} else {
suspense.pendingId = suspenseId++
}
patch(
null,
newBranch,
@ -349,7 +365,7 @@ function patchSuspense(
null,
parentComponent,
suspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -376,7 +392,7 @@ export interface SuspenseBoundary {
vnode: VNode<RendererNode, RendererElement, SuspenseProps>
parent: SuspenseBoundary | null
parentComponent: ComponentInternalInstance | null
isSVG: boolean
namespace: ElementNamespace
container: RendererElement
hiddenContainer: RendererElement
anchor: RendererNode | null
@ -413,7 +429,7 @@ function createSuspenseBoundary(
container: RendererElement,
hiddenContainer: RendererElement,
anchor: RendererNode | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals,
@ -455,7 +471,7 @@ function createSuspenseBoundary(
vnode,
parent: parentSuspense,
parentComponent,
isSVG,
namespace,
container,
hiddenContainer,
anchor,
@ -464,7 +480,7 @@ function createSuspenseBoundary(
timeout: typeof timeout === 'number' ? timeout : -1,
activeBranch: null,
pendingBranch: null,
isInFallback: true,
isInFallback: !isHydrating,
isHydrating,
isUnmounted: false,
effects: [],
@ -576,12 +592,13 @@ function createSuspenseBoundary(
return
}
const { vnode, activeBranch, parentComponent, container, isSVG } =
const { vnode, activeBranch, parentComponent, container, namespace } =
suspense
// invoke @fallback event
triggerEvent(vnode, 'onFallback')
const anchor = next(activeBranch!)
const mountFallback = () => {
if (!suspense.isInFallback) {
return
@ -591,10 +608,10 @@ function createSuspenseBoundary(
null,
fallbackVNode,
container,
next(activeBranch!),
anchor,
parentComponent,
null, // fallback tree will not have suspense context
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -675,7 +692,7 @@ function createSuspenseBoundary(
// consider the comment placeholder case.
hydratedEl ? null : next(instance.subTree),
suspense,
isSVG,
namespace,
optimized
)
if (placeholder) {
@ -721,7 +738,7 @@ function hydrateSuspense(
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals,
@ -742,7 +759,7 @@ function hydrateSuspense(
node.parentNode!,
document.createElement('div'),
null,
isSVG,
namespace,
slotScopeIds,
optimized,
rendererInternals,

View File

@ -6,7 +6,8 @@ import {
RendererElement,
RendererNode,
RendererOptions,
traverseStaticChildren
traverseStaticChildren,
ElementNamespace
} from '../renderer'
import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
import { isString, ShapeFlags } from '@vue/shared'
@ -28,6 +29,9 @@ const isTeleportDisabled = (props: VNode['props']): boolean =>
const isTargetSVG = (target: RendererElement): boolean =>
typeof SVGElement !== 'undefined' && target instanceof SVGElement
const isTargetMathML = (target: RendererElement): boolean =>
typeof MathMLElement === 'function' && target instanceof MathMLElement
const resolveTarget = <T = RendererElement>(
props: TeleportProps | null,
select: RendererOptions['querySelector']
@ -72,7 +76,7 @@ export const TeleportImpl = {
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
internals: RendererInternals
@ -109,7 +113,11 @@ export const TeleportImpl = {
if (target) {
insert(targetAnchor, target)
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
isSVG = isSVG || isTargetSVG(target)
if (namespace === 'svg' || isTargetSVG(target)) {
namespace = 'svg'
} else if (namespace === 'mathml' || isTargetMathML(target)) {
namespace = 'mathml'
}
} else if (__DEV__ && !disabled) {
warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
}
@ -124,7 +132,7 @@ export const TeleportImpl = {
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -145,7 +153,12 @@ export const TeleportImpl = {
const wasDisabled = isTeleportDisabled(n1.props)
const currentContainer = wasDisabled ? container : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
isSVG = isSVG || isTargetSVG(target)
if (namespace === 'svg' || isTargetSVG(target)) {
namespace = 'svg'
} else if (namespace === 'mathml' || isTargetMathML(target)) {
namespace = 'mathml'
}
if (dynamicChildren) {
// fast path when the teleport happens to be a block root
@ -155,7 +168,7 @@ export const TeleportImpl = {
currentContainer,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds
)
// even in block tree mode we need to make sure all root-level nodes
@ -170,7 +183,7 @@ export const TeleportImpl = {
currentAnchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
false
)

View File

@ -20,6 +20,11 @@ export function initFeatureFlags() {
getGlobalThis().__VUE_PROD_DEVTOOLS__ = false
}
if (typeof __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ !== 'boolean') {
__DEV__ && needWarn.push(`__VUE_PROD_HYDRATION_MISMATCH_DETAILS__`)
getGlobalThis().__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false
}
if (__DEV__ && needWarn.length) {
const multi = needWarn.length > 1
console.warn(

View File

@ -75,10 +75,27 @@ interface Constructor<P = any> {
new (...args: any[]): { $props: P }
}
type HTMLElementEventHandler = {
[K in keyof HTMLElementEventMap as `on${Capitalize<K>}`]?: (
ev: HTMLElementEventMap[K]
) => any
}
// The following is a series of overloads for providing props validation of
// manually written render functions.
// element
export function h<K extends keyof HTMLElementTagNameMap>(
type: K,
children?: RawChildren
): VNode
export function h<K extends keyof HTMLElementTagNameMap>(
type: K,
props?: (RawProps & HTMLElementEventHandler) | null,
children?: RawChildren | RawSlots
): VNode
// custom element
export function h(type: string, children?: RawChildren): VNode
export function h(
type: string,

View File

@ -14,7 +14,20 @@ import { flushPostFlushCbs } from './scheduler'
import { ComponentInternalInstance } from './component'
import { invokeDirectiveHook } from './directives'
import { warn } from './warning'
import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
import {
PatchFlags,
ShapeFlags,
isReservedProp,
isOn,
normalizeClass,
normalizeStyle,
stringifyStyle,
isBooleanAttr,
isString,
includeBooleanAttr,
isKnownHtmlAttr,
isKnownSvgAttr
} from '@vue/shared'
import { needTransition, RendererInternals } from './renderer'
import { setRef } from './rendererTemplateRef'
import {
@ -39,7 +52,17 @@ enum DOMNodeTypes {
let hasMismatch = false
const isSVGContainer = (container: Element) =>
/svg/.test(container.namespaceURI!) && container.tagName !== 'foreignObject'
container.namespaceURI!.includes('svg') &&
container.tagName !== 'foreignObject'
const isMathMLContainer = (container: Element) =>
container.namespaceURI!.includes('MathML')
const getContainerType = (container: Element): 'svg' | 'mathml' | undefined => {
if (isSVGContainer(container)) return 'svg'
if (isMathMLContainer(container)) return 'mathml'
return undefined
}
const isComment = (node: Node): node is Comment =>
node.nodeType === DOMNodeTypes.COMMENT
@ -68,7 +91,7 @@ export function createHydrationFunctions(
const hydrate: RootHydrateFunction = (vnode, container) => {
if (!container.hasChildNodes()) {
__DEV__ &&
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
warn(
`Attempting to hydrate existing markup but container is empty. ` +
`Performing full mount instead.`
@ -146,13 +169,14 @@ export function createHydrationFunctions(
} else {
if ((node as Text).data !== vnode.children) {
hasMismatch = true
__DEV__ &&
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
warn(
`Hydration text mismatch:` +
`\n- Server rendered: ${JSON.stringify(
(node as Text).data
)}` +
`\n- Client rendered: ${JSON.stringify(vnode.children)}`
`Hydration text mismatch in`,
node.parentNode,
`\n - rendered on server: ${JSON.stringify(
(node as Text).data
)}` +
`\n - expected on client: ${JSON.stringify(vnode.children)}`
)
;(node as Text).data = vnode.children as string
}
@ -263,7 +287,7 @@ export function createHydrationFunctions(
null,
parentComponent,
parentSuspense,
isSVGContainer(container),
getContainerType(container),
optimized
)
@ -306,13 +330,13 @@ export function createHydrationFunctions(
vnode,
parentComponent,
parentSuspense,
isSVGContainer(parentNode(node)!),
getContainerType(parentNode(node)!),
slotScopeIds,
optimized,
rendererInternals,
hydrateNode
)
} else if (__DEV__) {
} else if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
warn('Invalid HostVNode type:', type, `(${typeof type})`)
}
}
@ -344,51 +368,6 @@ export function createHydrationFunctions(
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
if (props) {
if (
forcePatch ||
!optimized ||
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
) {
for (const key in props) {
if (
(forcePatch &&
(key.endsWith('value') || key === 'indeterminate')) ||
(isOn(key) && !isReservedProp(key)) ||
// force hydrate v-bind with .prop modifiers
key[0] === '.'
) {
patchProp(
el,
key,
null,
props[key],
false,
undefined,
parentComponent
)
}
}
} else if (props.onClick) {
// Fast path for click listeners (which is most often) to avoid
// iterating through props.
patchProp(
el,
'onClick',
null,
props.onClick,
false,
undefined,
parentComponent
)
}
}
// vnode / directive hooks
let vnodeHooks: VNodeHook | null | undefined
if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHooks, parentComponent, vnode)
}
// handle appear transition
let needCallTransitionHooks = false
@ -411,21 +390,6 @@ export function createHydrationFunctions(
vnode.el = el = content
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
if (
(vnodeHooks = props && props.onVnodeMounted) ||
dirs ||
needCallTransitionHooks
) {
queueEffectWithSuspense(() => {
vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
// children
if (
shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
@ -444,10 +408,14 @@ export function createHydrationFunctions(
let hasWarned = false
while (next) {
hasMismatch = true
if (__DEV__ && !hasWarned) {
if (
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
!hasWarned
) {
warn(
`Hydration children mismatch in <${vnode.type as string}>: ` +
`server rendered element contains more child nodes than client vdom.`
`Hydration children mismatch on`,
el,
`\nServer rendered element contains more child nodes than client vdom.`
)
hasWarned = true
}
@ -459,18 +427,84 @@ export function createHydrationFunctions(
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (el.textContent !== vnode.children) {
hasMismatch = true
__DEV__ &&
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
warn(
`Hydration text content mismatch in <${
vnode.type as string
}>:\n` +
`- Server rendered: ${el.textContent}\n` +
`- Client rendered: ${vnode.children as string}`
`Hydration text content mismatch on`,
el,
`\n - rendered on server: ${el.textContent}` +
`\n - expected on client: ${vnode.children as string}`
)
el.textContent = vnode.children as string
}
}
// props
if (props) {
if (
__DEV__ ||
forcePatch ||
!optimized ||
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
) {
for (const key in props) {
// check hydration mismatch
if (__DEV__ && propHasMismatch(el, key, props[key])) {
hasMismatch = true
}
if (
(forcePatch &&
(key.endsWith('value') || key === 'indeterminate')) ||
(isOn(key) && !isReservedProp(key)) ||
// force hydrate v-bind with .prop modifiers
key[0] === '.'
) {
patchProp(
el,
key,
null,
props[key],
undefined,
undefined,
parentComponent
)
}
}
} else if (props.onClick) {
// Fast path for click listeners (which is most often) to avoid
// iterating through props.
patchProp(
el,
'onClick',
null,
props.onClick,
undefined,
undefined,
parentComponent
)
}
}
// vnode / directive hooks
let vnodeHooks: VNodeHook | null | undefined
if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHooks, parentComponent, vnode)
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
if (
(vnodeHooks = props && props.onVnodeMounted) ||
dirs ||
needCallTransitionHooks
) {
queueEffectWithSuspense(() => {
vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
}
return el.nextSibling
}
@ -504,10 +538,14 @@ export function createHydrationFunctions(
continue
} else {
hasMismatch = true
if (__DEV__ && !hasWarned) {
if (
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
!hasWarned
) {
warn(
`Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
`server rendered element contains fewer child nodes than client vdom.`
`Hydration children mismatch on`,
container,
`\nServer rendered element contains fewer child nodes than client vdom.`
)
hasWarned = true
}
@ -519,7 +557,7 @@ export function createHydrationFunctions(
null,
parentComponent,
parentSuspense,
isSVGContainer(container),
getContainerType(container),
slotScopeIds
)
}
@ -573,17 +611,17 @@ export function createHydrationFunctions(
isFragment: boolean
): Node | null => {
hasMismatch = true
__DEV__ &&
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
warn(
`Hydration node mismatch:\n- Client vnode:`,
vnode.type,
`\n- Server rendered DOM:`,
`Hydration node mismatch:\n- rendered on server:`,
node,
node.nodeType === DOMNodeTypes.TEXT
? `(text)`
: isComment(node) && node.data === '['
? `(start of fragment)`
: ``
: ``,
`\n- expected on client:`,
vnode.type
)
vnode.el = null
@ -611,7 +649,7 @@ export function createHydrationFunctions(
next,
parentComponent,
parentSuspense,
isSVGContainer(container),
getContainerType(container),
slotScopeIds
)
return next
@ -670,3 +708,60 @@ export function createHydrationFunctions(
return [hydrate, hydrateNode] as const
}
/**
* Dev only
*/
function propHasMismatch(el: Element, key: string, clientValue: any): boolean {
let mismatchType: string | undefined
let mismatchKey: string | undefined
let actual: any
let expected: any
if (key === 'class') {
actual = el.className
expected = normalizeClass(clientValue)
if (actual !== expected) {
mismatchType = mismatchKey = `class`
}
} else if (key === 'style') {
actual = el.getAttribute('style')
expected = isString(clientValue)
? clientValue
: stringifyStyle(normalizeStyle(clientValue))
if (actual !== expected) {
mismatchType = mismatchKey = 'style'
}
} else if (
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
(el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
) {
actual = el.hasAttribute(key) && el.getAttribute(key)
expected = isBooleanAttr(key)
? includeBooleanAttr(clientValue)
? ''
: false
: clientValue == null
? false
: String(clientValue)
if (actual !== expected) {
mismatchType = `attribute`
mismatchKey = key
}
}
if (mismatchType) {
const format = (v: any) =>
v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
warn(
`Hydration ${mismatchType} mismatch on`,
el,
`\n - rendered on server: ${format(actual)}` +
`\n - expected on client: ${format(expected)}` +
`\n Note: this mismatch is check-only. The DOM will not be rectified ` +
`in production due to performance overhead.` +
`\n You should fix the source of the mismatch.`
)
return true
}
return false
}

View File

@ -60,7 +60,7 @@ export { provide, inject, hasInjectionContext } from './apiInject'
export { nextTick } from './scheduler'
export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent'
export { useAttrs, useSlots } from './apiSetupHelpers'
export { useAttrs, useSlots, type DefineProps } from './apiSetupHelpers'
// <script setup> API ----------------------------------------------------------
@ -212,6 +212,8 @@ export type {
AppConfig,
AppContext,
Plugin,
ObjectPlugin,
FunctionPlugin,
CreateAppFunction,
OptionMergeFunction
} from './apiCreateApp'
@ -230,9 +232,10 @@ export type {
ComponentInternalInstance,
SetupContext,
ComponentCustomProps,
AllowedComponentProps
AllowedComponentProps,
ComponentInstance
} from './component'
export type { DefineComponent } from './apiDefineComponent'
export type { DefineComponent, PublicProps } from './apiDefineComponent'
export type {
ComponentOptions,
ComponentOptionsMixin,
@ -260,7 +263,8 @@ export type {
RendererElement,
HydrationRenderer,
RendererOptions,
RootRenderFunction
RootRenderFunction,
ElementNamespace
} from './renderer'
export type { RootHydrateFunction } from './hydration'
export type { Slot, Slots, SlotsType } from './componentSlots'

View File

@ -83,10 +83,12 @@ export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
hydrate: RootHydrateFunction
}
export type ElementNamespace = 'svg' | 'mathml' | undefined
export type RootRenderFunction<HostElement = RendererElement> = (
vnode: VNode | null,
container: HostElement,
isSVG?: boolean
namespace?: ElementNamespace
) => void
export interface RendererOptions<
@ -98,7 +100,7 @@ export interface RendererOptions<
key: string,
prevValue: any,
nextValue: any,
isSVG?: boolean,
namespace?: ElementNamespace,
prevChildren?: VNode<HostNode, HostElement>[],
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
@ -108,7 +110,7 @@ export interface RendererOptions<
remove(el: HostNode): void
createElement(
type: string,
isSVG?: boolean,
namespace?: ElementNamespace,
isCustomizedBuiltIn?: string,
vnodeProps?: (VNodeProps & { [key: string]: any }) | null
): HostElement
@ -125,7 +127,7 @@ export interface RendererOptions<
content: string,
parent: HostElement,
anchor: HostNode | null,
isSVG: boolean,
namespace: ElementNamespace,
start?: HostNode | null,
end?: HostNode | null
): [HostNode, HostNode]
@ -170,7 +172,7 @@ type PatchFn = (
anchor?: RendererNode | null,
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
isSVG?: boolean,
namespace?: ElementNamespace,
slotScopeIds?: string[] | null,
optimized?: boolean
) => void
@ -181,7 +183,7 @@ type MountChildrenFn = (
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
start?: number
@ -194,7 +196,7 @@ type PatchChildrenFn = (
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => void
@ -205,7 +207,7 @@ type PatchBlockChildrenFn = (
fallbackContainer: RendererElement,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null
) => void
@ -244,7 +246,7 @@ export type MountComponentFn = (
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
optimized: boolean
) => void
@ -261,7 +263,7 @@ export type SetupRenderEffectFn = (
container: RendererElement,
anchor: RendererNode | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
optimized: boolean
) => void
@ -362,7 +364,7 @@ function baseCreateRenderer(
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
namespace = undefined,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
@ -392,9 +394,9 @@ function baseCreateRenderer(
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
mountStaticNode(n2, container, anchor, namespace)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
patchStaticNode(n1, n2, container, namespace)
}
break
case Fragment:
@ -405,7 +407,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -419,7 +421,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -431,7 +433,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -443,7 +445,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized,
internals
@ -456,7 +458,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized,
internals
@ -509,7 +511,7 @@ function baseCreateRenderer(
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
isSVG: boolean
namespace: ElementNamespace
) => {
// static nodes are only present when used with compiler-dom/runtime-dom
// which guarantees presence of hostInsertStaticContent.
@ -517,7 +519,7 @@ function baseCreateRenderer(
n2.children as string,
container,
anchor,
isSVG,
namespace,
n2.el,
n2.anchor
)
@ -530,7 +532,7 @@ function baseCreateRenderer(
n1: VNode,
n2: VNode,
container: RendererElement,
isSVG: boolean
namespace: ElementNamespace
) => {
// static nodes are only patched during dev for HMR
if (n2.children !== n1.children) {
@ -542,7 +544,7 @@ function baseCreateRenderer(
n2.children as string,
container,
anchor,
isSVG
namespace
)
} else {
n2.el = n1.el
@ -581,11 +583,16 @@ function baseCreateRenderer(
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || n2.type === 'svg'
if (n2.type === 'svg') {
namespace = 'svg'
} else if (n2.type === 'math') {
namespace = 'mathml'
}
if (n1 == null) {
mountElement(
n2,
@ -593,7 +600,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -603,7 +610,7 @@ function baseCreateRenderer(
n2,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -616,17 +623,17 @@ function baseCreateRenderer(
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, dirs } = vnode
const { props, shapeFlag, transition, dirs } = vnode
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
namespace,
props && props.is,
props
)
@ -642,7 +649,7 @@ function baseCreateRenderer(
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
resolveChildrenNamespace(vnode, namespace),
slotScopeIds,
optimized
)
@ -662,7 +669,7 @@ function baseCreateRenderer(
key,
null,
props[key],
isSVG,
namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
@ -680,7 +687,7 @@ function baseCreateRenderer(
* affect non-DOM renderers)
*/
if ('value' in props) {
hostPatchProp(el, 'value', null, props.value)
hostPatchProp(el, 'value', null, props.value, namespace)
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
@ -764,7 +771,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace: ElementNamespace,
slotScopeIds,
optimized,
start = 0
@ -780,7 +787,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -792,7 +799,7 @@ function baseCreateRenderer(
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
@ -822,7 +829,6 @@ function baseCreateRenderer(
dynamicChildren = null
}
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
if (dynamicChildren) {
patchBlockChildren(
n1.dynamicChildren!,
@ -830,7 +836,7 @@ function baseCreateRenderer(
el,
parentComponent,
parentSuspense,
areChildrenSVG,
resolveChildrenNamespace(n2, namespace),
slotScopeIds
)
if (__DEV__) {
@ -846,7 +852,7 @@ function baseCreateRenderer(
null,
parentComponent,
parentSuspense,
areChildrenSVG,
resolveChildrenNamespace(n2, namespace),
slotScopeIds,
false
)
@ -866,21 +872,21 @@ function baseCreateRenderer(
newProps,
parentComponent,
parentSuspense,
isSVG
namespace
)
} else {
// class
// this flag is matched when the element has dynamic class bindings.
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
hostPatchProp(el, 'class', null, newProps.class, namespace)
}
}
// style
// this flag is matched when the element has dynamic style bindings
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
hostPatchProp(el, 'style', oldProps.style, newProps.style, namespace)
}
// props
@ -903,7 +909,7 @@ function baseCreateRenderer(
key,
prev,
next,
isSVG,
namespace,
n1.children as VNode[],
parentComponent,
parentSuspense,
@ -930,7 +936,7 @@ function baseCreateRenderer(
newProps,
parentComponent,
parentSuspense,
isSVG
namespace
)
}
@ -949,7 +955,7 @@ function baseCreateRenderer(
fallbackContainer,
parentComponent,
parentSuspense,
isSVG,
namespace: ElementNamespace,
slotScopeIds
) => {
for (let i = 0; i < newChildren.length; i++) {
@ -979,7 +985,7 @@ function baseCreateRenderer(
null,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
true
)
@ -993,7 +999,7 @@ function baseCreateRenderer(
newProps: Data,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean
namespace: ElementNamespace
) => {
if (oldProps !== newProps) {
if (oldProps !== EMPTY_OBJ) {
@ -1004,7 +1010,7 @@ function baseCreateRenderer(
key,
oldProps[key],
null,
isSVG,
namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
@ -1025,7 +1031,7 @@ function baseCreateRenderer(
key,
prev,
next,
isSVG,
namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
@ -1034,7 +1040,7 @@ function baseCreateRenderer(
}
}
if ('value' in newProps) {
hostPatchProp(el, 'value', oldProps.value, newProps.value)
hostPatchProp(el, 'value', oldProps.value, newProps.value, namespace)
}
}
}
@ -1046,7 +1052,7 @@ function baseCreateRenderer(
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
@ -1085,7 +1091,7 @@ function baseCreateRenderer(
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1106,7 +1112,7 @@ function baseCreateRenderer(
container,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds
)
if (__DEV__) {
@ -1134,7 +1140,7 @@ function baseCreateRenderer(
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1149,7 +1155,7 @@ function baseCreateRenderer(
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
@ -1160,7 +1166,7 @@ function baseCreateRenderer(
n2,
container,
anchor,
isSVG,
namespace,
optimized
)
} else {
@ -1170,7 +1176,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
optimized
)
}
@ -1185,7 +1191,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace: ElementNamespace,
optimized
) => {
// 2.x compat may pre-create the component instance before actually
@ -1236,19 +1242,18 @@ function baseCreateRenderer(
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
}
return
} else {
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace,
optimized
)
}
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
@ -1296,7 +1301,7 @@ function baseCreateRenderer(
container,
anchor,
parentSuspense,
isSVG,
namespace: ElementNamespace,
optimized
) => {
const componentUpdateFn = () => {
@ -1380,7 +1385,7 @@ function baseCreateRenderer(
anchor,
instance,
parentSuspense,
isSVG
namespace
)
if (__DEV__) {
endMeasure(instance, `patch`)
@ -1441,10 +1446,32 @@ function baseCreateRenderer(
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} else {
let { next, bu, u, parent, vnode } = instance
if (__FEATURE_SUSPENSE__) {
const nonHydratedAsyncRoot = locateNonHydratedAsyncRoot(instance)
// we are trying to update some async comp before hydration
// this will cause crash because we don't know the root node yet
if (nonHydratedAsyncRoot) {
// only sync the properties and abort the rest of operations
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
}
// and continue the rest of operations once the deps are resolved
nonHydratedAsyncRoot.asyncDep!.then(() => {
// the instance may be destroyed during the time period
if (!instance.isUnmounted) {
componentUpdateFn()
}
})
return
}
}
// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
if (__DEV__) {
@ -1499,7 +1526,7 @@ function baseCreateRenderer(
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
namespace
)
if (__DEV__) {
endMeasure(instance, `patch`)
@ -1588,7 +1615,7 @@ function baseCreateRenderer(
pauseTracking()
// props update may have triggered pre-flush watchers.
// flush them before the render update.
flushPreFlushCbs()
flushPreFlushCbs(instance)
resetTracking()
}
@ -1599,7 +1626,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace: ElementNamespace,
slotScopeIds,
optimized = false
) => {
@ -1620,7 +1647,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1634,7 +1661,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1663,7 +1690,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1685,7 +1712,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1701,7 +1728,7 @@ function baseCreateRenderer(
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
@ -1722,7 +1749,7 @@ function baseCreateRenderer(
null,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1745,7 +1772,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized,
commonLength
@ -1761,7 +1788,7 @@ function baseCreateRenderer(
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
@ -1786,7 +1813,7 @@ function baseCreateRenderer(
null,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1812,7 +1839,7 @@ function baseCreateRenderer(
null,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1844,7 +1871,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1947,7 +1974,7 @@ function baseCreateRenderer(
null,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -1976,7 +2003,7 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
isSVG,
namespace,
slotScopeIds,
optimized
)
@ -2321,13 +2348,21 @@ function baseCreateRenderer(
return hostNextSibling((vnode.anchor || vnode.el)!)
}
const render: RootRenderFunction = (vnode, container, isSVG) => {
const render: RootRenderFunction = (vnode, container, namespace) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace
)
}
flushPreFlushCbs()
flushPostFlushCbs()
@ -2362,6 +2397,20 @@ function baseCreateRenderer(
}
}
function resolveChildrenNamespace(
{ type, props }: VNode,
currentNamespace: ElementNamespace
): ElementNamespace {
return (currentNamespace === 'svg' && type === 'foreignObject') ||
(currentNamespace === 'mathml' &&
type === 'annotation-xml' &&
props &&
props.encoding &&
props.encoding.includes('html'))
? undefined
: currentNamespace
}
function toggleRecurse(
{ effect, update }: ComponentInternalInstance,
allowed: boolean
@ -2461,3 +2510,16 @@ function getSequence(arr: number[]): number[] {
}
return result
}
function locateNonHydratedAsyncRoot(
instance: ComponentInternalInstance
): ComponentInternalInstance | undefined {
const subComponent = instance.subTree.component
if (subComponent) {
if (subComponent.asyncDep && !subComponent.asyncResolved) {
return subComponent
} else {
return locateNonHydratedAsyncRoot(subComponent)
}
}
}

View File

@ -139,6 +139,7 @@ export function queuePostFlushCb(cb: SchedulerJobs) {
}
export function flushPreFlushCbs(
instance?: ComponentInternalInstance,
seen?: CountMap,
// if currently flushing, skip the current job itself
i = isFlushing ? flushIndex + 1 : 0
@ -149,6 +150,9 @@ export function flushPreFlushCbs(
for (; i < queue.length; i++) {
const cb = queue[i]
if (cb && cb.pre) {
if (instance && cb.id !== instance.uid) {
continue
}
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}

View File

@ -293,4 +293,35 @@ describe('useCssVars', () => {
await nextTick()
expect(target.children.length).toBe(0)
})
test('with string style', async () => {
document.body.innerHTML = ''
const state = reactive({ color: 'red' })
const root = document.createElement('div')
const disabled = ref(false)
const App = {
setup() {
useCssVars(() => state)
return () => [
h(
'div',
{ style: disabled.value ? 'pointer-events: none' : undefined },
'foo'
)
]
}
}
render(h(App), root)
await nextTick()
for (const c of [].slice.call(root.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
}
disabled.value = true
await nextTick()
for (const c of [].slice.call(root.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
}
})
})

View File

@ -2,7 +2,7 @@ import { nodeOps, svgNS } from '../src/nodeOps'
describe('runtime-dom: node-ops', () => {
test("the <select>'s multiple attr should be set in createElement", () => {
const el = nodeOps.createElement('select', false, undefined, {
const el = nodeOps.createElement('select', undefined, undefined, {
multiple: ''
}) as HTMLSelectElement
const option1 = nodeOps.createElement('option') as HTMLOptionElement
@ -21,7 +21,12 @@ describe('runtime-dom: node-ops', () => {
test('fresh insertion', () => {
const content = `<div>one</div><div>two</div>three`
const parent = document.createElement('div')
const nodes = nodeOps.insertStaticContent!(content, parent, null, false)
const nodes = nodeOps.insertStaticContent!(
content,
parent,
null,
undefined
)
expect(parent.innerHTML).toBe(content)
expect(nodes[0]).toBe(parent.firstChild)
expect(nodes[1]).toBe(parent.lastChild)
@ -33,7 +38,12 @@ describe('runtime-dom: node-ops', () => {
const parent = document.createElement('div')
parent.innerHTML = existing
const anchor = parent.firstChild
const nodes = nodeOps.insertStaticContent!(content, parent, anchor, false)
const nodes = nodeOps.insertStaticContent!(
content,
parent,
anchor,
undefined
)
expect(parent.innerHTML).toBe(content + existing)
expect(nodes[0]).toBe(parent.firstChild)
expect(nodes[1]).toBe(parent.childNodes[parent.childNodes.length - 2])
@ -46,7 +56,7 @@ describe('runtime-dom: node-ops', () => {
content,
parent,
null,
true
'svg'
)
expect(parent.innerHTML).toBe(content)
expect(first).toBe(parent.firstChild)
@ -65,7 +75,7 @@ describe('runtime-dom: node-ops', () => {
content,
parent,
anchor,
true
'svg'
)
expect(parent.innerHTML).toBe(content + existing)
expect(first).toBe(parent.firstChild)
@ -88,7 +98,7 @@ describe('runtime-dom: node-ops', () => {
content,
parent,
anchor,
false,
undefined,
cached.firstChild,
cached.lastChild
)

View File

@ -4,15 +4,15 @@ import { xlinkNS } from '../src/modules/attrs'
describe('runtime-dom: attrs patching', () => {
test('xlink attributes', () => {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'use')
patchProp(el, 'xlink:href', null, 'a', true)
patchProp(el, 'xlink:href', null, 'a', 'svg')
expect(el.getAttributeNS(xlinkNS, 'href')).toBe('a')
patchProp(el, 'xlink:href', 'a', null, true)
patchProp(el, 'xlink:href', 'a', null, 'svg')
expect(el.getAttributeNS(xlinkNS, 'href')).toBe(null)
})
test('textContent attributes /w svg', () => {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'use')
patchProp(el, 'textContent', null, 'foo', true)
patchProp(el, 'textContent', null, 'foo', 'svg')
expect(el.attributes.length).toBe(0)
expect(el.innerHTML).toBe('foo')
})

View File

@ -25,7 +25,7 @@ describe('runtime-dom: class patching', () => {
test('svg', () => {
const el = document.createElementNS(svgNS, 'svg')
patchProp(el, 'class', null, 'foo', true)
patchProp(el, 'class', null, 'foo', 'svg')
expect(el.getAttribute('class')).toBe('foo')
})
})

View File

@ -300,6 +300,13 @@ describe('runtime-dom: props patching', () => {
expect(el.getAttribute('width')).toBe('24px')
})
// # 9762 should fallthrough to `key in el` logic for non embedded tags
test('width and height on custom elements', () => {
const el = document.createElement('foobar')
patchProp(el, 'width', null, '24px')
expect(el.getAttribute('width')).toBe('24px')
})
test('translate attribute', () => {
const el = document.createElement('div')
patchProp(el, 'translate', null, 'no')

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-dom",
"version": "3.4.0-alpha.4",
"version": "3.4.0-beta.3",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",
@ -37,6 +37,6 @@
"dependencies": {
"@vue/shared": "workspace:*",
"@vue/runtime-core": "workspace:*",
"csstype": "^3.1.2"
"csstype": "^3.1.3"
}
}

View File

@ -10,6 +10,7 @@ import {
} from '@vue/runtime-core'
import { ShapeFlags } from '@vue/shared'
export const CSS_VAR_TEXT = Symbol(__DEV__ ? 'CSS_VAR_TEXT' : '')
/**
* Runtime helper for SFC's CSS variable injection feature.
* @private
@ -79,8 +80,11 @@ function setVarsOnVNode(vnode: VNode, vars: Record<string, string>) {
function setVarsOnNode(el: Node, vars: Record<string, string>) {
if (el.nodeType === 1) {
const style = (el as HTMLElement).style
let cssText = ''
for (const key in vars) {
style.setProperty(`--${key}`, vars[key])
cssText += `--${key}: ${vars[key]};`
}
;(style as any)[CSS_VAR_TEXT] = cssText
}
}

View File

@ -10,7 +10,8 @@ import {
RootHydrateFunction,
isRuntimeOnly,
DeprecationTypes,
compatUtils
compatUtils,
ElementNamespace
} from '@vue/runtime-core'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
@ -21,7 +22,8 @@ import {
isHTMLTag,
isSVGTag,
extend,
NOOP
NOOP,
isMathMLTag
} from '@vue/shared'
declare module '@vue/reactivity' {
@ -99,7 +101,7 @@ export const createApp = ((...args) => {
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container, false, container instanceof SVGElement)
const proxy = mount(container, false, resolveRootNamespace(container))
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
@ -122,18 +124,30 @@ export const createSSRApp = ((...args) => {
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector)
if (container) {
return mount(container, true, container instanceof SVGElement)
return mount(container, true, resolveRootNamespace(container))
}
}
return app
}) as CreateAppFunction<Element>
function resolveRootNamespace(container: Element): ElementNamespace {
if (container instanceof SVGElement) {
return 'svg'
}
if (
typeof MathMLElement === 'function' &&
container instanceof MathMLElement
) {
return 'mathml'
}
}
function injectNativeTagCheck(app: App) {
// Inject `isNativeTag`
// this is used for component name validation (dev only)
Object.defineProperty(app.config, 'isNativeTag', {
value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag),
value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag) || isMathMLTag(tag),
writable: false
})
}

View File

@ -46,7 +46,7 @@ type Booleanish = boolean | 'true' | 'false'
type Numberish = number | string
// All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/
interface AriaAttributes {
export interface AriaAttributes {
/** Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. */
'aria-activedescendant'?: string
/** Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute. */
@ -1077,6 +1077,7 @@ export interface SVGAttributes extends AriaAttributes, EventHandlers<Events> {
xlinkTitle?: string
xlinkType?: string
xmlns?: string
xmlnsXlink?: string
y1?: Numberish
y2?: Numberish
y?: Numberish

View File

@ -1,6 +1,7 @@
import { isString, hyphenate, capitalize, isArray } from '@vue/shared'
import { camelize, warn } from '@vue/runtime-core'
import { vShowOldKey } from '../directives/vShow'
import { CSS_VAR_TEXT } from '../helpers/useCssVars'
type Style = string | Record<string, string | string[]> | null
@ -22,6 +23,11 @@ export function patchStyle(el: Element, prev: Style, next: Style) {
const currentDisplay = style.display
if (isCssString) {
if (prev !== next) {
// #9821
const cssVarText = (style as any)[CSS_VAR_TEXT]
if (cssVarText) {
;(next as string) += ';' + cssVarText
}
style.cssText = next as string
}
} else if (prev) {

View File

@ -1,6 +1,7 @@
import { RendererOptions } from '@vue/runtime-core'
export const svgNS = 'http://www.w3.org/2000/svg'
export const mathmlNS = 'http://www.w3.org/1998/Math/MathML'
const doc = (typeof document !== 'undefined' ? document : null) as Document
@ -18,10 +19,13 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
}
},
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
createElement: (tag, namespace, is, props): Element => {
const el =
namespace === 'svg'
? doc.createElementNS(svgNS, tag)
: namespace === 'mathml'
? doc.createElementNS(mathmlNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
@ -56,7 +60,7 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
// Reason: innerHTML.
// Static content here can only come from compiled templates.
// As long as the user only uses trusted templates, this is safe.
insertStaticContent(content, parent, anchor, isSVG, start, end) {
insertStaticContent(content, parent, anchor, namespace, start, end) {
// <parent> before | first ... last | anchor </parent>
const before = anchor ? anchor.previousSibling : parent.lastChild
// #5308 can only take cached path if:
@ -70,10 +74,16 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
}
} else {
// fresh insert
templateContainer.innerHTML = isSVG ? `<svg>${content}</svg>` : content
templateContainer.innerHTML =
namespace === 'svg'
? `<svg>${content}</svg>`
: namespace === 'mathml'
? `<math>${content}</math>`
: content
const template = templateContainer.content
if (isSVG) {
// remove outer svg wrapper
if (namespace === 'svg' || namespace === 'mathml') {
// remove outer svg/math wrapper
const wrapper = template.firstChild!
while (wrapper.firstChild) {
template.appendChild(wrapper.firstChild)

View File

@ -20,12 +20,13 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
key,
prevValue,
nextValue,
isSVG = false,
namespace,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
const isSVG = namespace === 'svg'
if (key === 'class') {
patchClass(el, nextValue, isSVG)
} else if (key === 'style') {
@ -110,15 +111,17 @@ function shouldSetAsProp(
return false
}
// #8780 the width or heigth of embedded tags must be set as attribute
// #8780 the width or height of embedded tags must be set as attribute
if (key === 'width' || key === 'height') {
const tag = el.tagName
return !(
if (
tag === 'IMG' ||
tag === 'VIDEO' ||
tag === 'CANVAS' ||
tag === 'SOURCE'
)
) {
return false
}
}
// native onclick with string value, must be set as attribute

View File

@ -1,6 +1,6 @@
{
"name": "@vue/server-renderer",
"version": "3.4.0-alpha.4",
"version": "3.4.0-beta.3",
"description": "@vue/server-renderer",
"main": "index.js",
"module": "dist/server-renderer.esm-bundler.js",

View File

@ -10,7 +10,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.0",
"vite": "^5.0.0"
"vite": "^5.0.5"
},
"dependencies": {
"@vue/repl": "^3.0.0",

View File

@ -18,13 +18,15 @@ if (import.meta.env.DEV) {
)
}
const replRef = ref<InstanceType<typeof Repl>>()
const setVH = () => {
document.documentElement.style.setProperty('--vh', window.innerHeight + `px`)
}
window.addEventListener('resize', setVH)
setVH()
const useDevMode = ref(false)
const useDevMode = ref(true)
const useSSRMode = ref(false)
let hash = location.hash.slice(1)
@ -56,8 +58,7 @@ const sfcOptions: SFCOptions = {
script: {
inlineTemplate: !useDevMode.value,
isProd: !useDevMode.value,
propsDestructure: true,
defineModel: true
propsDestructure: true
},
style: {
isProd: !useDevMode.value
@ -91,6 +92,10 @@ function toggleSSR() {
store.setFiles(store.getFiles())
}
function reloadPage() {
replRef.value?.reload()
}
const theme = ref<'dark' | 'light'>('dark')
function toggleTheme(isDark: boolean) {
theme.value = isDark ? 'dark' : 'light'
@ -109,9 +114,11 @@ onMounted(() => {
@toggle-theme="toggleTheme"
@toggle-dev="toggleDevMode"
@toggle-ssr="toggleSSR"
@reload-page="reloadPage"
/>
<Repl
v-if="EditorComponent"
ref="replRef"
:theme="theme"
:editor="EditorComponent"
@keydown.ctrl.s.prevent

View File

@ -6,6 +6,7 @@ import Moon from './icons/Moon.vue'
import Share from './icons/Share.vue'
import Download from './icons/Download.vue'
import GitHub from './icons/GitHub.vue'
import Reload from './icons/Reload.vue'
import type { ReplStore } from '@vue/repl'
import VersionSelect from './VersionSelect.vue'
@ -14,7 +15,12 @@ const props = defineProps<{
dev: boolean
ssr: boolean
}>()
const emit = defineEmits(['toggle-theme', 'toggle-ssr', 'toggle-dev'])
const emit = defineEmits([
'toggle-theme',
'toggle-ssr',
'toggle-dev',
'reload-page'
])
const { store } = props
@ -24,7 +30,7 @@ const vueVersion = ref(`@${currentCommit}`)
async function setVueVersion(v: string) {
vueVersion.value = `loading...`
await store.setVueVersion(v)
vueVersion.value = `v${v}`
vueVersion.value = v
}
function resetVueVersion() {
@ -105,6 +111,9 @@ function toggleDark() {
<button title="Copy sharable URL" class="share" @click="copyLink">
<Share />
</button>
<button title="Reload page" class="reload" @click="$emit('reload-page')">
<Reload />
</button>
<button
title="Download project files"
class="download"

View File

@ -74,8 +74,8 @@ onMounted(() => {
<ul class="versions" :class="{ expanded }">
<li v-if="!versions"><a>loading versions...</a></li>
<li v-for="version of versions">
<a @click="setVersion(version)">v{{ version }}</a>
<li v-for="ver of versions" :class="{ active: ver === version }">
<a @click="setVersion(ver)">v{{ ver }}</a>
</li>
<div @click="expanded = false">
<slot />
@ -111,4 +111,8 @@ onMounted(() => {
border-top: 6px solid #aaa;
margin-left: 8px;
}
.versions .active a {
color: var(--green);
}
</style>

View File

@ -0,0 +1,6 @@
<template>
<svg fill="currentColor" width="1.7em" height="1.7em" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
</template>

View File

@ -10,7 +10,6 @@ export default defineConfig({
plugins: [
vue({
script: {
defineModel: true,
fs: {
fileExists: fs.existsSync,
readFile: file => fs.readFileSync(file, 'utf-8')

View File

@ -171,4 +171,49 @@ describe('toDisplayString', () => {
}"
`)
})
//#9727
test('Map with Symbol keys', () => {
const m = new Map<any, any>([
[Symbol(), 'foo'],
[Symbol(), 'bar'],
[Symbol('baz'), 'baz']
])
expect(toDisplayString(m)).toMatchInlineSnapshot(`
"{
"Map(3)": {
"Symbol(0) =>": "foo",
"Symbol(1) =>": "bar",
"Symbol(baz) =>": "baz"
}
}"
`)
// confirming the symbol renders Symbol(foo)
expect(toDisplayString(new Map([[Symbol('foo'), 'foo']]))).toContain(
String(Symbol('foo'))
)
})
test('Set with Symbol values', () => {
const s = new Set([Symbol('foo'), Symbol('bar'), Symbol()])
expect(toDisplayString(s)).toMatchInlineSnapshot(`
"{
"Set(3)": [
"Symbol(foo)",
"Symbol(bar)",
"Symbol()"
]
}"
`)
})
test('Object with Symbol values', () => {
expect(toDisplayString({ foo: Symbol('x'), bar: Symbol() }))
.toMatchInlineSnapshot(`
"{
"foo": "Symbol(x)",
"bar": "Symbol()"
}"
`)
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/shared",
"version": "3.4.0-alpha.4",
"version": "3.4.0-beta.3",
"description": "internal utils shared across @vue packages",
"main": "index.js",
"module": "dist/shared.esm-bundler.js",

View File

@ -118,6 +118,6 @@ export const isKnownSvgAttr = /*#__PURE__*/ makeMap(
`v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,` +
`vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,` +
`writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,` +
`xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,` +
`xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,` +
`xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan`
)

View File

@ -27,6 +27,14 @@ const SVG_TAGS =
'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' +
'text,textPath,title,tspan,unknown,use,view'
// https://www.w3.org/TR/mathml4/ (content elements excluded)
const MATH_TAGS =
'annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,' +
'merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,' +
'mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,' +
'mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,' +
'msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics'
const VOID_TAGS =
'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'
@ -40,6 +48,11 @@ export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS)
* Do NOT use in runtime code paths unless behind `__DEV__` flag.
*/
export const isSVGTag = /*#__PURE__*/ makeMap(SVG_TAGS)
/**
* Compiler only.
* Do NOT use in runtime code paths unless behind `__DEV__` flag.
*/
export const isMathMLTag = /*#__PURE__*/ makeMap(MATH_TAGS)
/**
* Compiler only.
* Do NOT use in runtime code paths unless behind `__DEV__` flag.

View File

@ -6,7 +6,8 @@ import {
isPlainObject,
isSet,
objectToString,
isString
isString,
isSymbol
} from './general'
/**
@ -31,17 +32,26 @@ const replacer = (_key: string, val: any): any => {
return replacer(_key, val.value)
} else if (isMap(val)) {
return {
[`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val]) => {
;(entries as any)[`${key} =>`] = val
return entries
}, {})
[`Map(${val.size})`]: [...val.entries()].reduce(
(entries, [key, val], i) => {
entries[stringifySymbol(key, i) + ' =>'] = val
return entries
},
{} as Record<string, any>
)
}
} else if (isSet(val)) {
return {
[`Set(${val.size})`]: [...val.values()]
[`Set(${val.size})`]: [...val.values()].map(v => stringifySymbol(v))
}
} else if (isSymbol(val)) {
return stringifySymbol(val)
} else if (isObject(val) && !isArray(val) && !isPlainObject(val)) {
// native elements
return String(val)
}
return val
}
const stringifySymbol = (v: unknown, i: number | string = ''): any =>
isSymbol(v) ? `Symbol(${v.description ?? i})` : v

View File

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

View File

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

View File

@ -37,6 +37,7 @@ Starting with 3.0.0-rc.3, `esm-bundler` builds now exposes global feature flags
- `__VUE_OPTIONS_API__` (enable/disable Options API support, default: `true`)
- `__VUE_PROD_DEVTOOLS__` (enable/disable devtools support in production, default: `false`)
- `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__` (enable/disable detailed warnings for hydration mismatches in production, default: `false`)
The build will work without configuring these flags, however it is **strongly recommended** to properly configure them in order to get proper tree-shaking in the final bundle. To configure these flags:

View File

@ -0,0 +1,80 @@
// MathML logic is technically dom-specific, but the logic is placed in core
// because splitting it out of core would lead to unnecessary complexity in both
// the renderer and compiler implementations.
// Related files:
// - runtime-core/src/renderer.ts
// - compiler-core/src/transforms/transformElement.ts
import { vtcKey } from '../../runtime-dom/src/components/Transition'
import { render, h, ref, nextTick } from '../src'
describe('MathML support', () => {
afterEach(() => {
document.body.innerHTML = ''
})
test('should mount elements with correct html namespace', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const App = {
template: `
<math display="block" id="e0">
<semantics id="e1">
<mrow id="e2">
<msup>
<mi>x</mi>
<mn>2</mn>
</msup>
<mo>+</mo>
<mi>y</mi>
</mrow>
<annotation-xml encoding="text/html" id="e3">
<div id="e4" />
<svg id="e5" />
</annotation-xml>
</semantics>
</math>
`
}
render(h(App), root)
const e0 = document.getElementById('e0')!
expect(e0.namespaceURI).toMatch('Math')
expect(e0.querySelector('#e1')!.namespaceURI).toMatch('Math')
expect(e0.querySelector('#e2')!.namespaceURI).toMatch('Math')
expect(e0.querySelector('#e3')!.namespaceURI).toMatch('Math')
expect(e0.querySelector('#e4')!.namespaceURI).toMatch('xhtml')
expect(e0.querySelector('#e5')!.namespaceURI).toMatch('svg')
})
test('should patch elements with correct namespaces', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const cls = ref('foo')
const App = {
setup: () => ({ cls }),
template: `
<div>
<math id="f1" :class="cls">
<annotation encoding="text/html">
<div id="f2" :class="cls"/>
</annotation>
</math>
</div>
`
}
render(h(App), root)
const f1 = document.querySelector('#f1')!
const f2 = document.querySelector('#f2')!
expect(f1.getAttribute('class')).toBe('foo')
expect(f2.className).toBe('foo')
// set a transition class on the <div> - which is only respected on non-svg
// patches
;(f2 as any)[vtcKey] = ['baz']
cls.value = 'bar'
await nextTick()
expect(f1.getAttribute('class')).toBe('bar')
expect(f2.className).toBe('bar baz')
})
})

View File

@ -9,7 +9,11 @@ import { vtcKey } from '../../runtime-dom/src/components/Transition'
import { render, h, ref, nextTick } from '../src'
describe('SVG support', () => {
test('should mount elements with correct namespaces', () => {
afterEach(() => {
document.body.innerHTML = ''
})
test('should mount elements with correct html namespace', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const App = {
@ -18,6 +22,8 @@ describe('SVG support', () => {
<svg id="e1">
<foreignObject id="e2">
<div id="e3"/>
<svg id="e4"/>
<math id="e5"/>
</foreignObject>
</svg>
</div>
@ -29,6 +35,8 @@ describe('SVG support', () => {
expect(e0.querySelector('#e1')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e2')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e3')!.namespaceURI).toMatch('xhtml')
expect(e0.querySelector('#e4')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e5')!.namespaceURI).toMatch('Math')
})
test('should patch elements with correct namespaces', async () => {

View File

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

View File

@ -1,4 +0,0 @@
// this is appended to the end of ../dist/vue.d.ts during build.
// imports the global JSX namespace registration for compat.
// TODO: remove in 3.4
import '../jsx'

File diff suppressed because it is too large Load Diff

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