chore: Merge branch 'main' into minor

This commit is contained in:
Evan You 2023-11-21 09:48:26 +08:00
commit 1ea775633d
131 changed files with 3718 additions and 681 deletions

View File

@ -17,7 +17,11 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
## Pull Request Guidelines ## Pull Request Guidelines
- Checkout a topic branch from a base branch, e.g. `main`, and merge back against that branch. - Vue core has two primary work branches: `main` and `minor`.
- If your pull request is a feature that adds new API surface, it should be submitted against the `minor` branch.
- Otherwise, it should be submitted against the `main` branch.
- [Make sure to tick the "Allow edits from maintainers" box](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This allows us to directly make minor edits / refactors and saves a lot of time. - [Make sure to tick the "Allow edits from maintainers" box](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This allows us to directly make minor edits / refactors and saves a lot of time.

1746
.github/git-branch-workflow.excalidraw vendored Normal file

File diff suppressed because it is too large Load Diff

BIN
.github/git-branch-workflow.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
.github/issue-workflow.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

122
.github/maintenance.md vendored Normal file
View File

@ -0,0 +1,122 @@
# Vue Core Maintenance Handbook
Unlike [contributing.md](./contributing.md), which targets external contributors, this document is mainly intended for team members responsible for maintaining the project. It provides guidelines on how to triage issues, review & merge PRs, and publish releases. However, it should also be valuable to external contributors even if you are not a maintainer, as it gives you a better idea of how the maintainers operate, and how you can better collaborate with them. And who knows - maybe one day you will join as a maintainer as well!
- [Issue Triage Workflow](#issue-triage-workflow)
- [Pull Request Review Guidelines](#pull-request-review-guidelines)
- [Reviewing a Fix](#reviewing-a-fix)
- [Reviewing a Refactor](#reviewing-a-refactor)
- [Reviewing a Feature](#reviewing-a-feature)
- [Common Considerations for All PRs](#common-considerations-for-all-prs)
- [PR Merge Rules for Team Members](#pr-merge-rules-for-team-members)
- [Git Branch and Release Workflow](#git-branch-and-release-workflow)
## Issue Triage Workflow
![Workflow](./issue-workflow.png)
## Pull Request Review Guidelines
The first step of reviewing a PR is to identify its purpose. We can usually put a PR in one of these categories:
- **Fix**: fixes some wrong behavior. Usually associated with an issue that has a reproduction of the behavior being fixed.
- **Refactor**: improves performance or code quality, but does not affect behavior.
- **Feature**: implements something that increases the public API surface.
Depending on the type of the PR, different considerations need to be taken into account.
### Reviewing a Fix
- Is the PR fixing a well defined issue / bug report?
- If not, ask to clarify context / provide reproduction or failing test case
- In most cases, a fix PR should include a test case that fails without the fix.
- Is it the right fix?
- If not, guide user to rework the PR.
- If the needed change is small and obvious, can directly push to the PR or add inline suggestions to reduce the back-and-forth.
- Is the cost justified?
- Sometimes the fix for a rare edge case might be introducing disproportionately large overhead (perf or code size). We should try our best to reduce the overhead to make the fix a reasonable tradeoff.
- If the reviewer is not sure about a fix, try to leave a comment explaining the concerns / reservations so the contributor at least gets some feedback.
#### Verifying a Fix
- **Always locally verify that the fix indeed fixes the original behavior, either through a reproduction or a failing test case.**
- We will run [ecosystem-ci](https://github.com/vuejs/ecosystem-ci) before every release, but if you are concerned about the potential impact of a change, it never hurts to manually run ecosystem-ci by leaving a `/ecosystem-ci run` comment (only works for team members).
- Take extra caution with snapshot tests! The CI can be "passing" even if the code generated in the snapshot contains bugs. It's best to always accompany a snapshot test with extra `expect(code).toMatch(...)` assertions.
### Reviewing a Refactor
- Performance: if a refactor PR claims to improve performance, there should be benchmarks showcasing said performance unless the improvement is self-explanatory.
- Code quality / stylistic PRs: we should be conservative on merging this type PRs because (1) they can be subjective in many cases, and (2) they often come with large git diffs, causing merge conflicts with other pending PRs, and leading to unwanted noise when tracing changes through git history. Use your best judgement on this type of PRs on whether they are worth it.
- For PRs in this category that are approved, do not merge immediately. Group them before releasing a new minor, after all feature-oriented PRs are merged.
### Reviewing a Feature
- Feature PRs should always have clear context and explanation on why the feature should be added, ideally in the form of an RFC. If the PR doesn't explain what real-world problem it is solving, ask the contributor to clarify.
- Decide if the feature should require an RFC process. The line isn't always clear, but a rough criteria is whether it is augmenting an existing API vs. adding a new API. Some examples:
- Adding a new built-in component or directive is "significant" and definitely requires an RFC.
- Template syntax additions like adding a new `v-on` modifier or a new `v-bind` syntax sugar are "substantial". It would be nice to have an RFC for it, but a detailed explanation on the use case and reasoning behind the design directly in the PR itself can be acceptable.
- Small, low-impact additions like exposing a new utility type or adding a new app config option can be self-explanatory, but should still provide enough context in the PR.
- Always ask if the use case can be solved with existing APIs. Vue already has a pretty large API surface, so we want to make sure every new addition either solves something that wasn't possible before, or significantly improves the DX of a common task.
### Common Considerations for All PRs
- Scope: a PR should only contain changes directly related to the problem being addressed. It should not contain unnecessary code style changes.
- Implementation: code style should be consistent with the rest of the codebase, follow common best practices. Prefer code that is boring but easy to understand over "clever" code.
- Size: bundle size matters. We have a GitHub action that compares the size change for every PR. We should always aim to realize the desired changes with the smallest amount of code size increase.
- Sometimes we need to compare the size increase vs. perceived benefits to decide whether a change is justifiable. Also take extra care to make sure added code can be tree-shaken if not needed.
- Make sure to put dev-only code in `__DEV__` branches so they are tree-shakable.
- Runtime code is more sensitive to size increase than compiler code.
- Make sure it doesn't accidentally cause dev-only or compiler-only code branches to be included in the runtime build. Notable case is that some functions in @vue/shared are compiler-only and should not be used in runtime code, e.g. `isHTMLTag` and `isSVGTag`.
- Performance
- Be careful about code changes in "hot paths", in particular the Virtual DOM renderer (`runtime-core/src/renderer.ts`) and component instantiation code.
- Potential Breakage
- avoiding runtime behavior breakage is the highest priority
- if not sure, use `ecosystem-ci` to verify!
- some fix inevitably cause behavior change, these must be discussed case-by-case
- type level breakage (e.g upgrading TS) is possible between minors
## PR Merge Rules for Team Members
Given that the PR meets the review requirements:
- Chore / dependencies bumps: can merge directly.
- Fixes / refactors: can merge with two or more approvals from team members.
- If you believe a PR looks good but you are not 100% confident to merge, label with "ready for merge" and Evan will provide a final review before merging.
- Features: if approved by two or more team members, label with "ready to merge". Evan will review periodically, or they can be raised and discussed at team meetings.
## Git Branch and Release Workflow
We use two primary work branches: `main` and `minor`.
- The `main` branch is for stable releases. Changes that are bug fixes or refactors that do not affect the public API surface should land in this branch. We periodically release patch releases from the `main` branch.
- The `minor` branch is the WIP branch for the next minor release. Changes that are new features or those that affect public API behavior should land in this branch. We will periodically release pre-releases (alpha / beta) for the next minor from this branch.
Before each release, we merge latest `main` into `minor` so it would include the latest bug fixes.
When the minor is ready, we do a final merge of `main` into `minor`, and then release a stable minor from this branch (e.g. `3.4.0`). After that, the `main` branch is fast-forwarded to the release commit, so the two branches are synced at each stable minor release.
![Workflow](./git-branch-workflow.png)
### Reasoning Behind the Workflow
The reason behind this workflow is to allow merging and releasing of fixes and features in parallel. In the past, we used a linear trunk-based development model. While the linear model results in a clean git history, the downside is that we need to be careful about when to merge patches vs. features.
Vue typically groups a number of features with the same scope in a minor release. We don't want to release a minor just because we happened to merge a feature PR along with a bunch of small bug fixes. So we usually "wait" until we feel we are ready to start working on a minor release before merging feature PRs.
But in reality, there are always bugs to fix and patch release to work on - this caused the intervals between minors to drag on longer than we had hoped, and many feature PRs were left waiting for a long period of time.
This is why we decided to separate bug fixes and feature PRs into separate branches. With this two-branch model, we are able to merge and release both types of changes in parallel.

View File

@ -17,7 +17,7 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
- name: Set node version to 18 - name: Set node version to 18
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 18
cache: pnpm cache: pnpm
@ -30,4 +30,4 @@ jobs:
- name: Run prettier - name: Run prettier
run: pnpm run format run: pnpm run format
- uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc - uses: autofix-ci/action@bee19d72e71787c12ca0f29de72f2833e437e4c9

View File

@ -20,7 +20,7 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
- name: Set node version to 18 - name: Set node version to 18
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 18
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

View File

@ -18,7 +18,7 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

View File

@ -23,7 +23,7 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -45,7 +45,7 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -74,7 +74,7 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -97,7 +97,7 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'vuejs/core' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') if: github.repository == 'vuejs/core' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
script: | script: |
const user = context.payload.sender.login const user = context.payload.sender.login
@ -43,7 +43,7 @@ jobs:
}) })
throw new Error('not allowed') throw new Error('not allowed')
} }
- uses: actions/github-script@v6 - uses: actions/github-script@v7
id: get-pr-data id: get-pr-data
with: with:
script: | script: |
@ -58,7 +58,7 @@ jobs:
branchName: pr.head.ref, branchName: pr.head.ref,
repo: pr.head.repo.full_name repo: pr.head.repo.full_name
} }
- uses: actions/github-script@v6 - uses: actions/github-script@v7
id: trigger id: trigger
env: env:
COMMENT: ${{ github.event.comment.body }} COMMENT: ${{ github.event.comment.body }}

View File

@ -12,7 +12,7 @@ jobs:
if: github.repository == 'vuejs/core' if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v4 - uses: dessant/lock-threads@v5
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '14' issue-inactive-days: '14'

View File

@ -25,7 +25,7 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: pnpm cache: pnpm

View File

@ -27,7 +27,7 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: pnpm cache: pnpm

View File

@ -1,3 +1,19 @@
## [3.3.8](https://github.com/vuejs/core/compare/v3.3.7...v3.3.8) (2023-11-06)
### Bug Fixes
* **compile-sfc:** support `Error` type in `defineProps` ([#5955](https://github.com/vuejs/core/issues/5955)) ([a989345](https://github.com/vuejs/core/commit/a9893458ec519aae442e1b99e64e6d74685cd22c))
* **compiler-core:** known global should be shadowed by local variables in expression rewrite ([#9492](https://github.com/vuejs/core/issues/9492)) ([a75d1c5](https://github.com/vuejs/core/commit/a75d1c5c6242e91a73cc5ba01e6da620dea0b3d9)), closes [#9482](https://github.com/vuejs/core/issues/9482)
* **compiler-sfc:** fix dynamic directive arguments usage check for slots ([#9495](https://github.com/vuejs/core/issues/9495)) ([b39fa1f](https://github.com/vuejs/core/commit/b39fa1f8157647859331ce439c42ae016a49b415)), closes [#9493](https://github.com/vuejs/core/issues/9493)
* **deps:** update dependency @vue/repl to ^2.6.2 ([#9536](https://github.com/vuejs/core/issues/9536)) ([5cef325](https://github.com/vuejs/core/commit/5cef325f41e3b38657c72fa1a38dedeee1c7a60a))
* **deps:** update dependency @vue/repl to ^2.6.3 ([#9540](https://github.com/vuejs/core/issues/9540)) ([176d590](https://github.com/vuejs/core/commit/176d59058c9aecffe9da4d4311e98496684f06d4))
* **hydration:** fix tagName access eeror on comment/text node hydration mismatch ([dd8a0cf](https://github.com/vuejs/core/commit/dd8a0cf5dcde13d2cbd899262a0e07f16e14e489)), closes [#9531](https://github.com/vuejs/core/issues/9531)
* **types:** avoid exposing lru-cache types in generated dts ([462aeb3](https://github.com/vuejs/core/commit/462aeb3b600765e219ded2ee9a0ed1e74df61de0)), closes [#9521](https://github.com/vuejs/core/issues/9521)
* **warn:** avoid warning on empty children with Suspense ([#3962](https://github.com/vuejs/core/issues/3962)) ([405f345](https://github.com/vuejs/core/commit/405f34587a63a5f1e3d147b9848219ea98acc22d))
# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28) # [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28)

View File

@ -20,7 +20,7 @@ Vue.js is an MIT-licensed open source project with its ongoing development made
<p align="center"> <p align="center">
<a target="_blank" href="https://vuejs.org/sponsor/#current-sponsors"> <a target="_blank" href="https://vuejs.org/sponsor/#current-sponsors">
<img alt="sponsors" src="https://sponsors.vuejs.org/sponsors.svg?v2"> <img alt="sponsors" src="https://sponsors.vuejs.org/sponsors.svg?v3">
</a> </a>
</p> </p>

View File

@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"version": "3.4.0-alpha.1", "version": "3.4.0-alpha.1",
"packageManager": "pnpm@8.9.2", "packageManager": "pnpm@8.10.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
@ -27,9 +27,9 @@
"dev-esm": "node scripts/dev.js -if esm-bundler-runtime", "dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
"dev-compiler": "run-p \"dev template-explorer\" serve", "dev-compiler": "run-p \"dev template-explorer\" serve",
"dev-sfc": "run-s dev-sfc-prepare dev-sfc-run", "dev-sfc": "run-s dev-sfc-prepare dev-sfc-run",
"dev-sfc-prepare": "node scripts/pre-dev-sfc.js || npm run build-compiler-cjs", "dev-sfc-prepare": "node scripts/pre-dev-sfc.js || npm run build-all-cjs",
"dev-sfc-serve": "vite packages/sfc-playground --host", "dev-sfc-serve": "vite packages/sfc-playground --host",
"dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve", "dev-sfc-run": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime\" \"dev vue -ipf esm-browser-runtime\" \"dev server-renderer -if esm-bundler\" dev-sfc-serve",
"serve": "serve", "serve": "serve",
"open": "open http://localhost:3000/packages/template-explorer/local.html", "open": "open http://localhost:3000/packages/template-explorer/local.html",
"build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self", "build-sfc-playground": "run-s build-all-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self",
@ -57,40 +57,40 @@
"node": ">=18.12.0" "node": ">=18.12.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.3",
"@babel/types": "^7.23.0", "@babel/types": "^7.23.3",
"@rollup/plugin-alias": "^5.0.1", "@rollup/plugin-alias": "^5.0.1",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.0.1", "@rollup/plugin-json": "^6.0.1",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.4", "@rollup/plugin-replace": "^5.0.4",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@types/hash-sum": "^1.0.1", "@types/hash-sum": "^1.0.2",
"@types/node": "^20.8.7", "@types/node": "^20.9.2",
"@typescript-eslint/parser": "^6.8.0", "@typescript-eslint/parser": "^6.11.0",
"@vitest/coverage-istanbul": "^0.34.6", "@vitest/coverage-istanbul": "^0.34.6",
"@vue/consolidate": "0.17.3", "@vue/consolidate": "0.17.3",
"conventional-changelog-cli": "^4.1.0", "conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.4.1", "enquirer": "^2.4.1",
"esbuild": "^0.19.5", "esbuild": "^0.19.5",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^8.52.0", "eslint": "^8.54.0",
"eslint-plugin-jest": "^27.4.3", "eslint-plugin-jest": "^27.6.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"execa": "^8.0.1", "execa": "^8.0.1",
"jsdom": "^22.1.0", "jsdom": "^22.1.0",
"lint-staged": "^15.0.2", "lint-staged": "^15.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
"markdown-table": "^3.0.3", "markdown-table": "^3.0.3",
"marked": "^9.1.2", "marked": "^9.1.6",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"prettier": "^3.0.3", "prettier": "^3.1.0",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"pug": "^3.0.2", "pug": "^3.0.2",
"puppeteer": "~21.4.0", "puppeteer": "~21.5.1",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"rollup": "^4.1.4", "rollup": "^4.1.4",
"rollup-plugin-dts": "^6.1.0", "rollup-plugin-dts": "^6.1.0",
@ -102,9 +102,9 @@
"terser": "^5.22.0", "terser": "^5.22.0",
"todomvc-app-css": "^2.4.3", "todomvc-app-css": "^2.4.3",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"tsx": "^3.14.0", "tsx": "^4.1.4",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0", "vite": "^5.0.0",
"vitest": "^0.34.6" "vitest": "^0.34.6"
} }
} }

View File

@ -2,7 +2,7 @@
exports[`compiler: expression transform > bindingMetadata > inline mode 1`] = ` exports[`compiler: expression transform > bindingMetadata > inline mode 1`] = `
"(_ctx, _cache) => { "(_ctx, _cache) => {
return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString(__props.props) + \\" \\" + _toDisplayString(_unref(setup)) + \\" \\" + _toDisplayString(setupConst) + \\" \\" + _toDisplayString(_ctx.data) + \\" \\" + _toDisplayString(_ctx.options), 1 /* TEXT */)) return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString(__props.props) + \\" \\" + _toDisplayString(_unref(setup)) + \\" \\" + _toDisplayString(setupConst) + \\" \\" + _toDisplayString(_ctx.data) + \\" \\" + _toDisplayString(_ctx.options) + \\" \\" + _toDisplayString(isNaN.value), 1 /* TEXT */))
}" }"
`; `;
@ -10,6 +10,48 @@ exports[`compiler: expression transform > bindingMetadata > non-inline mode 1`]
"const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) { return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString($props.props) + \\" \\" + _toDisplayString($setup.setup) + \\" \\" + _toDisplayString($data.data) + \\" \\" + _toDisplayString($options.options), 1 /* TEXT */)) return (_openBlock(), _createElementBlock(\\"div\\", null, _toDisplayString($props.props) + \\" \\" + _toDisplayString($setup.setup) + \\" \\" + _toDisplayString($data.data) + \\" \\" + _toDisplayString($options.options) + \\" \\" + _toDisplayString($setup.isNaN), 1 /* TEXT */))
}"
`;
exports[`compiler: expression transform > bindingMetadata > should not prefix temp variable of for loop 1`] = `
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(\\"div\\", {
onClick: () => {
for (let i = 0; i < _ctx.list.length; i++) {
_ctx.log(i)
}
}
}, null, 8 /* PROPS */, [\\"onClick\\"]))
}"
`;
exports[`compiler: expression transform > bindingMetadata > should not prefix temp variable of for...in 1`] = `
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(\\"div\\", {
onClick: () => {
for (const x in _ctx.list) {
_ctx.log(x)
}
}
}, null, 8 /* PROPS */, [\\"onClick\\"]))
}"
`;
exports[`compiler: expression transform > bindingMetadata > should not prefix temp variable of for...of 1`] = `
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(\\"div\\", {
onClick: () => {
for (const x of _ctx.list) {
_ctx.log(x)
}
}
}, null, 8 /* PROPS */, [\\"onClick\\"]))
}" }"
`; `;

View File

@ -85,7 +85,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(\\"input\\", { return (_openBlock(), _createElementBlock(\\"input\\", {
\\"foo-value\\": model, \\"foo-value\\": model,
\\"onUpdate:fooValue\\": $event => ((model) = $event) \\"onUpdate:fooValue\\": $event => ((model) = $event)
}, null, 40 /* PROPS, HYDRATE_EVENTS */, [\\"foo-value\\", \\"onUpdate:fooValue\\"])) }, null, 40 /* PROPS, NEED_HYDRATION */, [\\"foo-value\\", \\"onUpdate:fooValue\\"]))
} }
}" }"
`; `;

View File

@ -1089,7 +1089,7 @@ describe('compiler: element transform', () => {
}) })
}) })
test('HYDRATE_EVENTS', () => { test('NEED_HYDRATION for v-on', () => {
// ignore click events (has dedicated fast path) // ignore click events (has dedicated fast path)
const { node } = parseWithElementTransform(`<div @click="foo" />`, { const { node } = parseWithElementTransform(`<div @click="foo" />`, {
directiveTransforms: { directiveTransforms: {
@ -1108,12 +1108,24 @@ describe('compiler: element transform', () => {
} }
) )
expect(node2.patchFlag).toBe( expect(node2.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS]) genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
)
})
test('NEED_HYDRATION for v-bind.prop', () => {
const { node } = parseWithBind(`<div v-bind:id.prop="id" />`)
expect(node.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
)
const { node: node2 } = parseWithBind(`<div .id="id" />`)
expect(node2.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
) )
}) })
// #5870 // #5870
test('HYDRATE_EVENTS on dynamic component', () => { test('NEED_HYDRATION on dynamic component', () => {
const { node } = parseWithElementTransform( const { node } = parseWithElementTransform(
`<component :is="foo" @input="foo" />`, `<component :is="foo" @input="foo" />`,
{ {
@ -1123,7 +1135,7 @@ describe('compiler: element transform', () => {
} }
) )
expect(node.patchFlag).toBe( expect(node.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.HYDRATE_EVENTS]) genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION])
) )
}) })
}) })

View File

@ -506,7 +506,8 @@ describe('compiler: expression transform', () => {
data: BindingTypes.DATA, data: BindingTypes.DATA,
options: BindingTypes.OPTIONS, options: BindingTypes.OPTIONS,
reactive: BindingTypes.SETUP_REACTIVE_CONST, reactive: BindingTypes.SETUP_REACTIVE_CONST,
literal: BindingTypes.LITERAL_CONST literal: BindingTypes.LITERAL_CONST,
isNaN: BindingTypes.SETUP_REF
} }
function compileWithBindingMetadata( function compileWithBindingMetadata(
@ -522,19 +523,56 @@ describe('compiler: expression transform', () => {
test('non-inline mode', () => { test('non-inline mode', () => {
const { code } = compileWithBindingMetadata( const { code } = compileWithBindingMetadata(
`<div>{{ props }} {{ setup }} {{ data }} {{ options }}</div>` `<div>{{ props }} {{ setup }} {{ data }} {{ options }} {{ isNaN }}</div>`
) )
expect(code).toMatch(`$props.props`) expect(code).toMatch(`$props.props`)
expect(code).toMatch(`$setup.setup`) expect(code).toMatch(`$setup.setup`)
expect(code).toMatch(`$setup.isNaN`)
expect(code).toMatch(`$data.data`) expect(code).toMatch(`$data.data`)
expect(code).toMatch(`$options.options`) expect(code).toMatch(`$options.options`)
expect(code).toMatch(`_ctx, _cache, $props, $setup, $data, $options`) expect(code).toMatch(`_ctx, _cache, $props, $setup, $data, $options`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('should not prefix temp variable of for...in', () => {
const { code } = compileWithBindingMetadata(
`<div @click="() => {
for (const x in list) {
log(x)
}
}"/>`
)
expect(code).not.toMatch(`_ctx.x`)
expect(code).toMatchSnapshot()
})
test('should not prefix temp variable of for...of', () => {
const { code } = compileWithBindingMetadata(
`<div @click="() => {
for (const x of list) {
log(x)
}
}"/>`
)
expect(code).not.toMatch(`_ctx.x`)
expect(code).toMatchSnapshot()
})
test('should not prefix temp variable of for loop', () => {
const { code } = compileWithBindingMetadata(
`<div @click="() => {
for (let i = 0; i < list.length; i++) {
log(i)
}
}"/>`
)
expect(code).not.toMatch(`_ctx.i`)
expect(code).toMatchSnapshot()
})
test('inline mode', () => { test('inline mode', () => {
const { code } = compileWithBindingMetadata( const { code } = compileWithBindingMetadata(
`<div>{{ props }} {{ setup }} {{ setupConst }} {{ data }} {{ options }}</div>`, `<div>{{ props }} {{ setup }} {{ setupConst }} {{ data }} {{ options }} {{ isNaN }}</div>`,
{ inline: true } { inline: true }
) )
expect(code).toMatch(`__props.props`) expect(code).toMatch(`__props.props`)
@ -542,6 +580,7 @@ describe('compiler: expression transform', () => {
expect(code).toMatch(`_toDisplayString(setupConst)`) expect(code).toMatch(`_toDisplayString(setupConst)`)
expect(code).toMatch(`_ctx.data`) expect(code).toMatch(`_ctx.data`)
expect(code).toMatch(`_ctx.options`) expect(code).toMatch(`_ctx.options`)
expect(code).toMatch(`isNaN.value`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })

View File

@ -32,12 +32,12 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.3",
"@vue/shared": "3.4.0-alpha.1", "@vue/shared": "workspace:*",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.23.0" "@babel/types": "^7.23.3"
} }
} }

View File

@ -165,6 +165,19 @@ export function walkBlockDeclarations(
) { ) {
if (stmt.declare || !stmt.id) continue if (stmt.declare || !stmt.id) continue
onIdent(stmt.id) onIdent(stmt.id)
} else if (
stmt.type === 'ForOfStatement' ||
stmt.type === 'ForInStatement' ||
stmt.type === 'ForStatement'
) {
const variable = stmt.type === 'ForStatement' ? stmt.init : stmt.left
if (variable && variable.type === 'VariableDeclaration') {
for (const decl of variable.declarations) {
for (const id of extractIdentifiers(decl.id)) {
onIdent(id)
}
}
}
} }
} }
} }

View File

@ -1063,7 +1063,7 @@ function parseTextData(
) { ) {
return rawText return rawText
} else { } else {
// DATA or RCDATA containing "&"". Entity decoding required. // DATA or RCDATA containing "&". Entity decoding is required.
return context.options.decodeEntities( return context.options.decodeEntities(
rawText, rawText,
mode === TextModes.ATTRIBUTE_VALUE mode === TextModes.ATTRIBUTE_VALUE

View File

@ -550,7 +550,7 @@ export function buildProps(
) )
} else { } else {
// directives // directives
const { name, arg, exp, loc } = prop const { name, arg, exp, loc, modifiers } = prop
const isVBind = name === 'bind' const isVBind = name === 'bind'
const isVOn = name === 'on' const isVOn = name === 'on'
@ -678,6 +678,11 @@ export function buildProps(
continue continue
} }
// force hydration for v-bind with .prop modifier
if (isVBind && modifiers.includes('prop')) {
patchFlag |= PatchFlags.NEED_HYDRATION
}
const directiveTransform = context.directiveTransforms[name] const directiveTransform = context.directiveTransforms[name]
if (directiveTransform) { if (directiveTransform) {
// has built-in directive transform. // has built-in directive transform.
@ -743,12 +748,12 @@ export function buildProps(
patchFlag |= PatchFlags.PROPS patchFlag |= PatchFlags.PROPS
} }
if (hasHydrationEventBinding) { if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS patchFlag |= PatchFlags.NEED_HYDRATION
} }
} }
if ( if (
!shouldUseBlock && !shouldUseBlock &&
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) && (patchFlag === 0 || patchFlag === PatchFlags.NEED_HYDRATION) &&
(hasRef || hasVnodeHook || runtimeDirectives.length > 0) (hasRef || hasVnodeHook || runtimeDirectives.length > 0)
) { ) {
patchFlag |= PatchFlags.NEED_PATCH patchFlag |= PatchFlags.NEED_PATCH

View File

@ -227,10 +227,15 @@ export function processExpression(
const isScopeVarReference = context.identifiers[rawExp] const isScopeVarReference = context.identifiers[rawExp]
const isAllowedGlobal = isGloballyAllowed(rawExp) const isAllowedGlobal = isGloballyAllowed(rawExp)
const isLiteral = isLiteralWhitelisted(rawExp) const isLiteral = isLiteralWhitelisted(rawExp)
if (!asParams && !isScopeVarReference && !isAllowedGlobal && !isLiteral) { if (
!asParams &&
!isScopeVarReference &&
!isLiteral &&
(!isAllowedGlobal || bindingMetadata[rawExp])
) {
// const bindings exposed from setup can be skipped for patching but // const bindings exposed from setup can be skipped for patching but
// cannot be hoisted to module scope // cannot be hoisted to module scope
if (isConst(bindingMetadata[node.content])) { if (isConst(bindingMetadata[rawExp])) {
node.constType = ConstantTypes.CAN_SKIP_PATCH node.constType = ConstantTypes.CAN_SKIP_PATCH
} }
node.content = rewriteIdentifier(rawExp) node.content = rewriteIdentifier(rawExp)

View File

@ -37,7 +37,8 @@ import {
isTemplateNode, isTemplateNode,
isSlotOutlet, isSlotOutlet,
injectProp, injectProp,
findDir findDir,
forAliasRE
} from '../utils' } from '../utils'
import { import {
RENDER_LIST, RENDER_LIST,
@ -308,7 +309,6 @@ export function processFor(
} }
} }
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
// This regex doesn't cover the case if key or index aliases have destructuring, // This regex doesn't cover the case if key or index aliases have destructuring,
// but those do not make sense in the first place, so this works in practice. // but those do not make sense in the first place, so this works in practice.
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/

View File

@ -519,3 +519,5 @@ export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) {
return node return node
} }
} }
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/

View File

@ -20,10 +20,7 @@ describe('stringify static html', () => {
} }
function repeat(code: string, n: number): string { function repeat(code: string, n: number): string {
return new Array(n) return code.repeat(n)
.fill(0)
.map(() => code)
.join('')
} }
test('should bail on non-eligible static trees', () => { test('should bail on non-eligible static trees', () => {

View File

@ -137,6 +137,27 @@ describe('compiler: transform v-model', () => {
}) })
) )
}) })
test('should error on dynamic value binding alongside v-model', () => {
const onError = vi.fn()
transformWithModel(`<input v-model="test" :value="test" />`, {
onError
})
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE
})
)
})
// #3596
test('should NOT error on static value binding alongside v-model', () => {
const onError = vi.fn()
transformWithModel(`<input v-model="test" value="test" />`, {
onError
})
expect(onError).not.toHaveBeenCalled()
})
}) })
describe('modifiers', () => { describe('modifiers', () => {

View File

@ -272,7 +272,7 @@ describe('compiler-dom: transform v-on', () => {
// should not treat cached handler as dynamicProp, so it should have no // should not treat cached handler as dynamicProp, so it should have no
// dynamicProps flags and only the hydration flag // dynamicProps flags and only the hydration flag
expect((root as any).children[0].codegenNode.patchFlag).toBe( expect((root as any).children[0].codegenNode.patchFlag).toBe(
genFlagText(PatchFlags.HYDRATE_EVENTS) genFlagText(PatchFlags.NEED_HYDRATION)
) )
expect(prop).toMatchObject({ expect(prop).toMatchObject({
key: { key: {

View File

@ -37,7 +37,7 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.4.0-alpha.1", "@vue/shared": "workspace:*",
"@vue/compiler-core": "3.4.0-alpha.1" "@vue/compiler-core": "workspace:*"
} }
} }

View File

@ -4,7 +4,9 @@ import {
ElementTypes, ElementTypes,
findProp, findProp,
NodeTypes, NodeTypes,
hasDynamicKeyVBind hasDynamicKeyVBind,
findDir,
isStaticArgOf
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { createDOMCompilerError, DOMErrorCodes } from '../errors' import { createDOMCompilerError, DOMErrorCodes } from '../errors'
import { import {
@ -32,8 +34,8 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
} }
function checkDuplicatedValue() { function checkDuplicatedValue() {
const value = findProp(node, 'value') const value = findDir(node, 'bind')
if (value) { if (value && isStaticArgOf(value.arg, 'value')) {
context.onError( context.onError(
createDOMCompilerError( createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE, DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,

View File

@ -4,7 +4,7 @@
**Note: as of 3.2.13+, this package is included as a dependency of the main `vue` package and can be accessed as `vue/compiler-sfc`. This means you no longer need to explicitly install this package and ensure its version match that of `vue`'s. Just use the main `vue/compiler-sfc` deep import instead.** **Note: as of 3.2.13+, this package is included as a dependency of the main `vue` package and can be accessed as `vue/compiler-sfc`. This means you no longer need to explicitly install this package and ensure its version match that of `vue`'s. Just use the main `vue/compiler-sfc` deep import instead.**
This package contains lower level utilities that you can use if you are writing a plugin / transform for a bundler or module system that compiles Vue Single File Components (SFCs) into JavaScript. It is used in [vue-loader](https://github.com/vuejs/vue-loader), [rollup-plugin-vue](https://github.com/vuejs/rollup-plugin-vue) and [vite](https://github.com/vitejs/vite). This package contains lower level utilities that you can use if you are writing a plugin / transform for a bundler or module system that compiles Vue Single File Components (SFCs) into JavaScript. It is used in [vue-loader](https://github.com/vuejs/vue-loader) and [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue).
## API ## API
@ -77,4 +77,4 @@ export default script
Options needed for these APIs can be passed via the query string. Options needed for these APIs can be passed via the query string.
For detailed API references and options, check out the source type definitions. For actual usage of these APIs, check out [rollup-plugin-vue](https://github.com/vuejs/rollup-plugin-vue/tree/next) or [vue-loader](https://github.com/vuejs/vue-loader/tree/next). For detailed API references and options, check out the source type definitions. For actual usage of these APIs, check out [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue) or [vue-loader](https://github.com/vuejs/vue-loader/tree/next).

View File

@ -696,14 +696,14 @@ return { get vMyDir() { return vMyDir } }
exports[`SFC compile <script setup> > dev mode import usage check > dynamic arguments 1`] = ` exports[`SFC compile <script setup> > dev mode import usage check > dynamic arguments 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { FooBar, foo, bar, unused } from './x' import { FooBar, foo, bar, unused, baz } from './x'
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) { setup(__props, { expose: __expose }) {
__expose(); __expose();
return { get FooBar() { return FooBar }, get foo() { return foo }, get bar() { return bar } } return { get FooBar() { return FooBar }, get foo() { return foo }, get bar() { return bar }, get baz() { return baz } }
} }
})" })"

View File

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

View File

@ -81,6 +81,24 @@ return { emit }
})" })"
`; `;
exports[`defineEmits > w/ type (interface w/ extends) 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
interface Base { (e: 'foo'): void }
interface Emits extends Base { (e: 'bar'): void }
export default /*#__PURE__*/_defineComponent({
emits: [\\"bar\\", \\"foo\\"],
setup(__props, { expose: __expose, emit: __emit }) {
__expose();
const emit = __emit
return { emit }
}
})"
`;
exports[`defineEmits > w/ type (interface) 1`] = ` exports[`defineEmits > w/ type (interface) 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
interface Emits { (e: 'foo' | 'bar'): void } interface Emits { (e: 'foo' | 'bar'): void }

View File

@ -46,6 +46,51 @@ export default /*#__PURE__*/_defineComponent({
const { foo } = __props const { foo } = __props
return { }
}
})"
`;
exports[`defineProps > should escape names w/ special symbols 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
props: {
\\"spa ce\\": { type: null, required: true },
\\"exclamation!mark\\": { type: null, required: true },
\\"double\\\\\\"quote\\": { type: null, required: true },
\\"hash#tag\\": { type: null, required: true },
\\"dollar$sign\\": { type: null, required: true },
\\"percentage%sign\\": { type: null, required: true },
\\"amper&sand\\": { type: null, required: true },
\\"single'quote\\": { type: null, required: true },
\\"round(brack)ets\\": { type: null, required: true },
\\"aste*risk\\": { type: null, required: true },
\\"pl+us\\": { type: null, required: true },
\\"com,ma\\": { type: null, required: true },
\\"do.t\\": { type: null, required: true },
\\"sla/sh\\": { type: null, required: true },
\\"co:lon\\": { type: null, required: true },
\\"semi;colon\\": { type: null, required: true },
\\"angle<brack>ets\\": { type: null, required: true },
\\"equal=sign\\": { type: null, required: true },
\\"question?mark\\": { type: null, required: true },
\\"at@sign\\": { type: null, required: true },
\\"square[brack]ets\\": { type: null, required: true },
\\"back\\\\\\\\slash\\": { type: null, required: true },
\\"ca^ret\\": { type: null, required: true },
\\"back\`tick\\": { type: null, required: true },
\\"curly{bra}ces\\": { type: null, required: true },
\\"pi|pe\\": { type: null, required: true },
\\"til~de\\": { type: null, required: true },
\\"da-sh\\": { type: null, required: true }
},
setup(__props: any, { expose: __expose }) {
__expose();
return { } return { }
} }
@ -232,6 +277,7 @@ export default /*#__PURE__*/_defineComponent({
alias: { type: Array, required: true }, alias: { type: Array, required: true },
method: { type: Function, required: true }, method: { type: Function, required: true },
symbol: { type: Symbol, required: true }, symbol: { type: Symbol, required: true },
error: { type: Error, required: true },
extract: { type: Number, required: true }, extract: { type: Number, required: true },
exclude: { type: [Number, Boolean], required: true }, exclude: { type: [Number, Boolean], required: true },
uppercase: { type: String, required: true }, uppercase: { type: String, required: true },

View File

@ -80,6 +80,18 @@ const emit = defineEmits(['a', 'b'])
expect(content).toMatch(`emits: ["foo", "bar"]`) expect(content).toMatch(`emits: ["foo", "bar"]`)
}) })
test('w/ type (interface w/ extends)', () => {
const { content } = compile(`
<script setup lang="ts">
interface Base { (e: 'foo'): void }
interface Emits extends Base { (e: 'bar'): void }
const emit = defineEmits<Emits>()
</script>
`)
assertCode(content)
expect(content).toMatch(`emits: ["bar", "foo"]`)
})
test('w/ type (exported interface)', () => { test('w/ type (exported interface)', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">

View File

@ -97,6 +97,7 @@ const props = defineProps({ foo: String })
alias: Alias alias: Alias
method(): void method(): void
symbol: symbol symbol: symbol
error: Error
extract: Extract<1 | 2 | boolean, 2> extract: Extract<1 | 2 | boolean, 2>
exclude: Exclude<1 | 2 | boolean, 2> exclude: Exclude<1 | 2 | boolean, 2>
uppercase: Uppercase<'foo'> uppercase: Uppercase<'foo'>
@ -143,6 +144,7 @@ const props = defineProps({ foo: String })
expect(content).toMatch(`alias: { type: Array, required: true }`) expect(content).toMatch(`alias: { type: Array, required: true }`)
expect(content).toMatch(`method: { type: Function, required: true }`) expect(content).toMatch(`method: { type: Function, required: true }`)
expect(content).toMatch(`symbol: { type: Symbol, required: true }`) expect(content).toMatch(`symbol: { type: Symbol, required: true }`)
expect(content).toMatch(`error: { type: Error, required: true }`)
expect(content).toMatch( expect(content).toMatch(
`objectOrFn: { type: [Function, Object], required: true },` `objectOrFn: { type: [Function, Object], required: true },`
) )
@ -198,6 +200,7 @@ const props = defineProps({ foo: String })
alias: BindingTypes.PROPS, alias: BindingTypes.PROPS,
method: BindingTypes.PROPS, method: BindingTypes.PROPS,
symbol: BindingTypes.PROPS, symbol: BindingTypes.PROPS,
error: BindingTypes.PROPS,
objectOrFn: BindingTypes.PROPS, objectOrFn: BindingTypes.PROPS,
extract: BindingTypes.PROPS, extract: BindingTypes.PROPS,
exclude: BindingTypes.PROPS, exclude: BindingTypes.PROPS,
@ -608,4 +611,103 @@ const props = defineProps({ foo: String })
}).toThrow(`cannot accept both type and non-type arguments`) }).toThrow(`cannot accept both type and non-type arguments`)
}) })
}) })
test('should escape names w/ special symbols', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
defineProps<{
'spa ce': unknown
'exclamation!mark': unknown
'double"quote': unknown
'hash#tag': unknown
'dollar$sign': unknown
'percentage%sign': unknown
'amper&sand': unknown
"single'quote": unknown
'round(brack)ets': unknown
'aste*risk': unknown
'pl+us': unknown
'com,ma': unknown
'do.t': unknown
'sla/sh': unknown
'co:lon': unknown
'semi;colon': unknown
'angle<brack>ets': unknown
'equal=sign': unknown
'question?mark': unknown
'at@sign': unknown
'square[brack]ets': unknown
'back\\\\slash': unknown
'ca^ret': unknown
'back\`tick': unknown
'curly{bra}ces': unknown
'pi|pe': unknown
'til~de': unknown
'da-sh': unknown
}>()
</script>`)
assertCode(content)
expect(content).toMatch(`"spa ce": { type: null, required: true }`)
expect(content).toMatch(
`"exclamation!mark": { type: null, required: true }`
)
expect(content).toMatch(`"double\\"quote": { type: null, required: true }`)
expect(content).toMatch(`"hash#tag": { type: null, required: true }`)
expect(content).toMatch(`"dollar$sign": { type: null, required: true }`)
expect(content).toMatch(`"percentage%sign": { type: null, required: true }`)
expect(content).toMatch(`"amper&sand": { type: null, required: true }`)
expect(content).toMatch(`"single'quote": { type: null, required: true }`)
expect(content).toMatch(`"round(brack)ets": { type: null, required: true }`)
expect(content).toMatch(`"aste*risk": { type: null, required: true }`)
expect(content).toMatch(`"pl+us": { type: null, required: true }`)
expect(content).toMatch(`"com,ma": { type: null, required: true }`)
expect(content).toMatch(`"do.t": { type: null, required: true }`)
expect(content).toMatch(`"sla/sh": { type: null, required: true }`)
expect(content).toMatch(`"co:lon": { type: null, required: true }`)
expect(content).toMatch(`"semi;colon": { type: null, required: true }`)
expect(content).toMatch(`"angle<brack>ets": { type: null, required: true }`)
expect(content).toMatch(`"equal=sign": { type: null, required: true }`)
expect(content).toMatch(`"question?mark": { type: null, required: true }`)
expect(content).toMatch(`"at@sign": { type: null, required: true }`)
expect(content).toMatch(
`"square[brack]ets": { type: null, required: true }`
)
expect(content).toMatch(`"back\\\\slash": { type: null, required: true }`)
expect(content).toMatch(`"ca^ret": { type: null, required: true }`)
expect(content).toMatch(`"back\`tick": { type: null, required: true }`)
expect(content).toMatch(`"curly{bra}ces": { type: null, required: true }`)
expect(content).toMatch(`"pi|pe": { type: null, required: true }`)
expect(content).toMatch(`"til~de": { type: null, required: true }`)
expect(content).toMatch(`"da-sh": { type: null, required: true }`)
expect(bindings).toStrictEqual({
'spa ce': BindingTypes.PROPS,
'exclamation!mark': BindingTypes.PROPS,
'double"quote': BindingTypes.PROPS,
'hash#tag': BindingTypes.PROPS,
dollar$sign: BindingTypes.PROPS,
'percentage%sign': BindingTypes.PROPS,
'amper&sand': BindingTypes.PROPS,
"single'quote": BindingTypes.PROPS,
'round(brack)ets': BindingTypes.PROPS,
'aste*risk': BindingTypes.PROPS,
'pl+us': BindingTypes.PROPS,
'com,ma': BindingTypes.PROPS,
'do.t': BindingTypes.PROPS,
'sla/sh': BindingTypes.PROPS,
'co:lon': BindingTypes.PROPS,
'semi;colon': BindingTypes.PROPS,
'angle<brack>ets': BindingTypes.PROPS,
'equal=sign': BindingTypes.PROPS,
'question?mark': BindingTypes.PROPS,
'at@sign': BindingTypes.PROPS,
'square[brack]ets': BindingTypes.PROPS,
'back\\slash': BindingTypes.PROPS,
'ca^ret': BindingTypes.PROPS,
'back`tick': BindingTypes.PROPS,
'curly{bra}ces': BindingTypes.PROPS,
'pi|pe': BindingTypes.PROPS,
'til~de': BindingTypes.PROPS,
'da-sh': BindingTypes.PROPS
})
})
}) })

View File

@ -481,25 +481,28 @@ describe('resolveType', () => {
test.runIf(process.platform === 'win32')('relative ts on Windows', () => { test.runIf(process.platform === 'win32')('relative ts on Windows', () => {
const files = { const files = {
'C:\\Test\\foo.ts': 'export type P = { foo: number }', 'C:\\Test\\FolderA\\foo.ts': 'export type P = { foo: number }',
'C:\\Test\\bar.d.ts': 'C:\\Test\\FolderA\\bar.d.ts':
'type X = { bar: string }; export { X as Y };' + 'type X = { bar: string }; export { X as Y };' +
// verify that we can parse syntax that is only valid in d.ts // verify that we can parse syntax that is only valid in d.ts
'export const baz: boolean' 'export const baz: boolean',
'C:\\Test\\FolderB\\buz.ts': 'export type Z = { buz: string }'
} }
const { props, deps } = resolve( const { props, deps } = resolve(
` `
import { P } from './foo' import { P } from './foo'
import { Y as PP } from './bar' import { Y as PP } from './bar'
defineProps<P & PP>() import { Z as PPP } from '../FolderB/buz'
defineProps<P & PP & PPP>()
`, `,
files, files,
{}, {},
'C:\\Test\\Test.vue' 'C:\\Test\\FolderA\\Test.vue'
) )
expect(props).toStrictEqual({ expect(props).toStrictEqual({
foo: ['Number'], foo: ['Number'],
bar: ['String'] bar: ['String'],
buz: ['String']
}) })
expect(deps && [...deps].map(normalize)).toStrictEqual( expect(deps && [...deps].map(normalize)).toStrictEqual(
Object.keys(files).map(normalize) Object.keys(files).map(normalize)

View File

@ -32,27 +32,27 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.3",
"@vue/compiler-core": "3.4.0-alpha.1", "@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "3.4.0-alpha.1", "@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "3.4.0-alpha.1", "@vue/compiler-ssr": "workspace:*",
"@vue/reactivity-transform": "3.4.0-alpha.1", "@vue/reactivity-transform": "workspace:*",
"@vue/shared": "3.4.0-alpha.1", "@vue/shared": "workspace:*",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.23.0", "@babel/types": "^7.23.3",
"@vue/consolidate": "^0.17.3", "@vue/consolidate": "^0.17.3",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"lru-cache": "^10.0.1", "lru-cache": "^10.0.3",
"merge-source-map": "^1.1.0", "merge-source-map": "^1.1.0",
"minimatch": "^9.0.3", "minimatch": "^9.0.3",
"postcss-modules": "^4.3.1", "postcss-modules": "^4.3.1",
"postcss-selector-parser": "^6.0.13", "postcss-selector-parser": "^6.0.13",
"pug": "^3.0.2", "pug": "^3.0.2",
"sass": "^1.69.4" "sass": "^1.69.5"
} }
} }

View File

@ -1,13 +1,17 @@
export const version = __VERSION__ export const version = __VERSION__
// API // API
export { parse, parseCache } from './parse' export { parse } from './parse'
export { compileTemplate } from './compileTemplate' export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle' export { compileStyle, compileStyleAsync } from './compileStyle'
export { compileScript } from './compileScript' export { compileScript } from './compileScript'
export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault' export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault'
export { resolveTypeElements, inferRuntimeType } from './script/resolveType' export { resolveTypeElements, inferRuntimeType } from './script/resolveType'
import { SFCParseResult, parseCache as _parseCache } from './parse'
// #9521 export parseCache as a simple map to avoid exposing LRU types
export const parseCache = _parseCache as Map<string, SFCParseResult>
// TODO remove in 3.4 // TODO remove in 3.4
export { export {
shouldTransform as shouldTransformRef, shouldTransform as shouldTransformRef,

View File

@ -164,7 +164,7 @@ export function resolveParserPlugins(
} }
if (lang === 'ts' || lang === 'tsx') { if (lang === 'ts' || lang === 'tsx') {
plugins.push(['typescript', { dts }]) plugins.push(['typescript', { dts }])
if (!plugins.includes('decorators')) { if (!userPlugins || !userPlugins.includes('decorators')) {
plugins.push('decorators-legacy') plugins.push('decorators-legacy')
} }
} }

View File

@ -21,7 +21,7 @@ import {
isCallOf, isCallOf,
unwrapTSNode, unwrapTSNode,
toRuntimeTypeString, toRuntimeTypeString,
getEscapedKey getEscapedPropName
} from './utils' } from './utils'
import { genModelProps } from './defineModel' import { genModelProps } from './defineModel'
import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings' import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings'
@ -139,7 +139,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
const defaults: string[] = [] const defaults: string[] = []
for (const key in ctx.propsDestructuredBindings) { for (const key in ctx.propsDestructuredBindings) {
const d = genDestructuredDefaultValue(ctx, key) const d = genDestructuredDefaultValue(ctx, key)
const finalKey = getEscapedKey(key) const finalKey = getEscapedPropName(key)
if (d) if (d)
defaults.push( defaults.push(
`${finalKey}: ${d.valueString}${ `${finalKey}: ${d.valueString}${
@ -257,7 +257,7 @@ function genRuntimePropFromType(
} }
} }
const finalKey = getEscapedKey(key) const finalKey = getEscapedPropName(key)
if (!ctx.options.isProd) { if (!ctx.options.isProd) {
return `${finalKey}: { ${concatStrings([ return `${finalKey}: { ${concatStrings([
`type: ${toRuntimeTypeString(type)}`, `type: ${toRuntimeTypeString(type)}`,

View File

@ -4,6 +4,7 @@ import {
NodeTypes, NodeTypes,
SimpleExpressionNode, SimpleExpressionNode,
createRoot, createRoot,
forAliasRE,
parserOptions, parserOptions,
transform, transform,
walkIdentifiers walkIdentifiers
@ -50,12 +51,14 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
if (!isBuiltInDirective(prop.name)) { if (!isBuiltInDirective(prop.name)) {
code += `,v${capitalize(camelize(prop.name))}` code += `,v${capitalize(camelize(prop.name))}`
} }
// process dynamic directive arguments
if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) { if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
code += `,${processExp( code += `,${stripStrings(
(prop.arg as SimpleExpressionNode).content, (prop.arg as SimpleExpressionNode).content
prop.name
)}` )}`
} }
if (prop.exp) { if (prop.exp) {
code += `,${processExp( code += `,${processExp(
(prop.exp as SimpleExpressionNode).content, (prop.exp as SimpleExpressionNode).content,
@ -85,8 +88,6 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
return code return code
} }
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
function processExp(exp: string, dir?: string): string { function processExp(exp: string, dir?: string): string {
if (/ as\s+\w|<.*>|:/.test(exp)) { if (/ as\s+\w|<.*>|:/.test(exp)) {
if (dir === 'slot') { if (dir === 'slot') {

View File

@ -39,8 +39,9 @@ import { parse as babelParse } from '@babel/parser'
import { parse } from '../parse' import { parse } from '../parse'
import { createCache } from '../cache' import { createCache } from '../cache'
import type TS from 'typescript' import type TS from 'typescript'
import { extname, dirname } from 'path' import { extname, dirname, join } from 'path'
import { minimatch as isMatch } from 'minimatch' import { minimatch as isMatch } from 'minimatch'
import * as process from 'process'
export type SimpleTypeResolveOptions = Partial< export type SimpleTypeResolveOptions = Partial<
Pick< Pick<
@ -356,12 +357,15 @@ function resolveInterfaceMembers(
continue continue
} }
try { try {
const { props } = resolveTypeElements(ctx, ext, scope) const { props, calls } = resolveTypeElements(ctx, ext, scope)
for (const key in props) { for (const key in props) {
if (!hasOwn(base.props, key)) { if (!hasOwn(base.props, key)) {
base.props[key] = props[key] base.props[key] = props[key]
} }
} }
if (calls) {
;(base.calls || (base.calls = [])).push(...calls)
}
} catch (e) { } catch (e) {
ctx.error( ctx.error(
`Failed to resolve extends base type.\nIf this previously worked in 3.2, ` + `Failed to resolve extends base type.\nIf this previously worked in 3.2, ` +
@ -798,7 +802,12 @@ function importSourceToScope(
let resolved: string | undefined = scope.resolvedImportSources[source] let resolved: string | undefined = scope.resolvedImportSources[source]
if (!resolved) { if (!resolved) {
if (source.startsWith('.')) { if (source.startsWith('..')) {
const osSpecificJoinFn = process.platform === 'win32' ? join : joinPaths
const filename = osSpecificJoinFn(dirname(scope.filename), source)
resolved = resolveExt(filename, fs)
} else if (source.startsWith('.')) {
// relative import - fast path // relative import - fast path
const filename = joinPaths(dirname(scope.filename), source) const filename = joinPaths(dirname(scope.filename), source)
resolved = resolveExt(filename, fs) resolved = resolveExt(filename, fs)
@ -1413,6 +1422,7 @@ export function inferRuntimeType(
case 'WeakMap': case 'WeakMap':
case 'Date': case 'Date':
case 'Promise': case 'Promise':
case 'Error':
return [node.typeName.name] return [node.typeName.name]
// TS built-in utility types // TS built-in utility types

View File

@ -113,8 +113,14 @@ export const joinPaths = (path.posix || path).join
* key may contain symbols * key may contain symbols
* e.g. onUpdate:modelValue -> "onUpdate:modelValue" * e.g. onUpdate:modelValue -> "onUpdate:modelValue"
*/ */
export const escapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g export const propNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~\-]/
export function getEscapedKey(key: string) { export function getEscapedPropName(key: string) {
return escapeSymbolsRE.test(key) ? JSON.stringify(key) : key return propNameEscapeSymbolsRE.test(key) ? JSON.stringify(key) : key
}
export const cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g
export function getEscapedCssVarName(key: string) {
return key.replace(cssVarNameEscapeSymbolsRE, s => `\\${s}`)
} }

View File

@ -8,7 +8,7 @@ import {
BindingMetadata BindingMetadata
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { SFCDescriptor } from '../parse' import { SFCDescriptor } from '../parse'
import { escapeSymbolsRE } from '../script/utils' import { getEscapedCssVarName } from '../script/utils'
import { PluginCreator } from 'postcss' import { PluginCreator } from 'postcss'
import hash from 'hash-sum' import hash from 'hash-sum'
@ -32,7 +32,7 @@ function genVarName(id: string, raw: string, isProd: boolean): string {
return hash(id + raw) return hash(id + raw)
} else { } else {
// escape ASCII Punctuation & Symbols // escape ASCII Punctuation & Symbols
return `${id}-${raw.replace(escapeSymbolsRE, s => `\\${s}`)}` return `${id}-${getEscapedCssVarName(raw)}`
} }
} }

View File

@ -28,7 +28,7 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.4.0-alpha.1", "@vue/shared": "workspace:*",
"@vue/compiler-dom": "3.4.0-alpha.1" "@vue/compiler-dom": "workspace:*"
} }
} }

View File

@ -72,7 +72,7 @@ export function compile(
// reusing core v-bind // reusing core v-bind
bind: transformBind, bind: transformBind,
on: transformOn, on: transformOn,
// model and show has dedicated SSR handling // model and show have dedicated SSR handling
model: ssrTransformModel, model: ssrTransformModel,
show: ssrTransformShow, show: ssrTransformShow,
// the following are ignored during SSR // the following are ignored during SSR

View File

@ -1,11 +1,11 @@
{ {
"name": "@vue/dts-built-test", "name": "@vue/dts-built-test",
"private": true, "private": true,
"version": "0.0.0",
"types": "dist/dts-built-test.d.ts", "types": "dist/dts-built-test.d.ts",
"dependencies": { "dependencies": {
"@vue/shared": "workspace:*", "@vue/shared": "workspace:*",
"@vue/reactivity": "workspace:*", "@vue/reactivity": "workspace:*",
"vue": "workspace:*" "vue": "workspace:*"
}, }
"version": "3.4.0-alpha.1"
} }

View File

@ -1472,6 +1472,31 @@ describe('slots', () => {
expectType<Slots | undefined>(new comp2().$slots) expectType<Slots | undefined>(new comp2().$slots)
}) })
// #5885
describe('should work when props type is incompatible with setup returned type ', () => {
type SizeType = 'small' | 'big'
const Comp = defineComponent({
props: {
size: {
type: String as PropType<SizeType>,
required: true
}
},
setup(props) {
expectType<SizeType>(props.size)
return {
size: 1
}
}
})
type CompInstance = InstanceType<typeof Comp>
const CompA = {} as CompInstance
expectType<ComponentPublicInstance>(CompA)
expectType<number>(CompA.size)
expectType<SizeType>(CompA.$props.size)
})
import { import {
DefineComponent, DefineComponent,
ComponentOptionsMixin, ComponentOptionsMixin,

View File

@ -1,5 +1,9 @@
import { defineCustomElement } from 'vue' import {
import { expectType, describe } from './utils' defineCustomElement,
defineComponent,
type VueElementConstructor
} from 'vue'
import { expectType, describe, test } from './utils'
describe('inject', () => { describe('inject', () => {
// with object inject // with object inject
@ -62,3 +66,20 @@ describe('inject', () => {
} }
}) })
}) })
describe('defineCustomElement using defineComponent return type', () => {
test('with emits', () => {
const Comp1Vue = defineComponent({
props: {
a: String
},
emits: {
click: () => true
}
})
const Comp = defineCustomElement(Comp1Vue)
expectType<VueElementConstructor>(Comp)
expectType<string | undefined>(new Comp().a)
})
})

View File

@ -1,9 +1,9 @@
{ {
"name": "dts-test", "name": "dts-test",
"private": true, "private": true,
"version": "0.0.0",
"dependencies": { "dependencies": {
"vue": "workspace:*", "vue": "workspace:*",
"@vue/dts-built-test": "workspace:*" "@vue/dts-built-test": "workspace:*"
}, }
"version": "3.4.0-alpha.1"
} }

View File

@ -15,9 +15,10 @@ import {
MaybeRef, MaybeRef,
MaybeRefOrGetter, MaybeRefOrGetter,
ComputedRef, ComputedRef,
computed computed,
ShallowRef
} from 'vue' } from 'vue'
import { expectType, describe } from './utils' import { expectType, describe, IsUnion } from './utils'
function plainType(arg: number | Ref<number>) { function plainType(arg: number | Ref<number>) {
// ref coercing // ref coercing
@ -174,6 +175,27 @@ if (refStatus.value === 'initial') {
refStatus.value = 'invalidating' refStatus.value = 'invalidating'
} }
{
const shallow = shallowRef(1)
expectType<Ref<number>>(shallow)
expectType<ShallowRef<number>>(shallow)
}
{
//#7852
type Steps = { step: '1' } | { step: '2' }
const shallowUnionGenParam = shallowRef<Steps>({ step: '1' })
const shallowUnionAsCast = shallowRef({ step: '1' } as Steps)
expectType<IsUnion<typeof shallowUnionGenParam>>(false)
expectType<IsUnion<typeof shallowUnionAsCast>>(false)
}
describe('shallowRef with generic', <T>() => {
const r = ref({}) as MaybeRef<T>
expectType<ShallowRef<T> | Ref<T>>(shallowRef(r))
})
// proxyRefs: should return `reactive` directly // proxyRefs: should return `reactive` directly
const r1 = reactive({ const r1 = reactive({
k: 'v' k: 'v'

View File

@ -8,7 +8,8 @@ import {
defineSlots, defineSlots,
VNode, VNode,
Ref, Ref,
defineModel defineModel,
toRefs
} from 'vue' } from 'vue'
import { describe, expectType } from './utils' import { describe, expectType } from './utils'
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
@ -20,6 +21,7 @@ describe('defineProps w/ type declaration', () => {
foo: string foo: string
bool?: boolean bool?: boolean
boolAndUndefined: boolean | undefined boolAndUndefined: boolean | undefined
file?: File | File[]
}>() }>()
// explicitly declared type should be refined // explicitly declared type should be refined
expectType<string>(props.foo) expectType<string>(props.foo)
@ -108,6 +110,7 @@ describe('defineProps w/ generic type declaration + withDefaults', <T extends
defineProps<{ defineProps<{
n?: number n?: number
bool?: boolean bool?: boolean
s?: string
generic1?: T[] | { x: T } generic1?: T[] | { x: T }
generic2?: { x: T } generic2?: { x: T }
@ -126,6 +129,10 @@ describe('defineProps w/ generic type declaration + withDefaults', <T extends
) )
res.n + 1 res.n + 1
// @ts-expect-error should be readonly
res.n++
// @ts-expect-error should be readonly
res.s = ''
expectType<T[] | { x: T }>(res.generic1) expectType<T[] | { x: T }>(res.generic1)
expectType<{ x: T }>(res.generic2) expectType<{ x: T }>(res.generic2)
@ -328,3 +335,11 @@ describe('useSlots', () => {
const slots = useSlots() const slots = useSlots()
expectType<Slots>(slots) expectType<Slots>(slots)
}) })
// #6420
describe('toRefs w/ type declaration', () => {
const props = defineProps<{
file?: File | File[]
}>()
expectType<Ref<File | File[] | undefined>>(toRefs(props).file)
})

View File

@ -17,6 +17,59 @@ expectType<JSX.Element>(
<div style={[{ color: 'red' }, [{ fontSize: '1em' }]]} /> <div style={[{ color: 'red' }, [{ fontSize: '1em' }]]} />
) )
// allow undefined, string, object, array and nested array classes
expectType<JSX.Element>(<div class={undefined} />)
expectType<JSX.Element>(<div class={'foo'} />)
expectType<JSX.Element>(<div class={['foo', undefined, 'bar']} />)
expectType<JSX.Element>(<div class={[]} />)
expectType<JSX.Element>(<div class={['foo', ['bar'], [['baz']]]} />)
expectType<JSX.Element>(<div class={{ foo: true, bar: false, baz: true }} />)
expectType<JSX.Element>(<div class={{}} />)
expectType<JSX.Element>(
<div class={['foo', ['bar'], { baz: true }, [{ qux: true }]]} />
)
expectType<JSX.Element>(
<div
class={[
{ foo: false },
{ bar: 0 },
{ baz: -0 },
{ qux: '' },
{ quux: null },
{ corge: undefined },
{ grault: NaN }
]}
/>
)
expectType<JSX.Element>(
<div
class={[
{ foo: true },
{ bar: 'not-empty' },
{ baz: 1 },
{ qux: {} },
{ quux: [] }
]}
/>
)
// #7955
expectType<JSX.Element>(<div style={[undefined, '', null, false]} />)
expectType<JSX.Element>(<div style={undefined} />)
expectType<JSX.Element>(<div style={null} />)
expectType<JSX.Element>(<div style={''} />)
expectType<JSX.Element>(<div style={false} />)
// @ts-expect-error
;<div style={[0]} />
// @ts-expect-error
;<div style={0} />
// @ts-expect-error unknown prop // @ts-expect-error unknown prop
;<div foo="bar" /> ;<div foo="bar" />

View File

@ -1,4 +1,4 @@
import { ref, computed, watch, defineComponent } from 'vue' import { ref, computed, watch, defineComponent, shallowRef } from 'vue'
import { expectType } from './utils' import { expectType } from './utils'
const source = ref('foo') const source = ref('foo')
@ -92,3 +92,17 @@ defineComponent({
) )
} }
}) })
{
//#7852
type Steps = { step: '1' } | { step: '2' }
const shallowUnionGenParam = shallowRef<Steps>({ step: '1' })
const shallowUnionAsCast = shallowRef({ step: '1' } as Steps)
watch(shallowUnionGenParam, value => {
expectType<Steps>(value)
})
watch(shallowUnionAsCast, value => {
expectType<Steps>(value)
})
}

View File

@ -28,14 +28,14 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme",
"dependencies": { "dependencies": {
"@babel/parser": "^7.23.0", "@babel/parser": "^7.23.3",
"@vue/compiler-core": "3.4.0-alpha.1", "@vue/compiler-core": "workspace:*",
"@vue/shared": "3.4.0-alpha.1", "@vue/shared": "workspace:*",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.5" "magic-string": "^0.30.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.23.2", "@babel/core": "^7.23.3",
"@babel/types": "^7.23.0" "@babel/types": "^7.23.3"
} }
} }

View File

@ -275,6 +275,14 @@ describe('reactivity/readonly', () => {
expect(isReactive(value)).toBe(true) expect(isReactive(value)).toBe(true)
} }
}) })
test('should return undefined from Map.clear() call', () => {
const wrapped = readonly(new Collection())
expect(wrapped.clear()).toBeUndefined()
expect(
`Clear operation failed: target is readonly.`
).toHaveBeenWarned()
})
} }
}) })
}) })
@ -332,6 +340,14 @@ describe('reactivity/readonly', () => {
expect(isReadonly(v2)).toBe(true) expect(isReadonly(v2)).toBe(true)
} }
}) })
test('should return undefined from Set.clear() call', () => {
const wrapped = readonly(new Collection())
expect(wrapped.clear()).toBeUndefined()
expect(
`Clear operation failed: target is readonly.`
).toHaveBeenWarned()
})
} }
}) })
}) })

View File

@ -113,6 +113,12 @@ describe('reactivity/shallowReadonly', () => {
).not.toHaveBeenWarned() ).not.toHaveBeenWarned()
}) })
}) })
test('should return undefined from Map.clear() call', () => {
const sroMap = shallowReadonly(new Map())
expect(sroMap.clear()).toBeUndefined()
expect(`Clear operation failed: target is readonly.`).toHaveBeenWarned()
})
}) })
describe('collection/Set', () => { describe('collection/Set', () => {
@ -197,5 +203,11 @@ describe('reactivity/shallowReadonly', () => {
).not.toHaveBeenWarned() ).not.toHaveBeenWarned()
}) })
}) })
test('should return undefined from Set.clear() call', () => {
const sroSet = shallowReadonly(new Set())
expect(sroSet.clear()).toBeUndefined()
expect(`Clear operation failed: target is readonly.`).toHaveBeenWarned()
})
}) })
}) })

View File

@ -36,6 +36,6 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.4.0-alpha.1" "@vue/shared": "workspace:*"
} }
} }

View File

@ -228,7 +228,11 @@ function createReadonlyMethod(type: TriggerOpTypes): Function {
toRaw(this) toRaw(this)
) )
} }
return type === TriggerOpTypes.DELETE ? false : this return type === TriggerOpTypes.DELETE
? false
: type === TriggerOpTypes.CLEAR
? undefined
: this
} }
} }

View File

@ -1,3 +1,4 @@
import type { ComputedRef } from './computed'
import { import {
activeEffect, activeEffect,
shouldTrack, shouldTrack,
@ -128,9 +129,8 @@ export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }
* @param value - The "inner value" for the shallow ref. * @param value - The "inner value" for the shallow ref.
* @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowref} * @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowref}
*/ */
export function shallowRef<T extends object>( export function shallowRef<T>(value: MaybeRef<T>): Ref<T> | ShallowRef<T>
value: T export function shallowRef<T extends Ref>(value: T): T
): T extends Ref ? T : ShallowRef<T>
export function shallowRef<T>(value: T): ShallowRef<T> export function shallowRef<T>(value: T): ShallowRef<T>
export function shallowRef<T = any>(): ShallowRef<T | undefined> export function shallowRef<T = any>(): ShallowRef<T | undefined>
export function shallowRef(value?: unknown) { export function shallowRef(value?: unknown) {
@ -224,7 +224,7 @@ export type MaybeRefOrGetter<T = any> = MaybeRef<T> | (() => T)
* @param ref - Ref or plain value to be converted into the plain value. * @param ref - Ref or plain value to be converted into the plain value.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#unref} * @see {@link https://vuejs.org/api/reactivity-utilities.html#unref}
*/ */
export function unref<T>(ref: MaybeRef<T>): T { export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
return isRef(ref) ? ref.value : ref return isRef(ref) ? ref.value : ref
} }
@ -244,7 +244,7 @@ export function unref<T>(ref: MaybeRef<T>): T {
* @param source - A getter, an existing ref, or a non-function value. * @param source - A getter, an existing ref, or a non-function value.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#tovalue} * @see {@link https://vuejs.org/api/reactivity-utilities.html#tovalue}
*/ */
export function toValue<T>(source: MaybeRefOrGetter<T>): T { export function toValue<T>(source: MaybeRefOrGetter<T> | ComputedRef<T>): T {
return isFunction(source) ? source() : unref(source) return isFunction(source) ? source() : unref(source)
} }

View File

@ -91,7 +91,7 @@ describe('api: watch', () => {
array.push(1) array.push(1)
await nextTick() await nextTick()
expect(spy).toBeCalledTimes(1) expect(spy).toBeCalledTimes(1)
expect(spy).toBeCalledWith([1], expect.anything(), expect.anything()) expect(spy).toBeCalledWith([1], [1], expect.anything())
}) })
it('should not fire if watched getter result did not change', async () => { it('should not fire if watched getter result did not change', async () => {
@ -1243,4 +1243,39 @@ describe('api: watch', () => {
expect(count.value).toBe(2) expect(count.value).toBe(2)
expect(cb).toHaveBeenCalledTimes(1) expect(cb).toHaveBeenCalledTimes(1)
}) })
// #5151
test('OnCleanup also needs to be cleaned', async () => {
const spy1 = vi.fn()
const spy2 = vi.fn()
const num = ref(0)
watch(num, (value, oldValue, onCleanup) => {
if (value > 1) {
return
}
spy1()
onCleanup(() => {
// OnCleanup also needs to be cleaned
spy2()
})
})
num.value++
await nextTick()
expect(spy1).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledTimes(0)
num.value++
await nextTick()
expect(spy1).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledTimes(1)
num.value++
await nextTick()
// would not be calld when value>1
expect(spy1).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledTimes(1)
})
}) })

View File

@ -336,7 +336,8 @@ describe('component props', () => {
obj: { type: Object }, obj: { type: Object },
cls: { type: MyClass }, cls: { type: MyClass },
fn: { type: Function }, fn: { type: Function },
skipCheck: { type: [Boolean, Function], skipCheck: true } skipCheck: { type: [Boolean, Function], skipCheck: true },
empty: { type: [] }
}, },
setup() { setup() {
return () => null return () => null
@ -351,7 +352,8 @@ describe('component props', () => {
obj: 'false', obj: 'false',
cls: {}, cls: {},
fn: true, fn: true,
skipCheck: 'foo' skipCheck: 'foo',
empty: [1, 2, 3]
}), }),
nodeOps.createElement('div') nodeOps.createElement('div')
) )
@ -379,6 +381,9 @@ describe('component props', () => {
expect( expect(
`Invalid prop: type check failed for prop "skipCheck". Expected Boolean | Function, got String with value "foo".` `Invalid prop: type check failed for prop "skipCheck". Expected Boolean | Function, got String with value "foo".`
).not.toHaveBeenWarned() ).not.toHaveBeenWarned()
expect(
`Prop type [] for prop "empty" won't match anything. Did you mean to use type Array instead?`
).toHaveBeenWarned()
}) })
// #3495 // #3495

View File

@ -17,9 +17,12 @@ import {
onUnmounted, onUnmounted,
onErrorCaptured, onErrorCaptured,
shallowRef, shallowRef,
SuspenseProps,
resolveDynamicComponent,
Fragment Fragment
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { createApp, defineComponent } from 'vue' import { createApp, defineComponent } from 'vue'
import { type RawSlots } from 'packages/runtime-core/src/componentSlots'
describe('Suspense', () => { describe('Suspense', () => {
const deps: Promise<any>[] = [] const deps: Promise<any>[] = []
@ -1523,4 +1526,75 @@ describe('Suspense', () => {
expected = `<div>outerB</div><div>innerB</div>` expected = `<div>outerB</div><div>innerB</div>`
expect(serializeInner(root)).toBe(expected) expect(serializeInner(root)).toBe(expected)
}) })
describe('warnings', () => {
// base function to check if a combination of slots warns or not
function baseCheckWarn(
shouldWarn: boolean,
children: RawSlots,
props: SuspenseProps | null = null
) {
const Comp = {
setup() {
return () => h(Suspense, props, children)
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
if (shouldWarn) {
expect(`<Suspense> slots expect a single root node.`).toHaveBeenWarned()
} else {
expect(
`<Suspense> slots expect a single root node.`
).not.toHaveBeenWarned()
}
}
// actual function that we use in tests
const checkWarn = baseCheckWarn.bind(null, true)
const checkNoWarn = baseCheckWarn.bind(null, false)
test('does not warn on single child', async () => {
checkNoWarn({
default: h('div'),
fallback: h('div')
})
})
test('does not warn on null', async () => {
checkNoWarn({
default: null,
fallback: null
})
})
test('does not warn on <component :is="null" />', async () => {
checkNoWarn({
default: () => [resolveDynamicComponent(null)],
fallback: () => null
})
})
test('does not warn on empty array', async () => {
checkNoWarn({
default: [],
fallback: () => []
})
})
test('warns on multiple children in default', async () => {
checkWarn({
default: [h('div'), h('div')]
})
})
test('warns on multiple children in fallback', async () => {
checkWarn({
default: h('div'),
fallback: [h('div'), h('div')]
})
})
})
}) })

View File

@ -218,6 +218,75 @@ describe('hot module replacement', () => {
expect(deactiveSpy).toHaveBeenCalledTimes(1) expect(deactiveSpy).toHaveBeenCalledTimes(1)
}) })
// #7121
test('reload KeepAlive slot in Transition', async () => {
const root = nodeOps.createElement('div')
const childId = 'test-transition-keep-alive-reload'
const unmountSpy = vi.fn()
const mountSpy = vi.fn()
const activeSpy = vi.fn()
const deactiveSpy = vi.fn()
const Child: ComponentOptions = {
__hmrId: childId,
data() {
return { count: 0 }
},
unmounted: unmountSpy,
render: compileToFunction(`<div>{{ count }}</div>`)
}
createRecord(childId, Child)
const Parent: ComponentOptions = {
components: { Child },
data() {
return { toggle: true }
},
render: compileToFunction(
`<button @click="toggle = !toggle"></button><BaseTransition mode="out-in"><KeepAlive><Child v-if="toggle" /></KeepAlive></BaseTransition>`
)
}
render(h(Parent), root)
expect(serializeInner(root)).toBe(`<button></button><div>0</div>`)
reload(childId, {
__hmrId: childId,
data() {
return { count: 1 }
},
mounted: mountSpy,
unmounted: unmountSpy,
activated: activeSpy,
deactivated: deactiveSpy,
render: compileToFunction(`<div>{{ count }}</div>`)
})
await nextTick()
expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
expect(unmountSpy).toHaveBeenCalledTimes(1)
expect(mountSpy).toHaveBeenCalledTimes(1)
expect(activeSpy).toHaveBeenCalledTimes(1)
expect(deactiveSpy).toHaveBeenCalledTimes(0)
// should not unmount when toggling
triggerEvent(root.children[1] as TestElement, 'click')
await nextTick()
expect(serializeInner(root)).toBe(`<button></button><!---->`)
expect(unmountSpy).toHaveBeenCalledTimes(1)
expect(mountSpy).toHaveBeenCalledTimes(1)
expect(activeSpy).toHaveBeenCalledTimes(1)
expect(deactiveSpy).toHaveBeenCalledTimes(1)
// should not mount when toggling
triggerEvent(root.children[1] as TestElement, 'click')
await nextTick()
expect(serializeInner(root)).toBe(`<button></button><div>1</div>`)
expect(unmountSpy).toHaveBeenCalledTimes(1)
expect(mountSpy).toHaveBeenCalledTimes(1)
expect(activeSpy).toHaveBeenCalledTimes(2)
expect(deactiveSpy).toHaveBeenCalledTimes(1)
})
test('reload class component', async () => { test('reload class component', async () => {
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
const childId = 'test4-child' const childId = 'test4-child'

View File

@ -935,6 +935,18 @@ describe('SSR hydration', () => {
) )
}) })
test('force hydrate prop with `.prop` modifier', () => {
const { container } = mountWithHydration(
'<input type="checkbox" :indeterminate.prop="true">',
() =>
h('input', {
type: 'checkbox',
'.indeterminate': true
})
)
expect((container.firstChild! as any).indeterminate).toBe(true)
})
test('force hydrate input v-model with non-string value bindings', () => { test('force hydrate input v-model with non-string value bindings', () => {
const { container } = mountWithHydration( const { container } = mountWithHydration(
'<input type="checkbox" value="true">', '<input type="checkbox" value="true">',
@ -953,6 +965,20 @@ describe('SSR hydration', () => {
expect((container.firstChild as any)._trueValue).toBe(true) expect((container.firstChild as any)._trueValue).toBe(true)
}) })
test('force hydrate checkbox with indeterminate', () => {
const { container } = mountWithHydration(
'<input type="checkbox" indeterminate>',
() =>
createVNode(
'input',
{ type: 'checkbox', indeterminate: '' },
null,
PatchFlags.HOISTED
)
)
expect((container.firstChild as any).indeterminate).toBe(true)
})
test('force hydrate select option with non-string value bindings', () => { test('force hydrate select option with non-string value bindings', () => {
const { container } = mountWithHydration( const { container } = mountWithHydration(
'<select><option :value="true">ok</option></select>', '<select><option :value="true">ok</option></select>',
@ -1177,5 +1203,21 @@ describe('SSR hydration', () => {
expect(teleportContainer.innerHTML).toBe(`<span>value</span>`) expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
expect(`Hydration children mismatch`).toHaveBeenWarned() expect(`Hydration children mismatch`).toHaveBeenWarned()
}) })
test('comment mismatch (element)', () => {
const { container } = mountWithHydration(`<div><span></span></div>`, () =>
h('div', [createCommentVNode('hi')])
)
expect(container.innerHTML).toBe('<div><!--hi--></div>')
expect(`Hydration node mismatch`).toHaveBeenWarned()
})
test('comment mismatch (text)', () => {
const { container } = mountWithHydration(`<div>foobar</div>`, () =>
h('div', [createCommentVNode('hi')])
)
expect(container.innerHTML).toBe('<div><!--hi--></div>')
expect(`Hydration node mismatch`).toHaveBeenWarned()
})
}) })
}) })

View File

@ -354,4 +354,25 @@ describe('renderer: component', () => {
expect(serializeInner(root)).toBe(`<h1>1</h1>`) expect(serializeInner(root)).toBe(`<h1>1</h1>`)
expect(spy).toHaveBeenCalledTimes(2) expect(spy).toHaveBeenCalledTimes(2)
}) })
it('should warn accessing `this` in a <script setup> template', () => {
const App = {
setup() {
return {
__isScriptSetup: true
}
},
render(this: any) {
return this.$attrs.id
}
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(
`Property '$attrs' was accessed via 'this'. Avoid using 'this' in templates.`
).toHaveBeenWarned()
})
}) })

View File

@ -477,13 +477,13 @@ describe('vnode', () => {
expect(vnode.dynamicChildren).toStrictEqual([vnode1]) expect(vnode.dynamicChildren).toStrictEqual([vnode1])
}) })
test('should not track vnodes with only HYDRATE_EVENTS flag', () => { test('should not track vnodes with only NEED_HYDRATION flag', () => {
const hoist = createVNode('div') const hoist = createVNode('div')
const vnode = const vnode =
(openBlock(), (openBlock(),
createBlock('div', null, [ createBlock('div', null, [
hoist, hoist,
createVNode('div', null, 'text', PatchFlags.HYDRATE_EVENTS) createVNode('div', null, 'text', PatchFlags.NEED_HYDRATION)
])) ]))
expect(vnode.dynamicChildren).toStrictEqual([]) expect(vnode.dynamicChildren).toStrictEqual([])
}) })

View File

@ -32,7 +32,7 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-core#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-core#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.4.0-alpha.1", "@vue/shared": "workspace:*",
"@vue/reactivity": "3.4.0-alpha.1" "@vue/reactivity": "workspace:*"
} }
} }

View File

@ -70,8 +70,7 @@ export type DefineComponent<
true, true,
{}, {},
S S
> & >
Props
> & > &
ComponentOptionsBase< ComponentOptionsBase<
Props, Props,

View File

@ -4,7 +4,8 @@ import {
isFunction, isFunction,
Prettify, Prettify,
UnionToIntersection, UnionToIntersection,
extend extend,
LooseRequired
} from '@vue/shared' } from '@vue/shared'
import { import {
getCurrentInstance, getCurrentInstance,
@ -82,7 +83,7 @@ export function defineProps<
>(props: PP): Prettify<Readonly<ExtractPropTypes<PP>>> >(props: PP): Prettify<Readonly<ExtractPropTypes<PP>>>
// overload 3: typed-based declaration // overload 3: typed-based declaration
export function defineProps<TypeProps>(): DefineProps< export function defineProps<TypeProps>(): DefineProps<
TypeProps, LooseRequired<TypeProps>,
BooleanKey<TypeProps> BooleanKey<TypeProps>
> >
// implementation // implementation
@ -297,8 +298,8 @@ type PropsWithDefaults<
T, T,
Defaults extends InferDefaults<T>, Defaults extends InferDefaults<T>,
BKeys extends keyof T BKeys extends keyof T
> = Omit<T, keyof Defaults> & { > = Readonly<Omit<T, keyof Defaults>> & {
[K in keyof Defaults]-?: K extends keyof T readonly [K in keyof Defaults]-?: K extends keyof T
? Defaults[K] extends undefined ? Defaults[K] extends undefined
? T[K] ? T[K]
: NotUndefined<T[K]> : NotUndefined<T[K]>

View File

@ -288,10 +288,11 @@ function doWatch(
getter = () => traverse(baseGetter()) getter = () => traverse(baseGetter())
} }
let cleanup: () => void let cleanup: (() => void) | undefined
let onCleanup: OnCleanup = (fn: () => void) => { let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => { cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
cleanup = effect.onStop = undefined
} }
} }

View File

@ -725,6 +725,12 @@ function getInvalidTypeMessage(
value: unknown, value: unknown,
expectedTypes: string[] expectedTypes: string[]
): string { ): string {
if (expectedTypes.length === 0) {
return (
`Prop type [] for prop "${name}" won't match anything.` +
` Did you mean to use type Array instead?`
)
}
let message = let message =
`Invalid prop: type check failed for prop "${name}".` + `Invalid prop: type check failed for prop "${name}".` +
` Expected ${expectedTypes.map(capitalize).join(' | ')}` ` Expected ${expectedTypes.map(capitalize).join(' | ')}`

View File

@ -15,7 +15,8 @@ import {
isString, isString,
isFunction, isFunction,
UnionToIntersection, UnionToIntersection,
Prettify Prettify,
IfAny
} from '@vue/shared' } from '@vue/shared'
import { import {
toRaw, toRaw,
@ -187,7 +188,6 @@ export type CreateComponentPublicInstance<
I, I,
S S
> >
// public properties exposed on the proxy, which is used as the render context // public properties exposed on the proxy, which is used as the render context
// in templates (as `this` in the render option) // in templates (as `this` in the render option)
export type ComponentPublicInstance< export type ComponentPublicInstance<
@ -226,7 +226,7 @@ export type ComponentPublicInstance<
: (...args: any) => any, : (...args: any) => any,
options?: WatchOptions options?: WatchOptions
): WatchStopHandle ): WatchStopHandle
} & P & } & IfAny<P, P, Omit<P, keyof ShallowUnwrapRef<B>>> &
ShallowUnwrapRef<B> & ShallowUnwrapRef<B> &
UnwrapNestedRefs<D> & UnwrapNestedRefs<D> &
ExtractComputedReturns<C> & ExtractComputedReturns<C> &

View File

@ -73,9 +73,24 @@ export function renderComponentRoot(
// withProxy is a proxy with a different `has` trap only for // withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block. // runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy const proxyToUse = withProxy || proxy
// 'this' isn't available in production builds with `<script setup>`,
// so warn if it's used in dev.
const thisProxy =
__DEV__ && setupState.__isScriptSetup
? new Proxy(proxyToUse!, {
get(target, key, receiver) {
warn(
`Property '${String(
key
)}' was accessed via 'this'. Avoid using 'this' in templates.`
)
return Reflect.get(target, key, receiver)
}
})
: proxyToUse
result = normalizeVNode( result = normalizeVNode(
render!.call( render!.call(
proxyToUse, thisProxy,
proxyToUse!, proxyToUse!,
renderCache, renderCache,
props, props,

View File

@ -474,7 +474,11 @@ function emptyPlaceholder(vnode: VNode): VNode | undefined {
function getKeepAliveChild(vnode: VNode): VNode | undefined { function getKeepAliveChild(vnode: VNode): VNode | undefined {
return isKeepAlive(vnode) return isKeepAlive(vnode)
? vnode.children ? // #7121 ensure get the child component subtree in case
// it's been replaced during HMR
__DEV__ && vnode.component
? vnode.component.subTree
: vnode.children
? ((vnode.children as VNodeArrayChildren)[0] as VNode) ? ((vnode.children as VNodeArrayChildren)[0] as VNode)
: undefined : undefined
: vnode : vnode

View File

@ -29,6 +29,7 @@ import {
assertNumber assertNumber
} from '../warning' } from '../warning'
import { handleError, ErrorCodes } from '../errorHandling' import { handleError, ErrorCodes } from '../errorHandling'
import { NULL_DYNAMIC_COMPONENT } from '../helpers/resolveAssets'
export interface SuspenseProps { export interface SuspenseProps {
onResolve?: () => void onResolve?: () => void
@ -795,7 +796,11 @@ function normalizeSuspenseSlot(s: any) {
} }
if (isArray(s)) { if (isArray(s)) {
const singleChild = filterSingleRoot(s) const singleChild = filterSingleRoot(s)
if (__DEV__ && !singleChild) { if (
__DEV__ &&
!singleChild &&
s.filter(child => child !== NULL_DYNAMIC_COMPONENT).length > 0
) {
warn(`<Suspense> slots expect a single root node.`) warn(`<Suspense> slots expect a single root node.`)
} }
s = singleChild s = singleChild

View File

@ -63,6 +63,7 @@ const resolveTarget = <T = RendererElement>(
} }
export const TeleportImpl = { export const TeleportImpl = {
name: 'Teleport',
__isTeleport: true, __isTeleport: true,
process( process(
n1: TeleportVNode | null, n1: TeleportVNode | null,

View File

@ -111,6 +111,21 @@ export function createHydrationFunctions(
let domType = node.nodeType let domType = node.nodeType
vnode.el = node vnode.el = node
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
if (!('__vnode' in node)) {
Object.defineProperty(node, '__vnode', {
value: vnode,
enumerable: false
})
}
if (!('__vueParentComponent' in node)) {
Object.defineProperty(node, '__vueParentComponent', {
value: parentComponent,
enumerable: false
})
}
}
if (patchFlag === PatchFlags.BAIL) { if (patchFlag === PatchFlags.BAIL) {
optimized = false optimized = false
vnode.dynamicChildren = null vnode.dynamicChildren = null
@ -145,18 +160,17 @@ export function createHydrationFunctions(
} }
break break
case Comment: case Comment:
if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) { if (isTemplateNode(node)) {
if ((node as Element).tagName.toLowerCase() === 'template') {
const content = (vnode.el! as HTMLTemplateElement).content
.firstChild!
// replace <template> node with inner children
replaceNode(content, node, parentComponent)
vnode.el = node = content
nextNode = nextSibling(node) nextNode = nextSibling(node)
} else { // wrapped <transition appear>
// replace <template> node with inner child
replaceNode(
(vnode.el = node.content.firstChild!),
node,
parentComponent
)
} else if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
nextNode = onMismatch() nextNode = onMismatch()
}
} else { } else {
nextNode = nextSibling(node) nextNode = nextSibling(node)
} }
@ -209,7 +223,7 @@ export function createHydrationFunctions(
(domType !== DOMNodeTypes.ELEMENT || (domType !== DOMNodeTypes.ELEMENT ||
(vnode.type as string).toLowerCase() !== (vnode.type as string).toLowerCase() !==
(node as Element).tagName.toLowerCase()) && (node as Element).tagName.toLowerCase()) &&
!isTemplateNode(node as Element) !isTemplateNode(node)
) { ) {
nextNode = onMismatch() nextNode = onMismatch()
} else { } else {
@ -322,24 +336,28 @@ export function createHydrationFunctions(
const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode
// #4006 for form elements with non-string v-model value bindings // #4006 for form elements with non-string v-model value bindings
// e.g. <option :value="obj">, <input type="checkbox" :true-value="1"> // e.g. <option :value="obj">, <input type="checkbox" :true-value="1">
const forcePatchValue = (type === 'input' && dirs) || type === 'option' // #7476 <input indeterminate>
const forcePatch = type === 'input' || type === 'option'
// skip props & children if this is hoisted static nodes // skip props & children if this is hoisted static nodes
// #5405 in dev, always hydrate children for HMR // #5405 in dev, always hydrate children for HMR
if (__DEV__ || forcePatchValue || patchFlag !== PatchFlags.HOISTED) { if (__DEV__ || forcePatch || patchFlag !== PatchFlags.HOISTED) {
if (dirs) { if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created') invokeDirectiveHook(vnode, null, parentComponent, 'created')
} }
// props // props
if (props) { if (props) {
if ( if (
forcePatchValue || forcePatch ||
!optimized || !optimized ||
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.HYDRATE_EVENTS) patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
) { ) {
for (const key in props) { for (const key in props) {
if ( if (
(forcePatchValue && key.endsWith('value')) || (forcePatch &&
(isOn(key) && !isReservedProp(key)) (key.endsWith('value') || key === 'indeterminate')) ||
(isOn(key) && !isReservedProp(key)) ||
// force hydrate v-bind with .prop modifiers
key[0] === '.'
) { ) {
patchProp( patchProp(
el, el,
@ -637,17 +655,16 @@ export function createHydrationFunctions(
let parent = parentComponent let parent = parentComponent
while (parent) { while (parent) {
if (parent.vnode.el === oldNode) { if (parent.vnode.el === oldNode) {
parent.vnode.el = newNode parent.vnode.el = parent.subTree.el = newNode
parent.subTree.el = newNode
} }
parent = parent.parent parent = parent.parent
} }
} }
const isTemplateNode = (node: Element): boolean => { const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
return ( return (
node.nodeType === DOMNodeTypes.ELEMENT && node.nodeType === DOMNodeTypes.ELEMENT &&
node.tagName.toLowerCase() === 'template' (node as Element).tagName.toLowerCase() === 'template'
) )
} }

View File

@ -2401,7 +2401,7 @@ export function traverseStaticChildren(n1: VNode, n2: VNode, shallow = false) {
const c1 = ch1[i] as VNode const c1 = ch1[i] as VNode
let c2 = ch2[i] as VNode let c2 = ch2[i] as VNode
if (c2.shapeFlag & ShapeFlags.ELEMENT && !c2.dynamicChildren) { if (c2.shapeFlag & ShapeFlags.ELEMENT && !c2.dynamicChildren) {
if (c2.patchFlag <= 0 || c2.patchFlag === PatchFlags.HYDRATE_EVENTS) { if (c2.patchFlag <= 0 || c2.patchFlag === PatchFlags.NEED_HYDRATION) {
c2 = ch2[i] = cloneIfMounted(ch2[i] as VNode) c2 = ch2[i] = cloneIfMounted(ch2[i] as VNode)
c2.el = c1.el c2.el = c1.el
} }

View File

@ -488,7 +488,7 @@ function createBaseVNode(
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) && (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the // the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching. // vnode should not be considered dynamic due to handler caching.
vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS vnode.patchFlag !== PatchFlags.NEED_HYDRATION
) { ) {
currentBlock.push(vnode) currentBlock.push(vnode)
} }
@ -585,8 +585,8 @@ function _createVNode(
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) { if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
type = toRaw(type) type = toRaw(type)
warn( warn(
`Vue received a Component which was made a reactive object. This can ` + `Vue received a Component that was made a reactive object. This can ` +
`lead to unnecessary performance overhead, and should be avoided by ` + `lead to unnecessary performance overhead and should be avoided by ` +
`marking the component with \`markRaw\` or using \`shallowRef\` ` + `marking the component with \`markRaw\` or using \`shallowRef\` ` +
`instead of \`ref\`.`, `instead of \`ref\`.`,
`\nComponent that was made reactive: `, `\nComponent that was made reactive: `,

View File

@ -101,6 +101,61 @@ describe('vModel', () => {
expect(data.value).toEqual(1) expect(data.value).toEqual(1)
}) })
// #7003
it('should work with number input and be able to update rendering correctly', async () => {
const setValue1 = function (this: any, value: any) {
this.value1 = value
}
const setValue2 = function (this: any, value: any) {
this.value2 = value
}
const component = defineComponent({
data() {
return { value1: 1.002, value2: 1.002 }
},
render() {
return [
withVModel(
h('input', {
id: 'input_num1',
type: 'number',
'onUpdate:modelValue': setValue1.bind(this)
}),
this.value1
),
withVModel(
h('input', {
id: 'input_num2',
type: 'number',
'onUpdate:modelValue': setValue2.bind(this)
}),
this.value2
)
]
}
})
render(h(component), root)
const data = root._vnode.component.data
const inputNum1 = root.querySelector('#input_num1')!
expect(inputNum1.value).toBe('1.002')
const inputNum2 = root.querySelector('#input_num2')!
expect(inputNum2.value).toBe('1.002')
inputNum1.value = '1.00'
triggerEvent('input', inputNum1)
await nextTick()
expect(data.value1).toBe(1)
inputNum2.value = '1.00'
triggerEvent('input', inputNum2)
await nextTick()
expect(data.value2).toBe(1)
expect(inputNum1.value).toBe('1.00')
})
it('should work with multiple listeners', async () => { it('should work with multiple listeners', async () => {
const spy = vi.fn() const spy = vi.fn()
const component = defineComponent({ const component = defineComponent({

View File

@ -35,8 +35,8 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-dom#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-dom#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.4.0-alpha.1", "@vue/shared": "workspace:*",
"@vue/runtime-core": "3.4.0-alpha.1", "@vue/runtime-core": "workspace:*",
"csstype": "^3.1.2" "csstype": "^3.1.2"
} }
} }

View File

@ -4,7 +4,6 @@ import {
ComponentOptionsWithObjectProps, ComponentOptionsWithObjectProps,
ComponentOptionsWithoutProps, ComponentOptionsWithoutProps,
ComponentPropsOptions, ComponentPropsOptions,
ComponentPublicInstance,
ComputedOptions, ComputedOptions,
EmitsOptions, EmitsOptions,
MethodOptions, MethodOptions,
@ -21,7 +20,8 @@ import {
ConcreteComponent, ConcreteComponent,
ComponentOptions, ComponentOptions,
ComponentInjectOptions, ComponentInjectOptions,
SlotsType SlotsType,
DefineComponent
} from '@vue/runtime-core' } from '@vue/runtime-core'
import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared' import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
import { hydrate, render } from '.' import { hydrate, render } from '.'
@ -136,9 +136,9 @@ export function defineCustomElement<
// overload 5: defining a custom element from the returned value of // overload 5: defining a custom element from the returned value of
// `defineComponent` // `defineComponent`
export function defineCustomElement(options: { export function defineCustomElement<P>(
new (...args: any[]): ComponentPublicInstance options: DefineComponent<P, any, any, any>
}): VueElementConstructor ): VueElementConstructor<ExtractPropTypes<P>>
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function defineCustomElement( export function defineCustomElement(

View File

@ -83,25 +83,26 @@ export const vModelText: ModelDirective<
el[assignKey] = getModelAssigner(vnode) el[assignKey] = getModelAssigner(vnode)
// avoid clearing unresolved text. #2302 // avoid clearing unresolved text. #2302
if ((el as any).composing) return if ((el as any).composing) return
const elValue =
number || el.type === 'number' ? looseToNumber(el.value) : el.value
const newValue = value == null ? '' : value
if (elValue === newValue) {
return
}
if (document.activeElement === el && el.type !== 'range') { if (document.activeElement === el && el.type !== 'range') {
if (lazy) { if (lazy) {
return return
} }
if (trim && el.value.trim() === value) { if (trim && el.value.trim() === newValue) {
return
}
if (
(number || el.type === 'number') &&
looseToNumber(el.value) === value
) {
return return
} }
} }
const newValue = value == null ? '' : value
if (el.value !== newValue) {
el.value = newValue el.value = newValue
} }
}
} }
export const vModelCheckbox: ModelDirective<HTMLInputElement> = { export const vModelCheckbox: ModelDirective<HTMLInputElement> = {

View File

@ -188,7 +188,17 @@ interface AriaAttributes {
* Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified. * Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified.
* @see aria-atomic. * @see aria-atomic.
*/ */
'aria-relevant'?: 'additions' | 'additions text' | 'all' | 'removals' | 'text' 'aria-relevant'?:
| 'additions'
| 'additions removals'
| 'additions text'
| 'all'
| 'removals'
| 'removals additions'
| 'removals text'
| 'text'
| 'text additions'
| 'text removals'
/** Indicates that user input is required on the element before a form may be submitted. */ /** Indicates that user input is required on the element before a form may be submitted. */
'aria-required'?: Booleanish 'aria-required'?: Booleanish
/** Defines a human-readable, author-localized description for the role of an element. */ /** Defines a human-readable, author-localized description for the role of an element. */
@ -234,7 +244,13 @@ interface AriaAttributes {
} }
// Vue's style normalization supports nested arrays // Vue's style normalization supports nested arrays
export type StyleValue = string | CSSProperties | Array<StyleValue> export type StyleValue =
| false
| null
| undefined
| string
| CSSProperties
| Array<StyleValue>
export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> { export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> {
innerHTML?: string innerHTML?: string
@ -367,7 +383,7 @@ export interface ButtonHTMLAttributes extends HTMLAttributes {
formtarget?: string formtarget?: string
name?: string name?: string
type?: 'submit' | 'reset' | 'button' type?: 'submit' | 'reset' | 'button'
value?: string | string[] | number value?: string | ReadonlyArray<string> | number
} }
export interface CanvasHTMLAttributes extends HTMLAttributes { export interface CanvasHTMLAttributes extends HTMLAttributes {
@ -385,11 +401,12 @@ export interface ColgroupHTMLAttributes extends HTMLAttributes {
} }
export interface DataHTMLAttributes extends HTMLAttributes { export interface DataHTMLAttributes extends HTMLAttributes {
value?: string | string[] | number value?: string | ReadonlyArray<string> | number
} }
export interface DetailsHTMLAttributes extends HTMLAttributes { export interface DetailsHTMLAttributes extends HTMLAttributes {
open?: Booleanish open?: Booleanish
onToggle?: Event
} }
export interface DelHTMLAttributes extends HTMLAttributes { export interface DelHTMLAttributes extends HTMLAttributes {
@ -433,13 +450,17 @@ export interface IframeHTMLAttributes extends HTMLAttributes {
allow?: string allow?: string
allowfullscreen?: Booleanish allowfullscreen?: Booleanish
allowtransparency?: Booleanish allowtransparency?: Booleanish
/** @deprecated */
frameborder?: Numberish frameborder?: Numberish
height?: Numberish height?: Numberish
/** @deprecated */
marginheight?: Numberish marginheight?: Numberish
/** @deprecated */
marginwidth?: Numberish marginwidth?: Numberish
name?: string name?: string
referrerpolicy?: HTMLAttributeReferrerPolicy referrerpolicy?: HTMLAttributeReferrerPolicy
sandbox?: string sandbox?: string
/** @deprecated */
scrolling?: string scrolling?: string
seamless?: Booleanish seamless?: Booleanish
src?: string src?: string
@ -452,13 +473,13 @@ export interface ImgHTMLAttributes extends HTMLAttributes {
crossorigin?: 'anonymous' | 'use-credentials' | '' crossorigin?: 'anonymous' | 'use-credentials' | ''
decoding?: 'async' | 'auto' | 'sync' decoding?: 'async' | 'auto' | 'sync'
height?: Numberish height?: Numberish
loading?: 'eager' | 'lazy'
referrerpolicy?: HTMLAttributeReferrerPolicy referrerpolicy?: HTMLAttributeReferrerPolicy
sizes?: string sizes?: string
src?: string src?: string
srcset?: string srcset?: string
usemap?: string usemap?: string
width?: Numberish width?: Numberish
loading?: 'lazy' | 'eager'
} }
export interface InsHTMLAttributes extends HTMLAttributes { export interface InsHTMLAttributes extends HTMLAttributes {
@ -500,6 +521,14 @@ export interface InputHTMLAttributes extends HTMLAttributes {
checked?: Booleanish | any[] | Set<any> // for IDE v-model multi-checkbox support checked?: Booleanish | any[] | Set<any> // for IDE v-model multi-checkbox support
crossorigin?: string crossorigin?: string
disabled?: Booleanish disabled?: Booleanish
enterKeyHint?:
| 'enter'
| 'done'
| 'go'
| 'next'
| 'previous'
| 'search'
| 'send'
form?: string form?: string
formaction?: string formaction?: string
formenctype?: string formenctype?: string
@ -543,7 +572,7 @@ export interface LabelHTMLAttributes extends HTMLAttributes {
} }
export interface LiHTMLAttributes extends HTMLAttributes { export interface LiHTMLAttributes extends HTMLAttributes {
value?: string | string[] | number value?: string | ReadonlyArray<string> | number
} }
export interface LinkHTMLAttributes extends HTMLAttributes { export interface LinkHTMLAttributes extends HTMLAttributes {
@ -557,6 +586,7 @@ export interface LinkHTMLAttributes extends HTMLAttributes {
rel?: string rel?: string
sizes?: string sizes?: string
type?: string type?: string
charset?: string
} }
export interface MapHTMLAttributes extends HTMLAttributes { export interface MapHTMLAttributes extends HTMLAttributes {
@ -594,7 +624,7 @@ export interface MeterHTMLAttributes extends HTMLAttributes {
max?: Numberish max?: Numberish
min?: Numberish min?: Numberish
optimum?: Numberish optimum?: Numberish
value?: string | string[] | number value?: string | ReadonlyArray<string> | number
} }
export interface QuoteHTMLAttributes extends HTMLAttributes { export interface QuoteHTMLAttributes extends HTMLAttributes {
@ -639,16 +669,17 @@ export interface OutputHTMLAttributes extends HTMLAttributes {
export interface ParamHTMLAttributes extends HTMLAttributes { export interface ParamHTMLAttributes extends HTMLAttributes {
name?: string name?: string
value?: string | string[] | number value?: string | ReadonlyArray<string> | number
} }
export interface ProgressHTMLAttributes extends HTMLAttributes { export interface ProgressHTMLAttributes extends HTMLAttributes {
max?: Numberish max?: Numberish
value?: string | string[] | number value?: string | ReadonlyArray<string> | number
} }
export interface ScriptHTMLAttributes extends HTMLAttributes { export interface ScriptHTMLAttributes extends HTMLAttributes {
async?: Booleanish async?: Booleanish
/** @deprecated */
charset?: string charset?: string
crossorigin?: string crossorigin?: string
defer?: Booleanish defer?: Booleanish
@ -691,6 +722,7 @@ export interface TableHTMLAttributes extends HTMLAttributes {
cellpadding?: Numberish cellpadding?: Numberish
cellspacing?: Numberish cellspacing?: Numberish
summary?: string summary?: string
width?: Numberish
} }
export interface TextareaHTMLAttributes extends HTMLAttributes { export interface TextareaHTMLAttributes extends HTMLAttributes {
@ -707,7 +739,7 @@ export interface TextareaHTMLAttributes extends HTMLAttributes {
readonly?: Booleanish readonly?: Booleanish
required?: Booleanish required?: Booleanish
rows?: Numberish rows?: Numberish
value?: string | string[] | number value?: string | ReadonlyArray<string> | number
wrap?: string wrap?: string
} }
@ -717,6 +749,9 @@ export interface TdHTMLAttributes extends HTMLAttributes {
headers?: string headers?: string
rowspan?: Numberish rowspan?: Numberish
scope?: string scope?: string
abbr?: string
height?: Numberish
width?: Numberish
valign?: 'top' | 'middle' | 'bottom' | 'baseline' valign?: 'top' | 'middle' | 'bottom' | 'baseline'
} }
@ -726,6 +761,7 @@ export interface ThHTMLAttributes extends HTMLAttributes {
headers?: string headers?: string
rowspan?: Numberish rowspan?: Numberish
scope?: string scope?: string
abbr?: string
} }
export interface TimeHTMLAttributes extends HTMLAttributes { export interface TimeHTMLAttributes extends HTMLAttributes {
@ -746,6 +782,7 @@ export interface VideoHTMLAttributes extends MediaHTMLAttributes {
poster?: string poster?: string
width?: Numberish width?: Numberish
disablePictureInPicture?: Booleanish disablePictureInPicture?: Booleanish
disableRemotePlayback?: Booleanish
} }
export interface WebViewHTMLAttributes extends HTMLAttributes { export interface WebViewHTMLAttributes extends HTMLAttributes {
@ -794,6 +831,7 @@ export interface SVGAttributes extends AriaAttributes, EventHandlers<Events> {
// Other HTML properties supported by SVG elements in browsers // Other HTML properties supported by SVG elements in browsers
role?: string role?: string
tabindex?: Numberish tabindex?: Numberish
crossOrigin?: 'anonymous' | 'use-credentials' | ''
// SVG Specific attributes // SVG Specific attributes
'accent-height'?: Numberish 'accent-height'?: Numberish

View File

@ -1,8 +1,8 @@
{ {
"name": "@vue/runtime-test", "name": "@vue/runtime-test",
"version": "3.4.0-alpha.1",
"description": "@vue/runtime-test",
"private": true, "private": true,
"version": "0.0.0",
"description": "@vue/runtime-test",
"main": "index.js", "main": "index.js",
"module": "dist/runtime-test.esm-bundler.js", "module": "dist/runtime-test.esm-bundler.js",
"types": "dist/runtime-test.d.ts", "types": "dist/runtime-test.d.ts",
@ -25,7 +25,7 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-test#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-test#readme",
"dependencies": { "dependencies": {
"@vue/shared": "3.4.0-alpha.1", "@vue/shared": "workspace:*",
"@vue/runtime-core": "3.4.0-alpha.1" "@vue/runtime-core": "workspace:*"
} }
} }

View File

@ -32,10 +32,10 @@
}, },
"homepage": "https://github.com/vuejs/core/tree/main/packages/server-renderer#readme", "homepage": "https://github.com/vuejs/core/tree/main/packages/server-renderer#readme",
"peerDependencies": { "peerDependencies": {
"vue": "3.4.0-alpha.1" "vue": "workspace:*"
}, },
"dependencies": { "dependencies": {
"@vue/shared": "3.4.0-alpha.1", "@vue/shared": "workspace:*",
"@vue/compiler-ssr": "3.4.0-alpha.1" "@vue/compiler-ssr": "workspace:*"
} }
} }

View File

@ -1,8 +1,8 @@
{ {
"name": "@vue/sfc-playground", "name": "@vue/sfc-playground",
"version": "3.4.0-alpha.1",
"type": "module",
"private": true, "private": true,
"version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@ -10,10 +10,10 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",
"vite": "^4.5.0" "vite": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"@vue/repl": "^2.5.8", "@vue/repl": "^2.7.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"vue": "workspace:*" "vue": "workspace:*"

View File

@ -25,9 +25,13 @@ if (hash.startsWith('__SSR__')) {
const store = new ReplStore({ const store = new ReplStore({
serializedState: hash, serializedState: hash,
productionMode: !useDevMode.value,
defaultVueRuntimeURL: import.meta.env.PROD defaultVueRuntimeURL: import.meta.env.PROD
? `${location.origin}/vue.runtime.esm-browser.js` ? `${location.origin}/vue.runtime.esm-browser.js`
: `${location.origin}/src/vue-dev-proxy`, : `${location.origin}/src/vue-dev-proxy`,
defaultVueRuntimeProdURL: import.meta.env.PROD
? `${location.origin}/vue.runtime.esm-browser.prod.js`
: `${location.origin}/src/vue-dev-proxy-prod`,
defaultVueServerRendererURL: import.meta.env.PROD defaultVueServerRendererURL: import.meta.env.PROD
? `${location.origin}/server-renderer.esm-browser.js` ? `${location.origin}/server-renderer.esm-browser.js`
: `${location.origin}/src/vue-server-renderer-dev-proxy` : `${location.origin}/src/vue-server-renderer-dev-proxy`
@ -65,7 +69,7 @@ function toggleDevMode() {
sfcOptions.template!.isProd = sfcOptions.template!.isProd =
sfcOptions.style!.isProd = sfcOptions.style!.isProd =
!dev !dev
store.setFiles(store.getFiles()) store.toggleProduction()
} }
function toggleSSR() { function toggleSSR() {

View File

@ -1,6 +1,7 @@
{ {
"name": "vite-vue-starter", "name": "vite-vue-starter",
"version": "0.0.0", "version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@ -11,6 +12,6 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",
"vite": "^4.5.0" "vite": "^5.0.0"
} }
} }

View File

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

View File

@ -49,6 +49,7 @@ function copyVuePlugin(): Plugin {
} }
copyFile(`../vue/dist/vue.runtime.esm-browser.js`) copyFile(`../vue/dist/vue.runtime.esm-browser.js`)
copyFile(`../vue/dist/vue.runtime.esm-browser.prod.js`)
copyFile(`../server-renderer/dist/server-renderer.esm-browser.js`) copyFile(`../server-renderer/dist/server-renderer.esm-browser.js`)
} }
} }

View File

@ -1,6 +1,7 @@
import { escapeHtml } from '../src' import { escapeHtml, escapeHtmlComment } from '../src'
test('ssr: escapeHTML', () => { describe('escapeHtml', () => {
test('ssr: escapeHTML', () => {
expect(escapeHtml(`foo`)).toBe(`foo`) expect(escapeHtml(`foo`)).toBe(`foo`)
expect(escapeHtml(true)).toBe(`true`) expect(escapeHtml(true)).toBe(`true`)
expect(escapeHtml(false)).toBe(`false`) expect(escapeHtml(false)).toBe(`false`)
@ -8,4 +9,23 @@ test('ssr: escapeHTML', () => {
expect(escapeHtml(`"foo"`)).toBe(`&quot;foo&quot;`) expect(escapeHtml(`"foo"`)).toBe(`&quot;foo&quot;`)
expect(escapeHtml(`'bar'`)).toBe(`&#39;bar&#39;`) expect(escapeHtml(`'bar'`)).toBe(`&#39;bar&#39;`)
expect(escapeHtml(`<div>`)).toBe(`&lt;div&gt;`) expect(escapeHtml(`<div>`)).toBe(`&lt;div&gt;`)
})
test('ssr: escapeHTMLComment', () => {
const input = '<!-- Hello --><!-- World! -->'
const result = escapeHtmlComment(input)
expect(result).toEqual(' Hello World! ')
})
test('ssr: escapeHTMLComment', () => {
const input = '<!-- Comment 1 --> Hello <!--! Comment 2 --> World!'
const result = escapeHtmlComment(input)
expect(result).toEqual(' Comment 1 Hello ! Comment 2 World!')
})
test('should not affect non-comment strings', () => {
const input = 'Hello World'
const result = escapeHtmlComment(input)
expect(result).toEqual(input)
})
}) })

View File

@ -1,6 +1,10 @@
import { normalizeClass, parseStringStyle } from '../src' import { normalizeClass, parseStringStyle } from '../src'
describe('normalizeClass', () => { describe('normalizeClass', () => {
test('handles undefined correctly', () => {
expect(normalizeClass(undefined)).toEqual('')
})
test('handles string correctly', () => { test('handles string correctly', () => {
expect(normalizeClass('foo')).toEqual('foo') expect(normalizeClass('foo')).toEqual('foo')
}) })
@ -11,12 +15,56 @@ describe('normalizeClass', () => {
) )
}) })
test('handles empty array correctly', () => {
expect(normalizeClass([])).toEqual('')
})
test('handles nested array correctly', () => {
expect(normalizeClass(['foo', ['bar'], [['baz']]])).toEqual('foo bar baz')
})
test('handles object correctly', () => { test('handles object correctly', () => {
expect(normalizeClass({ foo: true, bar: false, baz: true })).toEqual( expect(normalizeClass({ foo: true, bar: false, baz: true })).toEqual(
'foo baz' 'foo baz'
) )
}) })
test('handles empty object correctly', () => {
expect(normalizeClass({})).toEqual('')
})
test('handles arrays and objects correctly', () => {
expect(
normalizeClass(['foo', ['bar'], { baz: true }, [{ qux: true }]])
).toEqual('foo bar baz qux')
})
test('handles array of objects with falsy values', () => {
expect(
normalizeClass([
{ foo: false },
{ bar: 0 },
{ baz: -0 },
{ qux: '' },
{ quux: null },
{ corge: undefined },
{ grault: NaN }
])
).toEqual('')
})
test('handles array of objects with truthy values', () => {
expect(
normalizeClass([
{ foo: true },
{ bar: 'not-empty' },
{ baz: 1 },
{ qux: {} },
{ quux: [] }
])
).toEqual('foo bar baz qux quux')
})
// #6777 // #6777
test('parse multi-line inline style', () => { test('parse multi-line inline style', () => {
expect( expect(

View File

@ -57,10 +57,11 @@ export const enum PatchFlags {
FULL_PROPS = 1 << 4, FULL_PROPS = 1 << 4,
/** /**
* Indicates an element with event listeners (which need to be attached * Indicates an element that requires props hydration
* during hydration) * (but not necessarily patching)
* e.g. event listeners & v-bind with prop modifier
*/ */
HYDRATE_EVENTS = 1 << 5, NEED_HYDRATION = 1 << 5,
/** /**
* Indicates a fragment whose children order doesn't change. * Indicates a fragment whose children order doesn't change.
@ -131,7 +132,7 @@ export const PatchFlagNames: Record<PatchFlags, string> = {
[PatchFlags.STYLE]: `STYLE`, [PatchFlags.STYLE]: `STYLE`,
[PatchFlags.PROPS]: `PROPS`, [PatchFlags.PROPS]: `PROPS`,
[PatchFlags.FULL_PROPS]: `FULL_PROPS`, [PatchFlags.FULL_PROPS]: `FULL_PROPS`,
[PatchFlags.HYDRATE_EVENTS]: `HYDRATE_EVENTS`, [PatchFlags.NEED_HYDRATION]: `NEED_HYDRATION`,
[PatchFlags.STABLE_FRAGMENT]: `STABLE_FRAGMENT`, [PatchFlags.STABLE_FRAGMENT]: `STABLE_FRAGMENT`,
[PatchFlags.KEYED_FRAGMENT]: `KEYED_FRAGMENT`, [PatchFlags.KEYED_FRAGMENT]: `KEYED_FRAGMENT`,
[PatchFlags.UNKEYED_FRAGMENT]: `UNKEYED_FRAGMENT`, [PatchFlags.UNKEYED_FRAGMENT]: `UNKEYED_FRAGMENT`,

View File

@ -1,7 +1,7 @@
{ {
"name": "@vue/template-explorer", "name": "@vue/template-explorer",
"version": "3.4.0-alpha.1",
"private": true, "private": true,
"version": "0.0.0",
"buildOptions": { "buildOptions": {
"formats": [ "formats": [
"global" "global"

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