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
- 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.

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
- name: Set node version to 18
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm
@ -30,4 +30,4 @@ jobs:
- name: Run prettier
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
- name: Set node version to 18
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ jobs:
uses: pnpm/action-setup@v2
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
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)

View File

@ -20,7 +20,7 @@ Vue.js is an MIT-licensed open source project with its ongoing development made
<p align="center">
<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>
</p>

View File

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

View File

@ -2,7 +2,7 @@
exports[`compiler: expression transform > bindingMetadata > inline mode 1`] = `
"(_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
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\\", {
\\"foo-value\\": model,
\\"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)
const { node } = parseWithElementTransform(`<div @click="foo" />`, {
directiveTransforms: {
@ -1108,12 +1108,24 @@ describe('compiler: element transform', () => {
}
)
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
test('HYDRATE_EVENTS on dynamic component', () => {
test('NEED_HYDRATION on dynamic component', () => {
const { node } = parseWithElementTransform(
`<component :is="foo" @input="foo" />`,
{
@ -1123,7 +1135,7 @@ describe('compiler: element transform', () => {
}
)
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,
options: BindingTypes.OPTIONS,
reactive: BindingTypes.SETUP_REACTIVE_CONST,
literal: BindingTypes.LITERAL_CONST
literal: BindingTypes.LITERAL_CONST,
isNaN: BindingTypes.SETUP_REF
}
function compileWithBindingMetadata(
@ -522,19 +523,56 @@ describe('compiler: expression transform', () => {
test('non-inline mode', () => {
const { code } = compileWithBindingMetadata(
`<div>{{ props }} {{ setup }} {{ data }} {{ options }}</div>`
`<div>{{ props }} {{ setup }} {{ data }} {{ options }} {{ isNaN }}</div>`
)
expect(code).toMatch(`$props.props`)
expect(code).toMatch(`$setup.setup`)
expect(code).toMatch(`$setup.isNaN`)
expect(code).toMatch(`$data.data`)
expect(code).toMatch(`$options.options`)
expect(code).toMatch(`_ctx, _cache, $props, $setup, $data, $options`)
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', () => {
const { code } = compileWithBindingMetadata(
`<div>{{ props }} {{ setup }} {{ setupConst }} {{ data }} {{ options }}</div>`,
`<div>{{ props }} {{ setup }} {{ setupConst }} {{ data }} {{ options }} {{ isNaN }}</div>`,
{ inline: true }
)
expect(code).toMatch(`__props.props`)
@ -542,6 +580,7 @@ describe('compiler: expression transform', () => {
expect(code).toMatch(`_toDisplayString(setupConst)`)
expect(code).toMatch(`_ctx.data`)
expect(code).toMatch(`_ctx.options`)
expect(code).toMatch(`isNaN.value`)
expect(code).toMatchSnapshot()
})

View File

@ -674,8 +674,8 @@ describe('compiler: v-for', () => {
patchFlag: !disableTracking
? genFlagText(PatchFlags.STABLE_FRAGMENT)
: keyed
? genFlagText(PatchFlags.KEYED_FRAGMENT)
: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
? genFlagText(PatchFlags.KEYED_FRAGMENT)
: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,

View File

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

@ -448,8 +448,8 @@ function genAssets(
__COMPAT__ && type === 'filter'
? RESOLVE_FILTER
: type === 'component'
? RESOLVE_COMPONENT
: RESOLVE_DIRECTIVE
? RESOLVE_COMPONENT
: RESOLVE_DIRECTIVE
)
for (let i = 0; i < assets.length; i++) {
let id = assets[i]

View File

@ -41,8 +41,8 @@ export function getBaseTransformPreset(
transformExpression
]
: __BROWSER__ && __DEV__
? [transformExpression]
: []),
? [transformExpression]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,

View File

@ -811,8 +811,8 @@ function parseAttribute(
(isPropShorthand || startsWith(name, ':')
? 'bind'
: startsWith(name, '@')
? 'on'
: 'slot')
? 'on'
: 'slot')
let arg: ExpressionNode | undefined
if (match[2]) {
@ -1063,7 +1063,7 @@ function parseTextData(
) {
return rawText
} else {
// DATA or RCDATA containing "&"". Entity decoding required.
// DATA or RCDATA containing "&". Entity decoding is required.
return context.options.decodeEntities(
rawText,
mode === TextModes.ATTRIBUTE_VALUE

View File

@ -238,8 +238,8 @@ export function createTransformContext(
const removalIndex = node
? list.indexOf(node)
: context.currentNode
? context.childIndex
: -1
? context.childIndex
: -1
/* istanbul ignore if */
if (__DEV__ && removalIndex < 0) {
throw new Error(`node being removed is not a child of current parent`)

View File

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

View File

@ -227,10 +227,15 @@ export function processExpression(
const isScopeVarReference = context.identifiers[rawExp]
const isAllowedGlobal = isGloballyAllowed(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
// cannot be hoisted to module scope
if (isConst(bindingMetadata[node.content])) {
if (isConst(bindingMetadata[rawExp])) {
node.constType = ConstantTypes.CAN_SKIP_PATCH
}
node.content = rewriteIdentifier(rawExp)

View File

@ -37,7 +37,8 @@ import {
isTemplateNode,
isSlotOutlet,
injectProp,
findDir
findDir,
forAliasRE
} from '../utils'
import {
RENDER_LIST,
@ -94,8 +95,8 @@ export const transformFor = createStructuralDirectiveTransform(
const fragmentFlag = isStableFragment
? PatchFlags.STABLE_FRAGMENT
: keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
forNode.codegenNode = createVNodeCall(
context,
@ -140,10 +141,10 @@ export const transformFor = createStructuralDirectiveTransform(
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
: null
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
: null
if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template>
@ -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,
// but those do not make sense in the first place, so this works in practice.
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/

View File

@ -349,8 +349,8 @@ export function buildSlots(
const slotFlag = hasDynamicSlots
? SlotFlags.DYNAMIC
: hasForwardedSlots(node.children)
? SlotFlags.FORWARDED
: SlotFlags.STABLE
? SlotFlags.FORWARDED
: SlotFlags.STABLE
let slots = createObjectExpression(
slotsProperties.concat(

View File

@ -519,3 +519,5 @@ export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) {
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 {
return new Array(n)
.fill(0)
.map(() => code)
.join('')
return code.repeat(n)
}
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', () => {

View File

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

View File

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

View File

@ -153,8 +153,8 @@ const isStringifiableAttr = (name: string, ns: DOMNamespaces) => {
(ns === DOMNamespaces.HTML
? isKnownHtmlAttr(name)
: ns === DOMNamespaces.SVG
? isKnownSvgAttr(name)
: false) || dataAriaRE.test(name)
? isKnownSvgAttr(name)
: false) || dataAriaRE.test(name)
)
}

View File

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

View File

@ -96,14 +96,14 @@ const transformClick = (key: ExpressionNode, event: string) => {
return isStaticClick
? createSimpleExpression(event, true)
: key.type !== NodeTypes.SIMPLE_EXPRESSION
? createCompoundExpression([
`(`,
key,
`) === "onClick" ? "${event}" : (`,
key,
`)`
])
: key
? createCompoundExpression([
`(`,
key,
`) === "onClick" ? "${event}" : (`,
key,
`)`
])
: key
}
export const transformOn: DirectiveTransform = (dir, node, context) => {

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.**
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
@ -77,4 +77,4 @@ export default script
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`] = `
"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({
setup(__props, { 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', () => {
const { content } = compile(`
<script setup lang="ts">
import { FooBar, foo, bar, unused } from './x'
import { FooBar, foo, bar, unused, baz } from './x'
</script>
<template>
<FooBar #[foo.slotName] />
<FooBar #unused />
<div :[bar.attrName]="15"></div>
<div unused="unused"></div>
<div #[\`item:\${baz.key}\`]="{ value }"></div>
</template>
`)
expect(content).toMatch(
`return { get FooBar() { return FooBar }, get foo() { return foo }, ` +
`get bar() { return bar } }`
`get bar() { return bar }, get baz() { return baz } }`
)
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`] = `
"import { defineComponent as _defineComponent } from 'vue'
interface Emits { (e: 'foo' | 'bar'): void }

View File

@ -46,6 +46,51 @@ export default /*#__PURE__*/_defineComponent({
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 { }
}
@ -232,6 +277,7 @@ export default /*#__PURE__*/_defineComponent({
alias: { type: Array, required: true },
method: { type: Function, required: true },
symbol: { type: Symbol, required: true },
error: { type: Error, required: true },
extract: { type: Number, required: true },
exclude: { type: [Number, Boolean], required: true },
uppercase: { type: String, required: true },

View File

@ -80,6 +80,18 @@ const emit = defineEmits(['a', 'b'])
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)', () => {
const { content } = compile(`
<script setup lang="ts">

View File

@ -97,6 +97,7 @@ const props = defineProps({ foo: String })
alias: Alias
method(): void
symbol: symbol
error: Error
extract: Extract<1 | 2 | boolean, 2>
exclude: Exclude<1 | 2 | boolean, 2>
uppercase: Uppercase<'foo'>
@ -143,6 +144,7 @@ const props = defineProps({ foo: String })
expect(content).toMatch(`alias: { type: Array, required: true }`)
expect(content).toMatch(`method: { type: Function, required: true }`)
expect(content).toMatch(`symbol: { type: Symbol, required: true }`)
expect(content).toMatch(`error: { type: Error, required: true }`)
expect(content).toMatch(
`objectOrFn: { type: [Function, Object], required: true },`
)
@ -198,6 +200,7 @@ const props = defineProps({ foo: String })
alias: BindingTypes.PROPS,
method: BindingTypes.PROPS,
symbol: BindingTypes.PROPS,
error: BindingTypes.PROPS,
objectOrFn: BindingTypes.PROPS,
extract: BindingTypes.PROPS,
exclude: BindingTypes.PROPS,
@ -608,4 +611,103 @@ const props = defineProps({ foo: String })
}).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', () => {
const files = {
'C:\\Test\\foo.ts': 'export type P = { foo: number }',
'C:\\Test\\bar.d.ts':
'C:\\Test\\FolderA\\foo.ts': 'export type P = { foo: number }',
'C:\\Test\\FolderA\\bar.d.ts':
'type X = { bar: string }; export { X as Y };' +
// 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(
`
import { P } from './foo'
import { Y as PP } from './bar'
defineProps<P & PP>()
import { Z as PPP } from '../FolderB/buz'
defineProps<P & PP & PPP>()
`,
files,
{},
'C:\\Test\\Test.vue'
'C:\\Test\\FolderA\\Test.vue'
)
expect(props).toStrictEqual({
foo: ['Number'],
bar: ['String']
bar: ['String'],
buz: ['String']
})
expect(deps && [...deps].map(normalize)).toStrictEqual(
Object.keys(files).map(normalize)

View File

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

View File

@ -1172,8 +1172,8 @@ function walkObjectPattern(
const type = isDefineCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
registerBinding(bindings, p.key, type)
} else {
walkPattern(p.value, bindings, isConst, isDefineCall)
@ -1208,8 +1208,8 @@ function walkPattern(
const type = isDefineCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
registerBinding(bindings, node, type)
} else if (node.type === 'RestElement') {
// argument can only be identifier when destructuring
@ -1224,8 +1224,8 @@ function walkPattern(
const type = isDefineCall
? BindingTypes.SETUP_CONST
: isConst
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
registerBinding(bindings, node.left, type)
} else {
walkPattern(node.left, bindings, isConst)

View File

@ -124,8 +124,8 @@ export function compileTemplate(
? preprocessCustomRequire
? preprocessCustomRequire(preprocessLang)
: __ESM_BROWSER__
? undefined
: consolidate[preprocessLang as keyof typeof consolidate]
? undefined
: consolidate[preprocessLang as keyof typeof consolidate]
: false
if (preprocessor) {
try {

View File

@ -1,13 +1,17 @@
export const version = __VERSION__
// API
export { parse, parseCache } from './parse'
export { parse } from './parse'
export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle'
export { compileScript } from './compileScript'
export { rewriteDefault, rewriteDefaultAST } from './rewriteDefault'
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
export {
shouldTransform as shouldTransformRef,

View File

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

View File

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

View File

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

View File

@ -39,8 +39,9 @@ import { parse as babelParse } from '@babel/parser'
import { parse } from '../parse'
import { createCache } from '../cache'
import type TS from 'typescript'
import { extname, dirname } from 'path'
import { extname, dirname, join } from 'path'
import { minimatch as isMatch } from 'minimatch'
import * as process from 'process'
export type SimpleTypeResolveOptions = Partial<
Pick<
@ -356,12 +357,15 @@ function resolveInterfaceMembers(
continue
}
try {
const { props } = resolveTypeElements(ctx, ext, scope)
const { props, calls } = resolveTypeElements(ctx, ext, scope)
for (const key in props) {
if (!hasOwn(base.props, key)) {
base.props[key] = props[key]
}
}
if (calls) {
;(base.calls || (base.calls = [])).push(...calls)
}
} catch (e) {
ctx.error(
`Failed to resolve extends base type.\nIf this previously worked in 3.2, ` +
@ -653,8 +657,8 @@ function innerResolveTypeReference(
? scope.exportedDeclares
: scope.declares
: onlyExported
? scope.exportedTypes
: scope.types
? scope.exportedTypes
: scope.types
if (lookupSource[name]) {
return lookupSource[name]
} else {
@ -697,10 +701,10 @@ function getReferenceName(node: ReferenceTypes): string | string[] {
node.type === 'TSTypeReference'
? node.typeName
: node.type === 'TSExpressionWithTypeArguments'
? node.expression
: node.type === 'TSImportType'
? node.qualifier
: node.exprName
? node.expression
: node.type === 'TSImportType'
? node.qualifier
: node.exprName
if (ref?.type === 'Identifier') {
return ref.name
} else if (ref?.type === 'TSQualifiedName') {
@ -798,7 +802,12 @@ function importSourceToScope(
let resolved: string | undefined = scope.resolvedImportSources[source]
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
const filename = joinPaths(dirname(scope.filename), source)
resolved = resolveExt(filename, fs)
@ -1069,8 +1078,8 @@ function ctxToScope(ctx: TypeResolveContext): TypeScope {
'ast' in ctx
? ctx.ast
: ctx.scriptAst
? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body]
: ctx.scriptSetupAst!.body
? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body]
: ctx.scriptSetupAst!.body
const scope = new TypeScope(
ctx.filename,
@ -1413,6 +1422,7 @@ export function inferRuntimeType(
case 'WeakMap':
case 'Date':
case 'Promise':
case 'Error':
return [node.typeName.name]
// TS built-in utility types

View File

@ -76,8 +76,8 @@ export function getId(node: Expression) {
return node.type === 'Identifier'
? node.name
: node.type === 'StringLiteral'
? node.value
: null
? node.value
: null
}
const identity = (str: string) => str
@ -113,8 +113,14 @@ export const joinPaths = (path.posix || path).join
* key may contain symbols
* e.g. onUpdate:modelValue -> "onUpdate:modelValue"
*/
export const escapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g
export const propNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~\-]/
export function getEscapedKey(key: string) {
return escapeSymbolsRE.test(key) ? JSON.stringify(key) : key
export function getEscapedPropName(key: string) {
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
} from '@vue/compiler-dom'
import { SFCDescriptor } from '../parse'
import { escapeSymbolsRE } from '../script/utils'
import { getEscapedCssVarName } from '../script/utils'
import { PluginCreator } from 'postcss'
import hash from 'hash-sum'
@ -32,7 +32,7 @@ function genVarName(id: string, raw: string, isProd: boolean): string {
return hash(id + raw)
} else {
// 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",
"dependencies": {
"@vue/shared": "3.4.0-alpha.1",
"@vue/compiler-dom": "3.4.0-alpha.1"
"@vue/shared": "workspace:*",
"@vue/compiler-dom": "workspace:*"
}
}

View File

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

View File

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

View File

@ -1472,6 +1472,31 @@ describe('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 {
DefineComponent,
ComponentOptionsMixin,

View File

@ -1,5 +1,9 @@
import { defineCustomElement } from 'vue'
import { expectType, describe } from './utils'
import {
defineCustomElement,
defineComponent,
type VueElementConstructor
} from 'vue'
import { expectType, describe, test } from './utils'
describe('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",
"private": true,
"version": "0.0.0",
"dependencies": {
"vue": "workspace:*",
"@vue/dts-built-test": "workspace:*"
},
"version": "3.4.0-alpha.1"
}
}

View File

@ -15,9 +15,10 @@ import {
MaybeRef,
MaybeRefOrGetter,
ComputedRef,
computed
computed,
ShallowRef
} from 'vue'
import { expectType, describe } from './utils'
import { expectType, describe, IsUnion } from './utils'
function plainType(arg: number | Ref<number>) {
// ref coercing
@ -174,6 +175,27 @@ if (refStatus.value === 'initial') {
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
const r1 = reactive({
k: 'v'

View File

@ -8,7 +8,8 @@ import {
defineSlots,
VNode,
Ref,
defineModel
defineModel,
toRefs
} from 'vue'
import { describe, expectType } from './utils'
import { defineComponent } from 'vue'
@ -20,6 +21,7 @@ describe('defineProps w/ type declaration', () => {
foo: string
bool?: boolean
boolAndUndefined: boolean | undefined
file?: File | File[]
}>()
// explicitly declared type should be refined
expectType<string>(props.foo)
@ -108,6 +110,7 @@ describe('defineProps w/ generic type declaration + withDefaults', <T extends
defineProps<{
n?: number
bool?: boolean
s?: string
generic1?: T[] | { x: T }
generic2?: { x: T }
@ -126,6 +129,10 @@ describe('defineProps w/ generic type declaration + withDefaults', <T extends
)
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<{ x: T }>(res.generic2)
@ -328,3 +335,11 @@ describe('useSlots', () => {
const slots = useSlots()
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' }]]} />
)
// 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
;<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'
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",
"dependencies": {
"@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.4.0-alpha.1",
"@vue/shared": "3.4.0-alpha.1",
"@babel/parser": "^7.23.3",
"@vue/compiler-core": "workspace:*",
"@vue/shared": "workspace:*",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5"
},
"devDependencies": {
"@babel/core": "^7.23.2",
"@babel/types": "^7.23.0"
"@babel/core": "^7.23.3",
"@babel/types": "^7.23.3"
}
}

View File

@ -448,8 +448,8 @@ export function transformAST(
const keyStr = isString(key)
? `'${key}'`
: key
? snip(key)
: `'${nameId.name}'`
? snip(key)
: `'${nameId.name}'`
const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
s.appendLeft(
call.end! + offset,

View File

@ -275,6 +275,14 @@ describe('reactivity/readonly', () => {
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)
}
})
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()
})
})
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', () => {
@ -197,5 +203,11 @@ describe('reactivity/shallowReadonly', () => {
).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",
"dependencies": {
"@vue/shared": "3.4.0-alpha.1"
"@vue/shared": "workspace:*"
}
}

View File

@ -109,8 +109,8 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target

View File

@ -228,7 +228,11 @@ function createReadonlyMethod(type: TriggerOpTypes): Function {
toRaw(this)
)
}
return type === TriggerOpTypes.DELETE ? false : this
return type === TriggerOpTypes.DELETE
? false
: type === TriggerOpTypes.CLEAR
? undefined
: this
}
}
@ -342,8 +346,8 @@ function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,

View File

@ -138,24 +138,24 @@ type Builtin = Primitive | Function | Date | Error | RegExp
export type DeepReadonly<T> = T extends Builtin
? T
: T extends Map<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends ReadonlyMap<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends WeakMap<infer K, infer V>
? WeakMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends Set<infer U>
? ReadonlySet<DeepReadonly<U>>
: T extends ReadonlySet<infer U>
? ReadonlySet<DeepReadonly<U>>
: T extends WeakSet<infer U>
? WeakSet<DeepReadonly<U>>
: T extends Promise<infer U>
? Promise<DeepReadonly<U>>
: T extends Ref<infer U>
? Readonly<Ref<DeepReadonly<U>>>
: T extends {}
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: Readonly<T>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends ReadonlyMap<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends WeakMap<infer K, infer V>
? WeakMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends Set<infer U>
? ReadonlySet<DeepReadonly<U>>
: T extends ReadonlySet<infer U>
? ReadonlySet<DeepReadonly<U>>
: T extends WeakSet<infer U>
? WeakSet<DeepReadonly<U>>
: T extends Promise<infer U>
? Promise<DeepReadonly<U>>
: T extends Ref<infer U>
? Readonly<Ref<DeepReadonly<U>>>
: T extends {}
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: Readonly<T>
/**
* Takes an object (reactive or plain) or a ref and returns a readonly proxy to

View File

@ -1,3 +1,4 @@
import type { ComputedRef } from './computed'
import {
activeEffect,
shouldTrack,
@ -128,9 +129,8 @@ export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }
* @param value - The "inner value" for the shallow ref.
* @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowref}
*/
export function shallowRef<T extends object>(
value: T
): T extends Ref ? T : ShallowRef<T>
export function shallowRef<T>(value: MaybeRef<T>): Ref<T> | ShallowRef<T>
export function shallowRef<T extends Ref>(value: T): T
export function shallowRef<T>(value: T): ShallowRef<T>
export function shallowRef<T = any>(): ShallowRef<T | undefined>
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.
* @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
}
@ -244,7 +244,7 @@ export function unref<T>(ref: MaybeRef<T>): T {
* @param source - A getter, an existing ref, or a non-function value.
* @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)
}
@ -429,8 +429,8 @@ export function toRef<T>(
): T extends () => infer R
? Readonly<Ref<R>>
: T extends Ref
? T
: Ref<UnwrapRef<T>>
? T
: Ref<UnwrapRef<T>>
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K
@ -491,17 +491,17 @@ export type ShallowUnwrapRef<T> = {
[K in keyof T]: T[K] extends Ref<infer V>
? V // if `V` is `unknown` that means it does not extend `Ref` and is undefined
: T[K] extends Ref<infer V> | undefined
? unknown extends V
? undefined
: V | undefined
: T[K]
? unknown extends V
? undefined
: V | undefined
: T[K]
}
export type UnwrapRef<T> = T extends ShallowRef<infer V>
? V
: T extends Ref<infer V>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>
export type UnwrapRefSimple<T> = T extends
| Function
@ -512,9 +512,9 @@ export type UnwrapRefSimple<T> = T extends
| { [RawSymbol]?: true }
? T
: T extends ReadonlyArray<any>
? { [K in keyof T]: UnwrapRefSimple<T[K]> }
: T extends object & { [ShallowReactiveMarker]?: never }
? {
[P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>
}
: T
? { [K in keyof T]: UnwrapRefSimple<T[K]> }
: T extends object & { [ShallowReactiveMarker]?: never }
? {
[P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>
}
: T

View File

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

View File

@ -17,9 +17,12 @@ import {
onUnmounted,
onErrorCaptured,
shallowRef,
SuspenseProps,
resolveDynamicComponent,
Fragment
} from '@vue/runtime-test'
import { createApp, defineComponent } from 'vue'
import { type RawSlots } from 'packages/runtime-core/src/componentSlots'
describe('Suspense', () => {
const deps: Promise<any>[] = []
@ -1523,4 +1526,75 @@ describe('Suspense', () => {
expected = `<div>outerB</div><div>innerB</div>`
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)
})
// #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 () => {
const root = nodeOps.createElement('div')
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', () => {
const { container } = mountWithHydration(
'<input type="checkbox" value="true">',
@ -953,6 +965,20 @@ describe('SSR hydration', () => {
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', () => {
const { container } = mountWithHydration(
'<select><option :value="true">ok</option></select>',
@ -1177,5 +1203,21 @@ describe('SSR hydration', () => {
expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
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(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])
})
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 vnode =
(openBlock(),
createBlock('div', null, [
hoist,
createVNode('div', null, 'text', PatchFlags.HYDRATE_EVENTS)
createVNode('div', null, 'text', PatchFlags.NEED_HYDRATION)
]))
expect(vnode.dynamicChildren).toStrictEqual([])
})

View File

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

View File

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

View File

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

View File

@ -60,10 +60,10 @@ type MapSources<T, Immediate> = {
? V | undefined
: V
: T[K] extends object
? Immediate extends true
? T[K] | undefined
: T[K]
: never
? Immediate extends true
? T[K] | undefined
: T[K]
: never
}
type OnCleanup = (cleanupFn: () => void) => void
@ -288,10 +288,11 @@ function doWatch(
getter = () => traverse(baseGetter())
}
let cleanup: () => void
let cleanup: (() => void) | undefined
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
cleanup = effect.onStop = undefined
}
}
@ -348,8 +349,8 @@ function doWatch(
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
? []
: oldValue,
onCleanup
])
oldValue = newValue

View File

@ -251,8 +251,8 @@ export function createCompatVue(
mergeBase[key] = isArray(superValue)
? superValue.slice()
: isObject(superValue)
? extend(Object.create(null), superValue)
: superValue
? extend(Object.create(null), superValue)
: superValue
}
SubVue.options = mergeOptions(

View File

@ -922,10 +922,10 @@ export function finishComponentSetup(
(__ESM_BUNDLER__
? ` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
: __ESM_BROWSER__
? ` Use "vue.esm-browser.js" instead.`
: __GLOBAL__
? ` Use "vue.global.js" instead.`
: ``) /* should not happen */
? ` Use "vue.esm-browser.js" instead.`
: __GLOBAL__
? ` Use "vue.global.js" instead.`
: ``) /* should not happen */
)
} else {
warn(`Component is missing template or render function.`)

View File

@ -41,19 +41,19 @@ export type EmitsToProps<T extends EmitsOptions> = T extends string[]
[K in string & `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
}
: T extends ObjectEmitsOptions
? {
[K in string &
`on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
? T[Uncapitalize<C>] extends null
? (...args: any[]) => any
: (
...args: T[Uncapitalize<C>] extends (...args: infer P) => any
? P
: never
) => any
: never
}
: {}
? {
[K in string &
`on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
? T[Uncapitalize<C>] extends null
? (...args: any[]) => any
: (
...args: T[Uncapitalize<C>] extends (...args: infer P) => any
? P
: never
) => any
: never
}
: {}
export type EmitFn<
Options = ObjectEmitsOptions,
@ -61,14 +61,14 @@ export type EmitFn<
> = Options extends Array<infer V>
? (event: V, ...args: any[]) => void
: {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
? (event: string, ...args: any[]) => void
: UnionToIntersection<
{
[key in Event]: Options[key] extends (...args: infer Args) => any
? (event: key, ...args: Args) => void
: (event: key, ...args: any[]) => void
}[Event]
>
? (event: string, ...args: any[]) => void
: UnionToIntersection<
{
[key in Event]: Options[key] extends (...args: infer Args) => any
? (event: key, ...args: Args) => void
: (event: key, ...args: any[]) => void
}[Event]
>
export function emit(
instance: ComponentInternalInstance,

View File

@ -419,8 +419,8 @@ export type ExtractComputedReturns<T extends any> = {
[key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn }
? TReturn
: T[key] extends (...args: any[]) => infer TReturn
? TReturn
: never
? TReturn
: never
}
export type ObjectWatchOptionItem = {
@ -450,10 +450,10 @@ export type InjectToObject<T extends ComponentInjectOptions> =
[K in T[number]]?: unknown
}
: T extends ObjectInjectOptions
? {
[K in keyof T]?: unknown
}
: never
? {
[K in keyof T]?: unknown
}
: never
interface LegacyOptions<
Props,
@ -752,8 +752,8 @@ export function applyOptions(instance: ComponentInternalInstance) {
const get = isFunction(opt)
? opt.bind(publicThis, publicThis)
: isFunction(opt.get)
? opt.get.bind(publicThis, publicThis)
: NOOP
? opt.get.bind(publicThis, publicThis)
: NOOP
if (__DEV__ && get === NOOP) {
warn(`Computed property "${key}" has no getter.`)
}
@ -761,12 +761,12 @@ export function applyOptions(instance: ComponentInternalInstance) {
!isFunction(opt) && isFunction(opt.set)
? opt.set.bind(publicThis)
: __DEV__
? () => {
warn(
`Write operation failed: computed property "${key}" is readonly.`
)
}
: NOOP
? () => {
warn(
`Write operation failed: computed property "${key}" is readonly.`
)
}
: NOOP
const c = computed({
get,
set

View File

@ -111,22 +111,22 @@ type DefaultKeys<T> = {
type InferPropType<T> = [T] extends [null]
? any // null & true would fail to infer
: [T] extends [{ type: null | true }]
? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean`
: [T] extends [ObjectConstructor | { type: ObjectConstructor }]
? Record<string, any>
: [T] extends [BooleanConstructor | { type: BooleanConstructor }]
? boolean
: [T] extends [DateConstructor | { type: DateConstructor }]
? Date
: [T] extends [(infer U)[] | { type: (infer U)[] }]
? U extends DateConstructor
? Date | InferPropType<U>
: InferPropType<U>
: [T] extends [Prop<infer V, infer D>]
? unknown extends V
? IfAny<V, V, D>
: V
: T
? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean`
: [T] extends [ObjectConstructor | { type: ObjectConstructor }]
? Record<string, any>
: [T] extends [BooleanConstructor | { type: BooleanConstructor }]
? boolean
: [T] extends [DateConstructor | { type: DateConstructor }]
? Date
: [T] extends [(infer U)[] | { type: (infer U)[] }]
? U extends DateConstructor
? Date | InferPropType<U>
: InferPropType<U>
: [T] extends [Prop<infer V, infer D>]
? unknown extends V
? IfAny<V, V, D>
: V
: T
/**
* Extract prop types from a runtime props options object.
@ -725,6 +725,12 @@ function getInvalidTypeMessage(
value: unknown,
expectedTypes: 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 =
`Invalid prop: type check failed for prop "${name}".` +
` Expected ${expectedTypes.map(capitalize).join(' | ')}`

View File

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

View File

@ -73,9 +73,24 @@ export function renderComponentRoot(
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
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(
render!.call(
proxyToUse,
thisProxy,
proxyToUse!,
renderCache,
props,

View File

@ -474,9 +474,13 @@ function emptyPlaceholder(vnode: VNode): VNode | undefined {
function getKeepAliveChild(vnode: VNode): VNode | undefined {
return isKeepAlive(vnode)
? vnode.children
? ((vnode.children as VNodeArrayChildren)[0] as VNode)
: undefined
? // #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)
: undefined
: vnode
}

View File

@ -29,6 +29,7 @@ import {
assertNumber
} from '../warning'
import { handleError, ErrorCodes } from '../errorHandling'
import { NULL_DYNAMIC_COMPONENT } from '../helpers/resolveAssets'
export interface SuspenseProps {
onResolve?: () => void
@ -795,7 +796,11 @@ function normalizeSuspenseSlot(s: any) {
}
if (isArray(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.`)
}
s = singleChild

View File

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

View File

@ -111,6 +111,21 @@ export function createHydrationFunctions(
let domType = node.nodeType
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) {
optimized = false
vnode.dynamicChildren = null
@ -145,18 +160,17 @@ export function createHydrationFunctions(
}
break
case Comment:
if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
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)
} else {
nextNode = onMismatch()
}
if (isTemplateNode(node)) {
nextNode = nextSibling(node)
// 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()
} else {
nextNode = nextSibling(node)
}
@ -209,7 +223,7 @@ export function createHydrationFunctions(
(domType !== DOMNodeTypes.ELEMENT ||
(vnode.type as string).toLowerCase() !==
(node as Element).tagName.toLowerCase()) &&
!isTemplateNode(node as Element)
!isTemplateNode(node)
) {
nextNode = onMismatch()
} else {
@ -322,24 +336,28 @@ export function createHydrationFunctions(
const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode
// #4006 for form elements with non-string v-model value bindings
// 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
// #5405 in dev, always hydrate children for HMR
if (__DEV__ || forcePatchValue || patchFlag !== PatchFlags.HOISTED) {
if (__DEV__ || forcePatch || patchFlag !== PatchFlags.HOISTED) {
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
if (props) {
if (
forcePatchValue ||
forcePatch ||
!optimized ||
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.HYDRATE_EVENTS)
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
) {
for (const key in props) {
if (
(forcePatchValue && key.endsWith('value')) ||
(isOn(key) && !isReservedProp(key))
(forcePatch &&
(key.endsWith('value') || key === 'indeterminate')) ||
(isOn(key) && !isReservedProp(key)) ||
// force hydrate v-bind with .prop modifiers
key[0] === '.'
) {
patchProp(
el,
@ -564,8 +582,8 @@ export function createHydrationFunctions(
node.nodeType === DOMNodeTypes.TEXT
? `(text)`
: isComment(node) && node.data === '['
? `(start of fragment)`
: ``
? `(start of fragment)`
: ``
)
vnode.el = null
@ -637,17 +655,16 @@ export function createHydrationFunctions(
let parent = parentComponent
while (parent) {
if (parent.vnode.el === oldNode) {
parent.vnode.el = newNode
parent.subTree.el = newNode
parent.vnode.el = parent.subTree.el = newNode
}
parent = parent.parent
}
}
const isTemplateNode = (node: Element): boolean => {
const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
return (
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
let c2 = ch2[i] as VNode
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.el = c1.el
}

View File

@ -488,7 +488,7 @@ function createBaseVNode(
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// 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.patchFlag !== PatchFlags.HYDRATE_EVENTS
vnode.patchFlag !== PatchFlags.NEED_HYDRATION
) {
currentBlock.push(vnode)
}
@ -573,20 +573,20 @@ function _createVNode(
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
type = toRaw(type)
warn(
`Vue received a Component which was made a reactive object. This can ` +
`lead to unnecessary performance overhead, and should be avoided by ` +
`Vue received a Component that was made a reactive object. This can ` +
`lead to unnecessary performance overhead and should be avoided by ` +
`marking the component with \`markRaw\` or using \`shallowRef\` ` +
`instead of \`ref\`.`,
`\nComponent that was made reactive: `,

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