Merge remote-tracking branch 'upstream/minor'

This commit is contained in:
三咲智子 Kevin Deng 2024-07-19 18:30:20 +08:00
commit a8248cf152
No known key found for this signature in database
125 changed files with 4476 additions and 2164 deletions

View File

@ -17,6 +17,27 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
## Pull Request Guidelines
### What kinds of Pull Requests are accepted?
- Bug fix that addresses a clearly identified bug. **"Clearly identified bug"** means the bug has a proper reproduction either from a related open issue, or is included in the PR itself. Avoid submitting PRs that claim to fix something but do not sufficiently explain what is being fixed.
- New feature that addresses a clearly explained and widely applicable use case. **"Widely applicable"** means the new feature should provide non-trivial improvements to the majority of the user base. Vue already has a large API surface so we are quite cautious about adding new features - if the use case is niche and can be addressed via userland implementations, it likely isn't suitable to go into core.
The feature implementation should also consider the trade-off between the added complexity vs. the benefits gained. For example, if a small feature requires significant changes that spreads across the codebase, it is likely not worth it, or the approach should be reconsidered.
If the feature has a non-trivial API surface addition, or significantly affects the way a common use case is approached by the users, it should go through a discussion first in the [RFC repo](https://github.com/vuejs/rfcs/discussions). PRs of such features without prior discussion make it really difficult to steer / adjust the API design due to coupling with concrete implementations, and can lead to wasted work.
- Chore: typos, comment improvements, build config, CI config, etc. For typos and comment changes, try to combine multiple of them into a single PR.
- **It should be noted that we discourage contributors from submitting code refactors that are largely stylistic.** Code refactors are only accepted if it improves performance, or comes with sufficient explanations on why it objectively improves the code quality (e.g. makes a related feature implementation easier).
The reason is that code readability is subjective. The maintainers of this project have chosen to write the code in its current style based on our preferences, and we do not want to spend time explaining our stylistic preferences. Contributors should just respect the established conventions when contributing code.
Another aspect of it is that large scale stylistic changes result in massive diffs that touch multiple files, adding noise to the git history and makes tracing behavior changes across commits more cumbersome.
### Pull Request Checklist
- 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.

View File

@ -28,7 +28,7 @@
},
{
groupName: 'build',
matchPackageNames: ['vite', 'terser'],
matchPackageNames: ['vite', '@swc/core'],
matchPackagePrefixes: ['rollup', 'esbuild', '@rollup', '@vitejs'],
},
{

34
.github/workflows/autofix.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: autofix.ci
on:
pull_request:
permissions:
contents: read
jobs:
autofix:
runs-on: ubuntu-latest
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- run: pnpm install
- name: Run eslint
run: pnpm run lint --fix
- name: Run prettier
run: pnpm run format
- uses: autofix-ci/action@2891949f3779a1cafafae1523058501de3d4e944

View File

@ -88,7 +88,7 @@ jobs:
- run: pnpm install
- run: pnpm release --vapor --skip-tests --tag ${{ github.ref == 'refs/heads/main' && 'latest' || 'branch' }}
- run: pnpm release --vapor --skipTests --tag ${{ github.ref == 'refs/heads/main' && 'latest' || 'branch' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: 'true'

View File

@ -52,3 +52,13 @@ jobs:
with:
name: size-data
path: temp/size
- name: Save PR number
if: ${{github.event_name == 'pull_request'}}
run: echo ${{ github.event.number }} > ./pr.txt
- uses: actions/upload-artifact@v4
if: ${{github.event_name == 'pull_request'}}
with:
name: pr-number
path: pr.txt

View File

@ -35,15 +35,28 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Download PR number
uses: dawidd6/action-download-artifact@v6
with:
name: pr-number
run_id: ${{ github.event.workflow_run.id }}
path: /tmp/pr-number
- name: Read PR Number
id: pr-number
uses: juliangruber/read-file-action@v1
with:
path: /tmp/pr-number/pr.txt
- name: Download Size Data
uses: dawidd6/action-download-artifact@v4
uses: dawidd6/action-download-artifact@v6
with:
name: size-data
run_id: ${{ github.event.workflow_run.id }}
path: temp/size
- name: Download Previous Size Data
uses: dawidd6/action-download-artifact@v4
uses: dawidd6/action-download-artifact@v6
with:
branch: main
workflow: size-data.yml
@ -52,7 +65,7 @@ jobs:
path: temp/size-prev
if_no_artifact_found: warn
- name: Compare size
- name: Prepare report
run: pnpm tsx scripts/size-report.ts > size-report.md
- name: Read Size Report
@ -65,6 +78,7 @@ jobs:
uses: actions-cool/maintain-one-comment@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ steps.pr-number.outputs.content }}
body: |
${{ steps.size-report.outputs.content }}
<!-- VUE_CORE_SIZE -->

View File

@ -1,5 +1,86 @@
# [3.5.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.33...v3.5.0-alpha.3) (2024-07-19)
### Bug Fixes
* **build:** enable SSR branches in esm-browser builds ([b14cd9a](https://github.com/vuejs/core/commit/b14cd9a68bab082332b0169be075be357be076ca))
* **compiler-core:** change node hoisting to caching per instance ([#11067](https://github.com/vuejs/core/issues/11067)) ([cd0ea0d](https://github.com/vuejs/core/commit/cd0ea0d479a276583fa181d8ecbc97fb0e4a9dce)), closes [#5256](https://github.com/vuejs/core/issues/5256) [#9219](https://github.com/vuejs/core/issues/9219) [#10959](https://github.com/vuejs/core/issues/10959)
* **compiler-sfc:** should properly walk desutructured props when reactive destructure is not enabled ([0fd6193](https://github.com/vuejs/core/commit/0fd6193def2380916eb51a118f37f2d9ec2ace23)), closes [#11325](https://github.com/vuejs/core/issues/11325)
* **types:** respect props with default on instance type when using __typeProps ([96e4738](https://github.com/vuejs/core/commit/96e473833422342c5ca371ae1aeb186dec9a55e3))
### Features
* **runtime-core:** useTemplateRef() ([3ba70e4](https://github.com/vuejs/core/commit/3ba70e49b5856c53611c314d4855d679a546a7df))
* **runtime-core:** useId() and app.config.idPrefix ([#11404](https://github.com/vuejs/core/issues/11404)) ([73ef156](https://github.com/vuejs/core/commit/73ef1561f6905d69f968c094d0180c61824f1247))
* **runtime-core:** add app.config.throwUnhandledErrorInProduction ([f476b7f](https://github.com/vuejs/core/commit/f476b7f030f2dd427ca655fcea36f4933a4b4da0)), closes [#7876](https://github.com/vuejs/core/issues/7876)
* **teleport:** support deferred Teleport ([#11387](https://github.com/vuejs/core/issues/11387)) ([59a3e88](https://github.com/vuejs/core/commit/59a3e88903b10ac2278170a44d5a03f24fef23ef)), closes [#2015](https://github.com/vuejs/core/issues/2015) [#11386](https://github.com/vuejs/core/issues/11386)
* **compiler-core:** support `Symbol` global in template expressions ([#9069](https://github.com/vuejs/core/issues/9069)) ([a501a85](https://github.com/vuejs/core/commit/a501a85a7c910868e01a5c70a2abea4e9d9e87f3))
* **types:** export more emit related types ([#11017](https://github.com/vuejs/core/issues/11017)) ([189573d](https://github.com/vuejs/core/commit/189573dcee2a16bd3ed36ff5589d43f535e5e733))
## [3.4.33](https://github.com/vuejs/core/compare/v3.4.32...v3.4.33) (2024-07-19)
### Bug Fixes
* **runtime-dom:** handle undefined values in v-html ([#11403](https://github.com/vuejs/core/issues/11403)) ([5df67e3](https://github.com/vuejs/core/commit/5df67e36756639ea7b923d1b139d6cb14450123b))
## [3.4.32](https://github.com/vuejs/core/compare/v3.4.31...v3.4.32) (2024-07-17)
### Bug Fixes
* **build:** use consistent minify options from previous terser config ([789675f](https://github.com/vuejs/core/commit/789675f65d2b72cf979ba6a29bd323f716154a4b))
* **compiler-sfc:** correctly resolve type annotation for declared function ([#11279](https://github.com/vuejs/core/issues/11279)) ([b287aee](https://github.com/vuejs/core/commit/b287aeec3ea85f20e4b1fc3d907c901bdc2a0176)), closes [#11266](https://github.com/vuejs/core/issues/11266)
* **defineModel:** force local update when setter results in same emitted value ([de174e1](https://github.com/vuejs/core/commit/de174e1aa756508c7542605a448e55a373afb1ed)), closes [#10279](https://github.com/vuejs/core/issues/10279) [#10301](https://github.com/vuejs/core/issues/10301)
* **hmr:** hmr reload should work with async component ([#11248](https://github.com/vuejs/core/issues/11248)) ([c8b9794](https://github.com/vuejs/core/commit/c8b97945759e869c997d60c3350d2451c5ff7887))
* **hydration:** fix tracking of reactive style objects in production ([c10e40a](https://github.com/vuejs/core/commit/c10e40a217b89ab7e0f7f3515242d4246ecffbdd)), closes [#11372](https://github.com/vuejs/core/issues/11372)
* **hydration:** handle consectuvie text nodes during hydration ([f44c3b3](https://github.com/vuejs/core/commit/f44c3b37d446d5f8e34539029dae0d806b25bb47)), closes [#7285](https://github.com/vuejs/core/issues/7285) [#7301](https://github.com/vuejs/core/issues/7301)
* **reactivity:** ensure `unref` correctly resolves type for `ShallowRef` ([#11360](https://github.com/vuejs/core/issues/11360)) ([a509e30](https://github.com/vuejs/core/commit/a509e30f059fcdd158f39fdf34670b1019eaf2d1)), closes [#11356](https://github.com/vuejs/core/issues/11356)
* **reactivity:** shallowReactive map "unwraps" the nested refs ([#8503](https://github.com/vuejs/core/issues/8503)) ([50ddafe](https://github.com/vuejs/core/commit/50ddafe91b9195cf94124466239f82c9794699fb)), closes [#8501](https://github.com/vuejs/core/issues/8501) [#11249](https://github.com/vuejs/core/issues/11249)
* **runtime-core:** avoid recursive warning ([3ee7b4c](https://github.com/vuejs/core/commit/3ee7b4c7b1374c5bdc50a579b49f6bc15022b085)), closes [#8074](https://github.com/vuejs/core/issues/8074)
* **runtime-core:** bail manually rendered compiler slot fragments in all cases ([3d34f40](https://github.com/vuejs/core/commit/3d34f406ac7497dafd2f4e62ab23579b78a0e08a)), closes [#10870](https://github.com/vuejs/core/issues/10870)
* **runtime-core:** do not emit when defineModel ref is set with same value ([#11162](https://github.com/vuejs/core/issues/11162)) ([f1bb0ae](https://github.com/vuejs/core/commit/f1bb0aef084b5cdd4d49aecfed01ec106d9b6897)), closes [#11125](https://github.com/vuejs/core/issues/11125)
* **runtime-core:** errors during component patch should be caught by error handlers ([ee0248a](https://github.com/vuejs/core/commit/ee0248accff589a94688e177e5e3af10c18288cb))
* **runtime-core:** force diff slot fallback content and provided content ([d76dd9c](https://github.com/vuejs/core/commit/d76dd9c58de24b273bc55af3a8ed81ba693e9683)), closes [#7256](https://github.com/vuejs/core/issues/7256) [#9200](https://github.com/vuejs/core/issues/9200) [#9308](https://github.com/vuejs/core/issues/9308) [#7266](https://github.com/vuejs/core/issues/7266) [#9213](https://github.com/vuejs/core/issues/9213)
* **runtime-core:** more edge case fix for manually rendered compiled slot ([685e3f3](https://github.com/vuejs/core/commit/685e3f381c024b9f4023e60fe0545dc60d90d984)), closes [#11336](https://github.com/vuejs/core/issues/11336)
* **runtime-core:** use separate prop caches for components and mixins ([#11350](https://github.com/vuejs/core/issues/11350)) ([b0aa234](https://github.com/vuejs/core/commit/b0aa234e5e7a611c018de68bc31e0cf55518d5ce)), closes [#7998](https://github.com/vuejs/core/issues/7998)
* **runtime-dom:** properly handle innerHTML unmount into new children ([#11159](https://github.com/vuejs/core/issues/11159)) ([3e9e32e](https://github.com/vuejs/core/commit/3e9e32ee0a6d0fbf67e9098a66ff0a1ea6647806)), closes [#9135](https://github.com/vuejs/core/issues/9135)
* **teleport:** skip teleported nodes when locating patch anchor ([8655ced](https://github.com/vuejs/core/commit/8655ced480ea0fe453ff5fe445cecf97b91ec260)), closes [#9071](https://github.com/vuejs/core/issues/9071) [#9134](https://github.com/vuejs/core/issues/9134) [#9313](https://github.com/vuejs/core/issues/9313) [#9313](https://github.com/vuejs/core/issues/9313)
* **v-model:** component v-model modifiers trim and number when cases don't match ([#9609](https://github.com/vuejs/core/issues/9609)) ([7fb6eb8](https://github.com/vuejs/core/commit/7fb6eb882b64bf99a99d00606e54b0e050674206)), closes [#4848](https://github.com/vuejs/core/issues/4848) [#4850](https://github.com/vuejs/core/issues/4850) [#4850](https://github.com/vuejs/core/issues/4850)
* **v-once:** properly unmount v-once cached trees ([d343a0d](https://github.com/vuejs/core/commit/d343a0dc01663f91db42b4ddb693e6fffcb45873)), closes [#5154](https://github.com/vuejs/core/issues/5154) [#8809](https://github.com/vuejs/core/issues/8809)
### Performance Improvements
* **server-renderer:** avoid unnecessary checks in `createBuffer` ([#11364](https://github.com/vuejs/core/issues/11364)) ([fc205bf](https://github.com/vuejs/core/commit/fc205bf4decde5ce0f4a61394ffa3914b502c287))
* **server-renderer:** optimize `unrollBuffer` by avoiding promises ([#11340](https://github.com/vuejs/core/issues/11340)) ([05779a7](https://github.com/vuejs/core/commit/05779a70bd0b567ae458a07636d229bd07c44c4e))
## [3.4.31](https://github.com/vuejs/core/compare/v3.4.30...v3.4.31) (2024-06-28)
### Bug Fixes
* **compiler-core:** handle inline comments with undefined bindings ([#11217](https://github.com/vuejs/core/issues/11217)) ([746352a](https://github.com/vuejs/core/commit/746352a14d62e9d3d9a38c359d2c54d418c1e0ac)), closes [#11216](https://github.com/vuejs/core/issues/11216)
* **shared:** unwrap refs in toDisplayString ([#7306](https://github.com/vuejs/core/issues/7306)) ([0126cff](https://github.com/vuejs/core/commit/0126cfff9d93bcec70e5745519f6378e3cd3f39c)), closes [#5578](https://github.com/vuejs/core/issues/5578) [#5593](https://github.com/vuejs/core/issues/5593) [#11199](https://github.com/vuejs/core/issues/11199) [#11201](https://github.com/vuejs/core/issues/11201)
### Reverts
* Revert "fix(reactivity): avoid infinite loop when render access a side effect computed ([#11135](https://github.com/vuejs/core/issues/11135))" ([e0df985](https://github.com/vuejs/core/commit/e0df985f0317fb65c5b461bf224375c7763f0269))
* Revert "fix(reactivity): fix side effect computed dirty level (#11183)" ([6c303ea](https://github.com/vuejs/core/commit/6c303eacd14b7b0de0accc228f6abeb43d706f63)), closes [#11183](https://github.com/vuejs/core/issues/11183)
## [3.4.30](https://github.com/vuejs/core/compare/v3.4.29...v3.4.30) (2024-06-22)
**Note: this release contains a fix (#11150) that requires `vue-tsc` to also be updated in sync to ^2.0.22. See #11196**
### Bug Fixes

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "3.0.0-vapor",
"packageManager": "pnpm@9.3.0",
"packageManager": "pnpm@9.5.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js vue vue-vapor",
@ -59,59 +59,54 @@
"node": ">=18.12.0"
},
"devDependencies": {
"@babel/parser": "^7.24.7",
"@babel/types": "^7.24.7",
"@codspeed/vitest-plugin": "^3.1.0",
"@babel/parser": "catalog:",
"@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.8",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.6.1",
"@swc/core": "^1.6.13",
"@types/hash-sum": "^1.0.2",
"@types/minimist": "^1.2.5",
"@types/node": "^20.14.2",
"@types/node": "^20.14.10",
"@types/semver": "^7.5.8",
"@vitest/coverage-istanbul": "^1.6.0",
"@vitest/ui": "^1.6.0",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^4.1.0",
"enquirer": "^2.4.1",
"esbuild": "^0.21.5",
"esbuild": "^0.23.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.5.0",
"eslint-plugin-import-x": "^0.5.1",
"eslint": "^9.6.0",
"eslint-plugin-import-x": "^0.5.3",
"eslint-plugin-vitest": "^0.5.4",
"estree-walker": "^2.0.2",
"execa": "^9.2.0",
"estree-walker": "catalog:",
"jsdom": "^24.1.0",
"lint-staged": "^15.2.7",
"lodash": "^4.17.21",
"magic-string": "^0.30.10",
"markdown-table": "^3.0.3",
"marked": "^12.0.2",
"minimist": "^1.2.8",
"npm-run-all2": "^6.2.0",
"npm-run-all2": "^6.2.2",
"picocolors": "^1.0.1",
"prettier": "^3.3.2",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~22.11.0",
"rimraf": "^5.0.7",
"rollup": "^4.18.0",
"puppeteer": "~22.12.1",
"rimraf": "^5.0.9",
"rollup": "^4.18.1",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.2",
"serve": "^14.2.3",
"simple-git-hooks": "^2.11.1",
"terser": "^5.31.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.6.3",
"tsx": "^4.15.5",
"tsx": "^4.16.2",
"typescript": "~5.4.5",
"typescript-eslint": "^7.13.0",
"vite": "^5.3.1",
"typescript-eslint": "^7.15.0",
"vite": "catalog:",
"vitest": "^1.6.0"
},
"pnpm": {

View File

@ -19,12 +19,12 @@ export function render(_ctx, _cache) {
}"
`;
exports[`compiler: codegen > CacheExpression w/ isVNode: true 1`] = `
exports[`compiler: codegen > CacheExpression w/ isVOnce: true 1`] = `
"
export function render(_ctx, _cache) {
return _cache[1] || (
_setBlockTracking(-1),
_cache[1] = foo,
(_cache[1] = foo).cacheIndex = 1,
_setBlockTracking(1),
_cache[1]
)
@ -54,7 +54,7 @@ return function render(_ctx, _cache) {
[foo + bar]: bar
}, [
_createElementVNode("p", { "some-key": "foo" })
], 16)
], 16 /* FULL_PROPS */)
}
}"
`;
@ -98,7 +98,7 @@ exports[`compiler: codegen > forNode 1`] = `
"
return function render(_ctx, _cache) {
with (_ctx) {
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(), 1))
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(), 1 /* TEXT */))
}
}"
`;

View File

@ -267,7 +267,7 @@ describe('compiler: codegen', () => {
disableTracking: true,
props: undefined,
children: createCallExpression(RENDER_LIST),
patchFlag: '1',
patchFlag: PatchFlags.TEXT,
dynamicProps: undefined,
directives: undefined,
loc: locStub,
@ -303,7 +303,7 @@ describe('compiler: codegen', () => {
disableTracking: false,
props: undefined,
children: createCallExpression(RENDER_LIST),
patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT),
patchFlag: PatchFlags.STABLE_FRAGMENT,
dynamicProps: undefined,
directives: undefined,
loc: locStub,
@ -364,7 +364,7 @@ describe('compiler: codegen', () => {
),
],
// flag
PatchFlags.FULL_PROPS + '',
PatchFlags.FULL_PROPS,
),
}),
)
@ -375,7 +375,7 @@ describe('compiler: codegen', () => {
[foo + bar]: bar
}, [
_${helperNameMap[CREATE_ELEMENT_VNODE]}("p", { "some-key": "foo" })
], ${PatchFlags.FULL_PROPS})`)
], ${genFlagText(PatchFlags.FULL_PROPS)})`)
expect(code).toMatchSnapshot()
})
@ -437,7 +437,7 @@ describe('compiler: codegen', () => {
expect(code).toMatchSnapshot()
})
test('CacheExpression w/ isVNode: true', () => {
test('CacheExpression w/ isVOnce: true', () => {
const { code } = generate(
createRoot({
cached: [],
@ -456,7 +456,7 @@ describe('compiler: codegen', () => {
`
_cache[1] || (
_setBlockTracking(-1),
_cache[1] = foo,
(_cache[1] = foo).cacheIndex = 1,
_setBlockTracking(1),
_cache[1]
)
@ -666,11 +666,14 @@ describe('compiler: codegen', () => {
})
test('with patchFlag and no children/props', () => {
expect(genCode(createVNodeCall(null, `"div"`, undefined, undefined, '1')))
.toMatchInlineSnapshot(`
"return _createElementVNode("div", null, null, 1)
"
`)
expect(
genCode(
createVNodeCall(null, `"div"`, undefined, undefined, PatchFlags.TEXT),
),
).toMatchInlineSnapshot(`
"return _createElementVNode("div", null, null, 1 /* TEXT */)
"
`)
})
test('as block', () => {

View File

@ -19,7 +19,6 @@ import { transformFor } from '../src/transforms/vFor'
import { transformElement } from '../src/transforms/transformElement'
import { transformSlotOutlet } from '../src/transforms/transformSlotOutlet'
import { transformText } from '../src/transforms/transformText'
import { genFlagText } from './testUtils'
import { PatchFlags } from '@vue/shared'
describe('compiler: transform', () => {
@ -358,7 +357,7 @@ describe('compiler: transform', () => {
{ type: NodeTypes.ELEMENT, tag: `div` },
{ type: NodeTypes.ELEMENT, tag: `div` },
] as any,
genFlagText(PatchFlags.STABLE_FRAGMENT),
PatchFlags.STABLE_FRAGMENT,
),
)
})
@ -374,10 +373,7 @@ describe('compiler: transform', () => {
{ type: NodeTypes.ELEMENT, tag: `div` },
{ type: NodeTypes.COMMENT },
] as any,
genFlagText([
PatchFlags.STABLE_FRAGMENT,
PatchFlags.DEV_ROOT_FRAGMENT,
]),
PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
),
)
})

View File

@ -8,7 +8,7 @@ return function render(_ctx, _cache) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */)
_createElementVNode("div", { key: "foo" }, null, -1 /* HOISTED */)
])))
}
}"
@ -25,11 +25,11 @@ return function render(_ctx, _cache) {
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
], -1 /* CACHED */),
], -1 /* HOISTED */),
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
], -1 /* CACHED */)
], -1 /* HOISTED */)
])))
}
}"
@ -45,7 +45,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", null, [
_createCommentVNode("comment")
], -1 /* CACHED */)
], -1 /* HOISTED */)
])))
}
}"
@ -59,9 +59,9 @@ return function render(_ctx, _cache) {
const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */),
_createElementVNode("span", null, null, -1 /* HOISTED */),
_createTextVNode("foo"),
_createElementVNode("div", null, null, -1 /* CACHED */)
_createElementVNode("div", null, null, -1 /* HOISTED */)
])))
}
}"
@ -75,7 +75,7 @@ return function render(_ctx, _cache) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */)
_createElementVNode("span", { class: "inline" }, "hello", -1 /* HOISTED */)
])))
}
}"
@ -148,7 +148,7 @@ return function render(_ctx, _cache) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */)
_createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* HOISTED */)
])))
}
}"
@ -162,7 +162,7 @@ return function render(_ctx, _cache) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */)
_createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* HOISTED */)
])))
}
}"
@ -178,7 +178,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(1, (i) => {
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", { class: "hi" }, null, -1 /* CACHED */)
_createElementVNode("span", { class: "hi" }, null, -1 /* HOISTED */)
]))]))
}), 256 /* UNKEYED_FRAGMENT */))
]))
@ -216,7 +216,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_withDirectives((_openBlock(), _createElementBlock("svg", null, _cache[0] || (_cache[0] = [
_createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */)
_createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* HOISTED */)
]))), [
[_directive_foo]
])
@ -402,7 +402,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
ok
? (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
_createElementVNode("span", null, null, -1 /* HOISTED */)
])))
: _createCommentVNode("v-if", true)
]))
@ -423,7 +423,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
_createElementVNode("span", null, null, -1 /* HOISTED */)
])))
}), 256 /* UNKEYED_FRAGMENT */))
]))

View File

@ -9,7 +9,7 @@ return function render(_ctx, _cache) {
return _cache[0] || (
_setBlockTracking(-1),
_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]),
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
@ -29,7 +29,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"]),
(_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
@ -48,7 +48,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"]),
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
@ -67,7 +67,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
_cache[0] = _renderSlot($slots, "default"),
(_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)
@ -86,7 +86,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
_cache[0] = _createElementVNode("div"),
(_cache[0] = _createElementVNode("div")).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
)

View File

@ -21,7 +21,7 @@ import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor'
import { transformBind } from '../../src/transforms/vBind'
import { transformOn } from '../../src/transforms/vOn'
import { createObjectMatcher, genFlagText } from '../testUtils'
import { createObjectMatcher } from '../testUtils'
import { transformText } from '../../src/transforms/transformText'
import { PatchFlags } from '@vue/shared'
@ -348,7 +348,7 @@ describe('compiler: cacheStatic transform', () => {
id: `[foo]`,
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.PROPS),
patchFlag: PatchFlags.PROPS,
dynamicProps: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`,
@ -402,7 +402,7 @@ describe('compiler: cacheStatic transform', () => {
ref: `[foo]`,
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.NEED_PATCH),
patchFlag: PatchFlags.NEED_PATCH,
},
},
])
@ -424,7 +424,7 @@ describe('compiler: cacheStatic transform', () => {
content: `_hoisted_1`,
},
children: undefined,
patchFlag: genFlagText(PatchFlags.NEED_PATCH),
patchFlag: PatchFlags.NEED_PATCH,
directives: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
@ -448,7 +448,7 @@ describe('compiler: cacheStatic transform', () => {
tag: `"div"`,
props: { content: `_hoisted_1` },
children: { type: NodeTypes.INTERPOLATION },
patchFlag: genFlagText(PatchFlags.TEXT),
patchFlag: PatchFlags.TEXT,
},
},
])
@ -520,7 +520,7 @@ describe('compiler: cacheStatic transform', () => {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
},
patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
patchFlag: PatchFlags.UNKEYED_FRAGMENT,
})
const innerBlockCodegen = forBlockCodegen!.children.arguments[1]
expect(innerBlockCodegen.returns).toMatchObject({
@ -620,7 +620,7 @@ describe('compiler: cacheStatic transform', () => {
constType: ConstantTypes.NOT_CONSTANT,
},
},
patchFlag: `1 /* TEXT */`,
patchFlag: PatchFlags.TEXT,
},
},
],
@ -750,7 +750,7 @@ describe('compiler: cacheStatic transform', () => {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
},
patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
patchFlag: PatchFlags.UNKEYED_FRAGMENT,
})
const innerBlockCodegen = forBlockCodegen!.children.arguments[1]
expect(innerBlockCodegen.returns).toMatchObject({

View File

@ -37,7 +37,7 @@ import { transformStyle } from '../../../compiler-dom/src/transforms/transformSt
import { transformOn } from '../../src/transforms/vOn'
import { transformBind } from '../../src/transforms/vBind'
import { PatchFlags } from '@vue/shared'
import { createObjectMatcher, genFlagText } from '../testUtils'
import { createObjectMatcher } from '../testUtils'
import { transformText } from '../../src/transforms/transformText'
import { parseWithForTransform } from './vFor.spec'
@ -521,7 +521,7 @@ describe('compiler: element transform', () => {
// keep-alive should not compile content to slots
children: [{ type: NodeTypes.ELEMENT, tag: 'span' }],
// should get a dynamic slots flag to force updates
patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS),
patchFlag: PatchFlags.DYNAMIC_SLOTS,
})
}
@ -588,7 +588,7 @@ describe('compiler: element transform', () => {
})
// should factor in props returned by custom directive transforms
// in patchFlag analysis
expect(node.patchFlag).toMatch(PatchFlags.PROPS + '')
expect(node.patchFlag).toBe(PatchFlags.PROPS)
expect(node.dynamicProps).toMatch(`"bar"`)
})
@ -612,7 +612,7 @@ describe('compiler: element transform', () => {
tag: `"div"`,
props: undefined,
children: undefined,
patchFlag: genFlagText(PatchFlags.NEED_PATCH), // should generate appropriate flag
patchFlag: PatchFlags.NEED_PATCH, // should generate appropriate flag
directives: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
@ -945,26 +945,26 @@ describe('compiler: element transform', () => {
expect(node.patchFlag).toBeUndefined()
const { node: node2 } = parseWithBind(`<div>{{ foo }}</div>`)
expect(node2.patchFlag).toBe(genFlagText(PatchFlags.TEXT))
expect(node2.patchFlag).toBe(PatchFlags.TEXT)
// multiple nodes, merged with optimize text
const { node: node3 } = parseWithBind(`<div>foo {{ bar }} baz</div>`)
expect(node3.patchFlag).toBe(genFlagText(PatchFlags.TEXT))
expect(node3.patchFlag).toBe(PatchFlags.TEXT)
})
test('CLASS', () => {
const { node } = parseWithBind(`<div :class="foo" />`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.CLASS))
expect(node.patchFlag).toBe(PatchFlags.CLASS)
})
test('STYLE', () => {
const { node } = parseWithBind(`<div :style="foo" />`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.STYLE))
expect(node.patchFlag).toBe(PatchFlags.STYLE)
})
test('PROPS', () => {
const { node } = parseWithBind(`<div id="foo" :foo="bar" :baz="qux" />`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
expect(node.patchFlag).toBe(PatchFlags.PROPS)
expect(node.dynamicProps).toBe(`["foo", "baz"]`)
})
@ -973,7 +973,7 @@ describe('compiler: element transform', () => {
`<div id="foo" :class="cls" :style="styl" :foo="bar" :baz="qux"/>`,
)
expect(node.patchFlag).toBe(
genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS]),
PatchFlags.CLASS | PatchFlags.STYLE | PatchFlags.PROPS,
)
expect(node.dynamicProps).toBe(`["foo", "baz"]`)
})
@ -983,40 +983,40 @@ describe('compiler: element transform', () => {
const { node } = parseWithBind(
`<Foo :id="foo" :class="cls" :style="styl" />`,
)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
expect(node.patchFlag).toBe(PatchFlags.PROPS)
expect(node.dynamicProps).toBe(`["id", "class", "style"]`)
})
test('FULL_PROPS (v-bind)', () => {
const { node } = parseWithBind(`<div v-bind="foo" />`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
expect(node.patchFlag).toBe(PatchFlags.FULL_PROPS)
})
test('FULL_PROPS (dynamic key)', () => {
const { node } = parseWithBind(`<div :[foo]="bar" />`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
expect(node.patchFlag).toBe(PatchFlags.FULL_PROPS)
})
test('FULL_PROPS (w/ others)', () => {
const { node } = parseWithBind(
`<div id="foo" v-bind="bar" :class="cls" />`,
)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))
expect(node.patchFlag).toBe(PatchFlags.FULL_PROPS)
})
test('NEED_PATCH (static ref)', () => {
const { node } = parseWithBind(`<div ref="foo" />`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(node.patchFlag).toBe(PatchFlags.NEED_PATCH)
})
test('NEED_PATCH (dynamic ref)', () => {
const { node } = parseWithBind(`<div :ref="foo" />`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(node.patchFlag).toBe(PatchFlags.NEED_PATCH)
})
test('NEED_PATCH (custom directives)', () => {
const { node } = parseWithBind(`<div v-foo />`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(node.patchFlag).toBe(PatchFlags.NEED_PATCH)
})
test('NEED_PATCH (vnode hooks)', () => {
@ -1025,7 +1025,7 @@ describe('compiler: element transform', () => {
cacheHandlers: true,
}).ast
const node = (root as any).children[0].codegenNode
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
expect(node.patchFlag).toBe(PatchFlags.NEED_PATCH)
})
test('script setup inline mode template ref (binding exists)', () => {
@ -1120,7 +1120,7 @@ describe('compiler: element transform', () => {
},
})
// should only have props flag
expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
expect(node.patchFlag).toBe(PatchFlags.PROPS)
const { node: node2 } = parseWithElementTransform(
`<div @keyup="foo" />`,
@ -1130,21 +1130,15 @@ describe('compiler: element transform', () => {
},
},
)
expect(node2.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]),
)
expect(node2.patchFlag).toBe(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]),
)
expect(node.patchFlag).toBe(PatchFlags.PROPS | PatchFlags.NEED_HYDRATION)
const { node: node2 } = parseWithBind(`<div .id="id" />`)
expect(node2.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]),
)
expect(node2.patchFlag).toBe(PatchFlags.PROPS | PatchFlags.NEED_HYDRATION)
})
// #5870
@ -1157,9 +1151,7 @@ describe('compiler: element transform', () => {
},
},
)
expect(node.patchFlag).toBe(
genFlagText([PatchFlags.PROPS, PatchFlags.NEED_HYDRATION]),
)
expect(node.patchFlag).toBe(PatchFlags.PROPS | PatchFlags.NEED_HYDRATION)
})
test('should not have PROPS patchflag for constant v-on handlers', () => {
@ -1173,7 +1165,7 @@ describe('compiler: element transform', () => {
},
})
// should only have hydration flag
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_HYDRATION))
expect(node.patchFlag).toBe(PatchFlags.NEED_HYDRATION)
})
})

View File

@ -384,6 +384,17 @@ describe('compiler: expression transform', () => {
)
})
test('should not error', () => {
const onError = vi.fn()
parseWithExpressionTransform(
`<p :id="undefined /* force override the id */"/>`,
{
onError,
},
)
expect(onError).not.toHaveBeenCalled()
})
test('should prefix in assignment', () => {
const node = parseWithExpressionTransform(
`{{ x = 1 }}`,

View File

@ -18,8 +18,8 @@ import {
import { ErrorCodes } from '../../src/errors'
import { type CompilerOptions, generate } from '../../src'
import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers'
import { PatchFlagNames, PatchFlags } from '@vue/shared'
import { createObjectMatcher, genFlagText } from '../testUtils'
import { PatchFlags } from '@vue/shared'
import { createObjectMatcher } from '../testUtils'
export function parseWithForTransform(
template: string,
@ -696,10 +696,10 @@ describe('compiler: v-for', () => {
tag: FRAGMENT,
disableTracking,
patchFlag: !disableTracking
? genFlagText(PatchFlags.STABLE_FRAGMENT)
? PatchFlags.STABLE_FRAGMENT
: keyed
? genFlagText(PatchFlags.KEYED_FRAGMENT)
: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT,
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
@ -822,7 +822,7 @@ describe('compiler: v-for', () => {
constType: ConstantTypes.NOT_CONSTANT,
},
},
patchFlag: genFlagText(PatchFlags.TEXT),
patchFlag: PatchFlags.TEXT,
},
})
expect(generate(root).code).toMatchSnapshot()
@ -846,7 +846,7 @@ describe('compiler: v-for', () => {
{ type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: `span` },
],
patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT),
patchFlag: PatchFlags.STABLE_FRAGMENT,
},
})
expect(generate(root).code).toMatchSnapshot()
@ -950,7 +950,7 @@ describe('compiler: v-for', () => {
{ type: NodeTypes.TEXT, content: `hello` },
{ type: NodeTypes.ELEMENT, tag: `span` },
],
patchFlag: genFlagText(PatchFlags.STABLE_FRAGMENT),
patchFlag: PatchFlags.STABLE_FRAGMENT,
},
})
expect(generate(root).code).toMatchSnapshot()
@ -971,7 +971,7 @@ describe('compiler: v-for', () => {
}),
isBlock: true,
disableTracking: true,
patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
patchFlag: PatchFlags.UNKEYED_FRAGMENT,
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
@ -1009,7 +1009,7 @@ describe('compiler: v-for', () => {
}),
isBlock: true,
disableTracking: true,
patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
patchFlag: PatchFlags.UNKEYED_FRAGMENT,
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
@ -1048,9 +1048,7 @@ describe('compiler: v-for', () => {
const {
node: { codegenNode },
} = parseWithForTransform('<div v-for="key in keys" :key>test</div>')
expect(codegenNode.patchFlag).toBe(
`${PatchFlags.KEYED_FRAGMENT} /* ${PatchFlagNames[PatchFlags.KEYED_FRAGMENT]} */`,
)
expect(codegenNode.patchFlag).toBe(PatchFlags.KEYED_FRAGMENT)
})
test('template v-for key w/ :key shorthand on template injected to the child', () => {

View File

@ -24,7 +24,7 @@ import {
trackVForSlotScopes,
} from '../../src/transforms/vSlot'
import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeHelpers'
import { createObjectMatcher, genFlagText } from '../testUtils'
import { createObjectMatcher } from '../testUtils'
import { PatchFlags } from '@vue/shared'
import { transformFor } from '../../src/transforms/vFor'
import { transformIf } from '../../src/transforms/vIf'
@ -432,7 +432,7 @@ describe('compiler: transform component slots', () => {
),
// nested slot should be forced dynamic, since scope variables
// are not tracked as dependencies of the slot.
patchFlag: genFlagText(PatchFlags.DYNAMIC_SLOTS),
patchFlag: PatchFlags.DYNAMIC_SLOTS,
},
},
// test scope
@ -474,9 +474,7 @@ describe('compiler: transform component slots', () => {
const div = ((root.children[0] as ForNode).children[0] as ElementNode)
.codegenNode as any
const comp = div.children[0]
expect(comp.codegenNode.patchFlag).toBe(
genFlagText(PatchFlags.DYNAMIC_SLOTS),
)
expect(comp.codegenNode.patchFlag).toBe(PatchFlags.DYNAMIC_SLOTS)
})
test('should only force dynamic slots when actually using scope vars w/ prefixIdentifiers: true', () => {
@ -494,7 +492,7 @@ describe('compiler: transform component slots', () => {
flag = (innerComp.codegenNode as VNodeCall).patchFlag
}
if (shouldForce) {
expect(flag).toBe(genFlagText(PatchFlags.DYNAMIC_SLOTS))
expect(flag).toBe(PatchFlags.DYNAMIC_SLOTS)
} else {
expect(flag).toBeUndefined()
}
@ -581,8 +579,8 @@ describe('compiler: transform component slots', () => {
},
],
})
expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + '',
expect((root as any).children[0].codegenNode.patchFlag).toBe(
PatchFlags.DYNAMIC_SLOTS,
)
expect(generate(root).code).toMatchSnapshot()
})
@ -630,8 +628,8 @@ describe('compiler: transform component slots', () => {
},
],
})
expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + '',
expect((root as any).children[0].codegenNode.patchFlag).toBe(
PatchFlags.DYNAMIC_SLOTS,
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
@ -693,8 +691,8 @@ describe('compiler: transform component slots', () => {
},
],
})
expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + '',
expect((root as any).children[0].codegenNode.patchFlag).toBe(
PatchFlags.DYNAMIC_SLOTS,
)
expect((root as any).children[0].children.length).toBe(3)
expect(generate(root).code).toMatchSnapshot()
@ -744,8 +742,8 @@ describe('compiler: transform component slots', () => {
},
],
})
expect((root as any).children[0].codegenNode.patchFlag).toMatch(
PatchFlags.DYNAMIC_SLOTS + '',
expect((root as any).children[0].codegenNode.patchFlag).toBe(
PatchFlags.DYNAMIC_SLOTS,
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
"version": "3.5.0-alpha.2",
"version": "3.5.0-alpha.3",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
@ -46,13 +46,13 @@
},
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-core#readme",
"dependencies": {
"@babel/parser": "^7.24.7",
"@babel/parser": "catalog:",
"@vue/shared": "workspace:*",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
"estree-walker": "catalog:",
"source-map-js": "catalog:"
},
"devDependencies": {
"@babel/types": "^7.24.7"
"@babel/types": "catalog:"
}
}

View File

@ -1,4 +1,4 @@
import { isString } from '@vue/shared'
import { type PatchFlags, isString } from '@vue/shared'
import {
CREATE_BLOCK,
CREATE_ELEMENT_BLOCK,
@ -339,7 +339,7 @@ export interface VNodeCall extends Node {
| SimpleExpressionNode // hoisted
| CacheExpression // cached
| undefined
patchFlag: string | undefined
patchFlag: PatchFlags | undefined
dynamicProps: string | SimpleExpressionNode | undefined
directives: DirectiveArguments | undefined
isBlock: boolean
@ -570,7 +570,7 @@ export interface ForCodegenNode extends VNodeCall {
tag: typeof FRAGMENT
props: undefined
children: ForRenderListExpression
patchFlag: string
patchFlag: PatchFlags
disableTracking: boolean
}

View File

@ -17,7 +17,7 @@ export function walkIdentifiers(
root: Node,
onIdentifier: (
node: Identifier,
parent: Node,
parent: Node | null,
parentStack: Node[],
isReference: boolean,
isLocal: boolean,
@ -36,7 +36,7 @@ export function walkIdentifiers(
: root
walk(root, {
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | null) {
parent && parentStack.push(parent)
if (
parent &&
@ -47,9 +47,9 @@ export function walkIdentifiers(
}
if (node.type === 'Identifier') {
const isLocal = !!knownIds[node.name]
const isRefed = isReferencedIdentifier(node, parent!, parentStack)
const isRefed = isReferencedIdentifier(node, parent, parentStack)
if (includeAll || (isRefed && !isLocal)) {
onIdentifier(node, parent!, parentStack, isRefed, isLocal)
onIdentifier(node, parent, parentStack, isRefed, isLocal)
}
} else if (
node.type === 'ObjectProperty' &&
@ -79,7 +79,7 @@ export function walkIdentifiers(
}
}
},
leave(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {
leave(node: Node & { scopeIds?: Set<string> }, parent: Node | null) {
parent && parentStack.pop()
if (node !== rootExp && node.scopeIds) {
for (const id of node.scopeIds) {

View File

@ -35,7 +35,13 @@ import {
isSimpleIdentifier,
toValidAssetId,
} from './utils'
import { isArray, isString, isSymbol } from '@vue/shared'
import {
PatchFlagNames,
type PatchFlags,
isArray,
isString,
isSymbol,
} from '@vue/shared'
import {
CREATE_COMMENT,
CREATE_ELEMENT_VNODE,
@ -833,6 +839,28 @@ function genVNodeCall(node: VNodeCall, context: CodegenContext) {
disableTracking,
isComponent,
} = node
// add dev annotations to patch flags
let patchFlagString
if (patchFlag) {
if (__DEV__) {
if (patchFlag < 0) {
// special flags (negative and mutually exclusive)
patchFlagString = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */`
} else {
// bitwise flags
const flagNames = Object.keys(PatchFlagNames)
.map(Number)
.filter(n => n > 0 && patchFlag & n)
.map(n => PatchFlagNames[n as PatchFlags])
.join(`, `)
patchFlagString = patchFlag + ` /* ${flagNames} */`
}
} else {
patchFlagString = String(patchFlag)
}
}
if (directives) {
push(helper(WITH_DIRECTIVES) + `(`)
}
@ -847,7 +875,7 @@ function genVNodeCall(node: VNodeCall, context: CodegenContext) {
: getVNodeHelper(context.inSSR, isComponent)
push(helper(callHelper) + `(`, NewlineType.None, node)
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
genNullableArgs([tag, props, children, patchFlagString, dynamicProps]),
context,
)
push(`)`)
@ -1007,11 +1035,12 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
newline()
push(`(`)
}
push(`_cache[${node.index}] = `)
genNode(node.value, context)
if (needPauseTracking) {
push(`,`)
push(`).cacheIndex = ${node.index},`)
newline()
push(`${helper(SET_BLOCK_TRACKING)}(1),`)
newline()

View File

@ -388,7 +388,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
helper(FRAGMENT),
undefined,
root.children,
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
patchFlag,
undefined,
undefined,
true,

View File

@ -75,8 +75,7 @@ function walk(
: getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_CACHE) {
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.CACHED + (__DEV__ ? ` /* CACHED */` : ``)
;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.CACHED
toCache.push(child)
continue
}
@ -85,9 +84,9 @@ function walk(
// hoisting.
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
const flag = getPatchFlag(codegenNode)
const flag = codegenNode.patchFlag
if (
(!flag ||
(flag === undefined ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >=
@ -259,8 +258,7 @@ export function getConstantType(
) {
return ConstantTypes.NOT_CONSTANT
}
const flag = getPatchFlag(codegenNode)
if (!flag) {
if (codegenNode.patchFlag === undefined) {
let returnType = ConstantTypes.CAN_STRINGIFY
// Element itself has no patch flag. However we still need to check:
@ -447,8 +445,3 @@ function getNodeProps(node: PlainElementNode) {
return codegenNode.props
}
}
function getPatchFlag(node: VNodeCall): number | undefined {
const flag = node.patchFlag
return flag ? parseInt(flag, 10) : undefined
}

View File

@ -23,7 +23,6 @@ import {
createVNodeCall,
} from '../ast'
import {
PatchFlagNames,
PatchFlags,
camelize,
capitalize,
@ -101,8 +100,7 @@ export const transformElement: NodeTransform = (node, context) => {
let vnodeProps: VNodeCall['props']
let vnodeChildren: VNodeCall['children']
let vnodePatchFlag: VNodeCall['patchFlag']
let patchFlag: number = 0
let patchFlag: VNodeCall['patchFlag'] | 0 = 0
let vnodeDynamicProps: VNodeCall['dynamicProps']
let dynamicPropNames: string[] | undefined
let vnodeDirectives: VNodeCall['directives']
@ -206,27 +204,8 @@ export const transformElement: NodeTransform = (node, context) => {
}
// patchFlag & dynamicPropNames
if (patchFlag !== 0) {
if (__DEV__) {
if (patchFlag < 0) {
// special flags (negative and mutually exclusive)
vnodePatchFlag =
patchFlag + ` /* ${PatchFlagNames[patchFlag as PatchFlags]} */`
} else {
// bitwise flags
const flagNames = Object.keys(PatchFlagNames)
.map(Number)
.filter(n => n > 0 && patchFlag & n)
.map(n => PatchFlagNames[n as PatchFlags])
.join(`, `)
vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
}
} else {
vnodePatchFlag = String(patchFlag)
}
if (dynamicPropNames && dynamicPropNames.length) {
vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
}
if (dynamicPropNames && dynamicPropNames.length) {
vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
}
node.codegenNode = createVNodeCall(
@ -234,7 +213,7 @@ export const transformElement: NodeTransform = (node, context) => {
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
patchFlag === 0 ? undefined : patchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,

View File

@ -118,7 +118,11 @@ export function processExpression(
}
const { inline, bindingMetadata } = context
const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
const rewriteIdentifier = (
raw: string,
parent?: Node | null,
id?: Identifier,
) => {
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
if (inline) {
// x = y
@ -315,9 +319,10 @@ export function processExpression(
// local scope variable (a v-for alias, or a v-slot prop)
if (
!(needPrefix && isLocal) &&
parent.type !== 'CallExpression' &&
parent.type !== 'NewExpression' &&
parent.type !== 'MemberExpression'
(!parent ||
(parent.type !== 'CallExpression' &&
parent.type !== 'NewExpression' &&
parent.type !== 'MemberExpression'))
) {
;(node as QualifiedId).isConstant = true
}

View File

@ -46,7 +46,7 @@ import {
} from '../runtimeHelpers'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { PatchFlagNames, PatchFlags } from '@vue/shared'
import { PatchFlags } from '@vue/shared'
import { transformBindShorthand } from './vBind'
export const transformFor = createStructuralDirectiveTransform(
@ -109,8 +109,7 @@ export const transformFor = createStructuralDirectiveTransform(
helper(FRAGMENT),
undefined,
renderExp,
fragmentFlag +
(__DEV__ ? ` /* ${PatchFlagNames[fragmentFlag]} */` : ``),
fragmentFlag,
undefined,
undefined,
true /* isBlock */,
@ -169,10 +168,7 @@ export const transformFor = createStructuralDirectiveTransform(
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : undefined,
node.children,
PatchFlags.STABLE_FRAGMENT +
(__DEV__
? ` /* ${PatchFlagNames[PatchFlags.STABLE_FRAGMENT]} */`
: ``),
PatchFlags.STABLE_FRAGMENT,
undefined,
undefined,
true,

View File

@ -280,7 +280,7 @@ function createChildrenCodegenNode(
helper(FRAGMENT),
createObjectExpression([keyProperty]),
children,
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
patchFlag,
undefined,
undefined,
true,

View File

@ -40,5 +40,7 @@ describe('decodeHtmlBrowser', () => {
true,
),
).toBe('<strong><strong>&</strong></strong>')
expect(decodeHtmlBrowser('"', true)).toBe('"')
expect(decodeHtmlBrowser("'", true)).toBe("'")
})
})

View File

@ -31,7 +31,7 @@ return function render(_ctx, _cache) {
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 })
], -1 /* CACHED */)
], -1 /* HOISTED */)
])))
}"
`;
@ -48,7 +48,7 @@ return function render(_ctx, _cache) {
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("img", { src: _imports_0_ })
], -1 /* CACHED */)
], -1 /* HOISTED */)
])))
}"
`;

View File

@ -6,10 +6,7 @@ import {
} from '@vue/compiler-core'
import { transformVHtml } from '../../src/transforms/vHtml'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import {
createObjectMatcher,
genFlagText,
} from '../../../compiler-core/__tests__/testUtils'
import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
import { DOMErrorCodes } from '../../src/errors'
@ -34,7 +31,7 @@ describe('compiler: v-html transform', () => {
innerHTML: `[test]`,
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.PROPS),
patchFlag: PatchFlags.PROPS,
dynamicProps: `["innerHTML"]`,
})
})
@ -53,7 +50,7 @@ describe('compiler: v-html transform', () => {
innerHTML: `[test]`,
}),
children: undefined, // <-- children should have been removed
patchFlag: genFlagText(PatchFlags.PROPS),
patchFlag: PatchFlags.PROPS,
dynamicProps: `["innerHTML"]`,
})
})

View File

@ -14,7 +14,6 @@ import { transformOn } from '../../src/transforms/vOn'
import { V_ON_WITH_KEYS, V_ON_WITH_MODIFIERS } from '../../src/runtimeHelpers'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression'
import { genFlagText } from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
function parseWithVOn(template: string, options: CompilerOptions = {}) {
@ -272,7 +271,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.NEED_HYDRATION),
PatchFlags.NEED_HYDRATION,
)
expect(prop).toMatchObject({
key: {
@ -300,6 +299,6 @@ describe('compiler-dom: transform v-on', () => {
},
})
// should only have hydration flag
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_HYDRATION))
expect(node.patchFlag).toBe(PatchFlags.NEED_HYDRATION)
})
})

View File

@ -6,10 +6,7 @@ import {
} from '@vue/compiler-core'
import { transformVText } from '../../src/transforms/vText'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import {
createObjectMatcher,
genFlagText,
} from '../../../compiler-core/__tests__/testUtils'
import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
import { DOMErrorCodes } from '../../src/errors'
@ -36,7 +33,7 @@ describe('compiler: v-text transform', () => {
},
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.PROPS),
patchFlag: PatchFlags.PROPS,
dynamicProps: `["textContent"]`,
})
})
@ -57,7 +54,7 @@ describe('compiler: v-text transform', () => {
},
}),
children: undefined, // <-- children should have been removed
patchFlag: genFlagText(PatchFlags.PROPS),
patchFlag: PatchFlags.PROPS,
dynamicProps: `["textContent"]`,
})
})

View File

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

View File

@ -861,7 +861,7 @@ export default {
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", null, _toDisplayString(count.value), 1 /* TEXT */),
_cache[0] || (_cache[0] = _createElementVNode("div", null, "static", -1 /* CACHED */))
_cache[0] || (_cache[0] = _createElementVNode("div", null, "static", -1 /* HOISTED */))
], 64 /* STABLE_FRAGMENT */))
}
}

View File

@ -41,8 +41,8 @@ const _hoisted_1 = _imports_0 + '#fragment'
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_cache[0] || (_cache[0] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* CACHED */))
_cache[0] || (_cache[0] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */)),
_cache[1] || (_cache[1] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;

View File

@ -10,8 +10,8 @@ const _hoisted_2 = _imports_0 + ' 1x, ' + "/foo/logo.png" + ' 2x'
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_cache[0] || (_cache[0] = _createElementVNode("img", { srcset: _hoisted_1 }, null, -1 /* CACHED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", { srcset: _hoisted_2 }, null, -1 /* CACHED */))
_cache[0] || (_cache[0] = _createElementVNode("img", { srcset: _hoisted_1 }, null, -1 /* HOISTED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", { srcset: _hoisted_2 }, null, -1 /* HOISTED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -35,51 +35,51 @@ export function render(_ctx, _cache) {
_cache[0] || (_cache[0] = _createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[2] || (_cache[2] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* CACHED */))
}, null, -1 /* HOISTED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -92,51 +92,51 @@ export function render(_ctx, _cache) {
_cache[0] || (_cache[0] = _createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[2] || (_cache[2] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /foo/logo.png 2x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* CACHED */))
}, null, -1 /* HOISTED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -162,51 +162,51 @@ export function render(_ctx, _cache) {
_cache[0] || (_cache[0] = _createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[1] || (_cache[1] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[2] || (_cache[2] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_9
}, null, -1 /* CACHED */)),
}, null, -1 /* HOISTED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* CACHED */))
}, null, -1 /* HOISTED */))
], 64 /* STABLE_FRAGMENT */))
}"
`;

View File

@ -87,7 +87,7 @@ export default /*#__PURE__*/_defineComponent({
const { foo } = __props
return { }
return { foo }
}
})"

View File

@ -591,7 +591,7 @@ const props = defineProps({ foo: String })
// #8289
test('destructure without enabling reactive destructure', () => {
const { content } = compile(
const { content, bindings } = compile(
`<script setup lang="ts">
const { foo } = defineProps<{
foo: Foo
@ -602,6 +602,10 @@ const props = defineProps({ foo: String })
},
)
expect(content).toMatch(`const { foo } = __props`)
expect(content).toMatch(`return { foo }`)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_CONST,
})
assertCode(content)
})

View File

@ -635,6 +635,26 @@ describe('resolveType', () => {
})
})
// #11266
test('correctly parse type annotation for declared function', () => {
const { props } = resolve(`
import { ExtractPropTypes } from 'vue'
interface UploadFile<T = any> {
xhr?: T
}
declare function uploadProps<T = any>(): {
fileList: {
type: PropType<UploadFile<T>[]>
default: UploadFile<T>[]
}
}
type UploadProps = ExtractPropTypes<ReturnType<typeof uploadProps>>
defineProps<UploadProps>()`)
expect(props).toStrictEqual({
fileList: ['Array'],
})
})
describe('generics', () => {
test('generic with type literal', () => {
expect(

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.5.0-alpha.2",
"version": "3.5.0-alpha.3",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@ -42,27 +42,27 @@
},
"homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-sfc#readme",
"dependencies": {
"@babel/parser": "^7.24.7",
"@babel/parser": "catalog:",
"@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*",
"@vue/compiler-vapor": "workspace:*",
"@vue/shared": "workspace:*",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.10",
"postcss": "^8.4.38",
"source-map-js": "^1.2.0"
"estree-walker": "catalog:",
"magic-string": "catalog:",
"postcss": "^8.4.39",
"source-map-js": "catalog:"
},
"devDependencies": {
"@babel/types": "^7.24.7",
"@babel/types": "catalog:",
"@vue/consolidate": "^1.0.0",
"hash-sum": "^2.0.0",
"lru-cache": "10.1.0",
"merge-source-map": "^1.1.0",
"minimatch": "^9.0.4",
"minimatch": "^9.0.5",
"postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.0",
"pug": "^3.0.3",
"sass": "^1.77.5"
"sass": "^1.77.8"
}
}

View File

@ -607,6 +607,7 @@ export function compileScript(
setupBindings,
vueImportAliases,
hoistStatic,
!!ctx.propsDestructureDecl,
)
}
@ -623,7 +624,7 @@ export function compileScript(
) {
const scope: Statement[][] = [scriptSetupAst.body]
walk(node, {
enter(child: Node, parent: Node | undefined) {
enter(child: Node, parent: Node | null) {
if (isFunctionType(child)) {
this.skip()
}
@ -1071,6 +1072,7 @@ function walkDeclaration(
bindings: Record<string, BindingTypes>,
userImportAliases: Record<string, string>,
hoistStatic: boolean,
isPropsDestructureEnabled = false,
): boolean {
let isAllLiteral = false
@ -1139,7 +1141,7 @@ function walkDeclaration(
}
registerBinding(bindings, id, bindingType)
} else {
if (isCallOf(init, DEFINE_PROPS)) {
if (isCallOf(init, DEFINE_PROPS) && isPropsDestructureEnabled) {
continue
}
if (id.type === 'ObjectPattern') {

View File

@ -233,7 +233,7 @@ export function transformDestructuredProps(
const ast = ctx.scriptSetupAst!
walkScope(ast, true)
walk(ast, {
enter(node: Node, parent?: Node) {
enter(node: Node, parent: Node | null) {
parent && parentStack.push(parent)
// skip type nodes
@ -288,7 +288,7 @@ export function transformDestructuredProps(
}
}
},
leave(node: Node, parent?: Node) {
leave(node: Node, parent: Node | null) {
parent && parentStack.pop()
if (
(node.type === 'BlockStatement' && !isFunctionType(parent!)) ||

View File

@ -177,6 +177,7 @@ function innerResolveTypeElements(
case 'TSInterfaceDeclaration':
return resolveInterfaceMembers(ctx, node, scope, typeParameters)
case 'TSTypeAliasDeclaration':
case 'TSTypeAnnotation':
case 'TSParenthesizedType':
return resolveTypeElements(
ctx,

View File

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

View File

@ -17,6 +17,7 @@ import {
toRefs,
toValue,
unref,
useTemplateRef,
} from 'vue'
import { type IsAny, type IsUnion, describe, expectType } from './utils'
@ -452,3 +453,14 @@ describe('toRef <-> toValue', () => {
),
)
})
// unref
declare const text: ShallowRef<string> | ComputedRef<string> | MaybeRef<string>
expectType<string>(unref(text))
// useTemplateRef
const tRef = useTemplateRef('foo')
expectType<Readonly<ShallowRef<unknown>>>(tRef)
const tRef2 = useTemplateRef<HTMLElement>('bar')
expectType<Readonly<ShallowRef<HTMLElement | null>>>(tRef2)

13
packages/global.d.ts vendored
View File

@ -19,15 +19,6 @@ declare var __FEATURE_PROD_DEVTOOLS__: boolean
declare var __FEATURE_SUSPENSE__: boolean
declare var __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: boolean
// for tests
declare namespace jest {
interface Matchers<R, T> {
toHaveBeenWarned(): R
toHaveBeenWarnedLast(): R
toHaveBeenWarnedTimes(n: number): R
}
}
declare module '*.vue' {}
declare module 'file-saver' {
@ -38,8 +29,8 @@ declare module 'estree-walker' {
export function walk<T>(
root: T,
options: {
enter?: (node: T, parent: T | undefined) => any
leave?: (node: T, parent: T | undefined) => any
enter?: (node: T, parent: T | null) => any
leave?: (node: T, parent: T | null) => any
exit?: (node: T) => any
} & ThisType<{ skip: () => void }>,
)

View File

@ -904,6 +904,22 @@ describe('reactivity/computed', () => {
expect(serializeInner(root)).toBe('11')
})
it('should be recomputed without being affected by side effects', () => {
const v = ref(0)
const c1 = computed(() => {
v.value = 1
return 0
})
const c2 = computed(() => {
return v.value + ',' + c1.value
})
expect(c2.value).toBe('0,0')
v.value = 1
expect(c2.value).toBe('1,0')
// expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
it('debug: onTrigger (ref)', () => {
let events: DebuggerEvent[] = []
const onTrigger = vi.fn((e: DebuggerEvent) => {

View File

@ -2,6 +2,8 @@ import { isRef, ref } from '../src/ref'
import {
isProxy,
isReactive,
isReadonly,
isShallow,
markRaw,
reactive,
readonly,
@ -359,4 +361,25 @@ describe('reactivity/reactive', () => {
const c = computed(() => {})
expect(isProxy(c)).toBe(false)
})
test('The results of the shallow and readonly assignments are the same (Map)', () => {
const map = reactive(new Map())
map.set('foo', shallowReactive({ a: 2 }))
expect(isShallow(map.get('foo'))).toBe(true)
map.set('bar', readonly({ b: 2 }))
expect(isReadonly(map.get('bar'))).toBe(true)
})
test('The results of the shallow and readonly assignments are the same (Set)', () => {
const set = reactive(new Set())
set.add(shallowReactive({ a: 2 }))
set.add(readonly({ b: 2 }))
let count = 0
for (const i of set) {
if (count === 0) expect(isShallow(i)).toBe(true)
else expect(isReadonly(i)).toBe(true)
count++
}
})
})

View File

@ -123,6 +123,29 @@ describe('shallowReactive', () => {
shallowSet.forEach(x => expect(isReactive(x)).toBe(false))
})
test('Setting a reactive object on a shallowReactive map', () => {
const msg = ref('ads')
const bar = reactive({ msg })
const foo = shallowReactive(new Map([['foo1', bar]]))
foo.set('foo2', bar)
expect(isReactive(foo.get('foo2'))).toBe(true)
expect(isReactive(foo.get('foo1'))).toBe(true)
})
test('Setting a reactive object on a shallowReactive set', () => {
const msg = ref(1)
const bar = reactive({ msg })
const foo = reactive({ msg })
const deps = shallowReactive(new Set([bar]))
deps.add(foo)
deps.forEach(dep => {
expect(isReactive(dep)).toBe(true)
})
})
// #1210
test('onTrack on called on objectSpread', () => {
const onTrackFn = vi.fn()

View File

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

View File

@ -198,7 +198,7 @@ export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
// instrument iterators to take ARRAY_ITERATE dependency
function iterator(
self: unknown[],
method: keyof Array<any>,
method: keyof Array<unknown>,
wrapValue: (value: any) => unknown,
) {
// note that taking ARRAY_ITERATE dependency here is not strictly equivalent
@ -210,11 +210,13 @@ function iterator(
// given that JS iterator can only be read once, this doesn't seem like
// a plausible use-case, so this tracking simplification seems ok.
const arr = shallowReadArray(self)
const iter = (arr[method] as any)()
const iter = (arr[method] as any)() as IterableIterator<unknown> & {
_next: IterableIterator<unknown>['next']
}
if (arr !== self && !isShallow(self)) {
;(iter as any)._next = iter.next
iter._next = iter.next
iter.next = () => {
const result = (iter as any)._next()
const result = iter._next()
if (result.value) {
result.value = wrapValue(result.value)
}

View File

@ -34,7 +34,7 @@ const builtInSymbols = new Set(
// but accessing them on Symbol leads to TypeError because Symbol is a strict mode
// function
.filter(key => key !== 'arguments' && key !== 'caller')
.map(key => (Symbol as any)[key])
.map(key => Symbol[key as keyof SymbolConstructor])
.filter(isSymbol),
)
@ -137,12 +137,12 @@ class MutableReactiveHandler extends BaseReactiveHandler {
}
set(
target: object,
target: Record<string | symbol, unknown>,
key: string | symbol,
value: unknown,
receiver: object,
): boolean {
let oldValue = (target as any)[key]
let oldValue = target[key]
if (!this._isShallow) {
const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
@ -177,9 +177,12 @@ class MutableReactiveHandler extends BaseReactiveHandler {
return result
}
deleteProperty(target: object, key: string | symbol): boolean {
deleteProperty(
target: Record<string | symbol, unknown>,
key: string | symbol,
): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const oldValue = target[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
@ -187,14 +190,15 @@ class MutableReactiveHandler extends BaseReactiveHandler {
return result
}
has(target: object, key: string | symbol): boolean {
has(target: Record<string | symbol, unknown>, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
ownKeys(target: object): (string | symbol)[] {
ownKeys(target: Record<string | symbol, unknown>): (string | symbol)[] {
track(
target,
TrackOpTypes.ITERATE,

View File

@ -1,4 +1,11 @@
import { toRaw, toReactive, toReadonly } from './reactive'
import {
type Target,
isReadonly,
isShallow,
toRaw,
toReactive,
toReadonly,
} from './reactive'
import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared'
@ -6,10 +13,10 @@ import { warn } from './warning'
type CollectionTypes = IterableCollections | WeakCollections
type IterableCollections = Map<any, any> | Set<any>
type WeakCollections = WeakMap<any, any> | WeakSet<any>
type MapTypes = Map<any, any> | WeakMap<any, any>
type SetTypes = Set<any> | WeakSet<any>
type IterableCollections = (Map<any, any> | Set<any>) & Target
type WeakCollections = (WeakMap<any, any> | WeakSet<any>) & Target
type MapTypes = (Map<any, any> | WeakMap<any, any>) & Target
type SetTypes = (Set<any> | WeakSet<any>) & Target
const toShallow = <T extends unknown>(value: T): T => value
@ -24,7 +31,7 @@ function get(
) {
// #1772: readonly(reactive(Map)) should return readonly + reactive version
// of the value
target = (target as any)[ReactiveFlags.RAW]
target = target[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
@ -47,7 +54,7 @@ function get(
}
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = (this as any)[ReactiveFlags.RAW]
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
@ -62,13 +69,15 @@ function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
}
function size(target: IterableCollections, isReadonly = false) {
target = (target as any)[ReactiveFlags.RAW]
target = target[ReactiveFlags.RAW]
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
function add(this: SetTypes, value: unknown) {
value = toRaw(value)
function add(this: SetTypes, value: unknown, _isShallow = false) {
if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
@ -79,8 +88,10 @@ function add(this: SetTypes, value: unknown) {
return this
}
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
function set(this: MapTypes, key: unknown, value: unknown, _isShallow = false) {
if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const { has, get } = getProto(target)
@ -144,7 +155,7 @@ function createForEach(isReadonly: boolean, isShallow: boolean) {
callback: Function,
thisArg?: unknown,
) {
const observed = this as any
const observed = this
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
@ -180,7 +191,7 @@ function createIterableMethod(
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
const target = (this as any)[ReactiveFlags.RAW]
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)
const isPair =
@ -258,8 +269,12 @@ function createInstrumentations() {
return size(this as unknown as IterableCollections)
},
has,
add,
set,
add(this: SetTypes, value: unknown) {
return add.call(this, value, true)
},
set(this: MapTypes, key: unknown, value: unknown) {
return set.call(this, key, value, true)
},
delete: deleteEntry,
clear,
forEach: createForEach(false, true),

View File

@ -210,7 +210,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> | ComputedRef<T>): T {
export function unref<T>(ref: MaybeRef<T> | ComputedRef<T> | ShallowRef<T>): T {
return isRef(ref) ? ref.value : ref
}
@ -230,7 +230,9 @@ export function unref<T>(ref: MaybeRef<T> | ComputedRef<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> | ComputedRef<T>): T {
export function toValue<T>(
source: MaybeRefOrGetter<T> | ComputedRef<T> | ShallowRef<T>,
): T {
return isFunction(source) ? source() : unref(source)
}
@ -248,11 +250,9 @@ const shallowUnwrapHandlers: ProxyHandler<any> = {
}
/**
* Returns a reactive proxy for the given object.
*
* If the object already is reactive, it's returned as-is. If not, a new
* reactive proxy is created. Direct child properties that are refs are properly
* handled, as well.
* Returns a proxy for the given object that shallowly unwraps properties that
* are refs. If the object already is reactive, it's returned as-is. If not, a
* new reactive proxy is created.
*
* @param objectWithRefs - Either an already-reactive object or a simple object
* that contains refs.

View File

@ -538,6 +538,23 @@ describe('api: createApp', () => {
expect(serializeInner(root)).toBe('hello')
})
test('config.throwUnhandledErrorInProduction', () => {
__DEV__ = false
try {
const err = new Error()
const app = createApp({
setup() {
throw err
},
})
app.config.throwUnhandledErrorInProduction = true
const root = nodeOps.createElement('div')
expect(() => app.mount(root)).toThrow(err)
} finally {
__DEV__ = true
}
})
test('return property "_" should not overwrite "ctx._", __isScriptSetup: false', () => {
const Comp = defineComponent({
setup() {

View File

@ -5,6 +5,7 @@ import {
defineComponent,
getCurrentInstance,
nextTick,
onErrorCaptured,
onWatcherCleanup,
reactive,
ref,
@ -1649,4 +1650,60 @@ describe('api: watch', () => {
expect(spy).toHaveBeenCalledTimes(1)
expect(foo.value.a).toBe(2)
})
test('watch immediate error in effect scope should be catched by onErrorCaptured', async () => {
const warn = vi.spyOn(console, 'warn')
warn.mockImplementation(() => {})
const ERROR_IN_SCOPE = 'ERROR_IN_SCOPE'
const ERROR_OUT_SCOPE = 'ERROR_OUT_SCOPE'
const errors = ref<string[]>([])
const Comp = {
setup() {
const trigger = ref(0)
effectScope(true).run(() => {
watch(
trigger,
() => {
throw new Error(ERROR_IN_SCOPE)
},
{ immediate: true },
)
})
watchEffect(() => {
throw new Error(ERROR_OUT_SCOPE)
})
return () => ''
},
}
const root = nodeOps.createElement('div')
render(
h(
{
setup(_, { slots }) {
onErrorCaptured(e => {
errors.value.push(e.message)
return false
})
return () => h('div', slots.default && slots.default())
},
},
null,
() => [h(Comp)],
),
root,
)
await nextTick()
// only watchEffect as ran so far
expect(errors.value).toHaveLength(2)
expect(errors.value[0]).toBe(ERROR_IN_SCOPE)
expect(errors.value[1]).toBe(ERROR_OUT_SCOPE)
warn.mockRestore()
})
})

View File

@ -356,6 +356,83 @@ describe('component: emit', () => {
expect(fn2).toHaveBeenCalledWith('two')
})
test('.trim modifier should work with v-model on component for kebab-cased props and camelCased emit', () => {
const Foo = defineComponent({
render() {},
created() {
this.$emit('update:firstName', ' one ')
},
})
const fn1 = vi.fn()
const Comp = () =>
h(Foo, {
'first-name': null,
'first-nameModifiers': { trim: true },
'onUpdate:first-name': fn1,
})
render(h(Comp), nodeOps.createElement('div'))
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn1).toHaveBeenCalledWith('one')
})
test('.trim modifier should work with v-model on component for camelCased props and kebab-cased emit', () => {
const Foo = defineComponent({
render() {},
created() {
this.$emit('update:model-value', ' one ')
this.$emit('update:first-name', ' two ')
},
})
const fn1 = vi.fn()
const fn2 = vi.fn()
const Comp = () =>
h(Foo, {
modelValue: null,
modelModifiers: { trim: true },
'onUpdate:modelValue': fn1,
firstName: null,
firstNameModifiers: { trim: true },
'onUpdate:firstName': fn2,
})
render(h(Comp), nodeOps.createElement('div'))
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn1).toHaveBeenCalledWith('one')
expect(fn2).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledWith('two')
})
test('.trim modifier should work with v-model on component for mixed cased props and emit', () => {
const Foo = defineComponent({
render() {},
created() {
this.$emit('update:base-URL', ' one ')
},
})
const fn1 = vi.fn()
const Comp = () =>
h(Foo, {
'base-URL': null,
'base-URLModifiers': { trim: true },
'onUpdate:base-URL': fn1,
})
render(h(Comp), nodeOps.createElement('div'))
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn1).toHaveBeenCalledWith('one')
})
test('.trim and .number modifiers should work with v-model on component', () => {
const Foo = defineComponent({
render() {},

View File

@ -538,6 +538,96 @@ describe('component props', () => {
expect(renderProxy.$props).toMatchObject(props)
})
test('merging props from global mixins and extends', () => {
let renderProxy: any
let extendedRenderProxy: any
const defaultProp = ' from global'
const props = {
globalProp: {
type: String,
default: defaultProp,
},
}
const globalMixin = {
props,
}
const Comp = {
render(this: any) {
renderProxy = this
return h('div', ['Comp', this.globalProp])
},
}
const ExtendedComp = {
extends: Comp,
render(this: any) {
extendedRenderProxy = this
return h('div', ['ExtendedComp', this.globalProp])
},
}
const app = createApp(
{
render: () => [h(ExtendedComp), h(Comp)],
},
{},
)
app.mixin(globalMixin)
const root = nodeOps.createElement('div')
app.mount(root)
expect(serializeInner(root)).toMatch(
`<div>ExtendedComp from global</div><div>Comp from global</div>`,
)
expect(renderProxy.$props).toMatchObject({ globalProp: defaultProp })
expect(extendedRenderProxy.$props).toMatchObject({
globalProp: defaultProp,
})
})
test('merging props for a component that is also used as a mixin', () => {
const CompA = {
render(this: any) {
return this.foo
},
}
const mixin = {
props: {
foo: {
default: 'from mixin',
},
},
}
const CompB = {
mixins: [mixin, CompA],
render(this: any) {
return this.foo
},
}
const app = createApp({
render() {
return [h(CompA), ', ', h(CompB)]
},
})
app.mixin({
props: {
foo: {
default: 'from global mixin',
},
},
})
const root = nodeOps.createElement('div')
app.mount(root)
expect(serializeInner(root)).toMatch(`from global mixin, from mixin`)
})
test('props type support BigInt', () => {
const Comp = {
props: {

View File

@ -7,7 +7,7 @@ import {
ref,
render,
} from '@vue/runtime-test'
import { normalizeVNode } from '../src/vnode'
import { createBlock, normalizeVNode } from '../src/vnode'
import { createSlots } from '../src/helpers/createSlots'
describe('component: slots', () => {
@ -25,8 +25,21 @@ describe('component: slots', () => {
}
test('initSlots: instance.slots should be set correctly', () => {
let instance: any
const Comp = {
render() {
instance = getCurrentInstance()
return h('div')
},
}
const slots = { foo: () => {}, _: 1 }
render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
expect(instance.slots).toMatchObject(slots)
})
test('initSlots: instance.slots should remove compiler marker if parent is using manual render function', () => {
const { slots } = renderWithSlots({ _: 1 })
expect(slots).toMatchObject({ _: 1 })
expect(slots).toMatchObject({})
})
test('initSlots: should normalize object slots (when value is null, string, array)', () => {

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
import {
type VNode,
computed,
createApp,
defineComponent,
h,
@ -11,6 +13,7 @@ import {
watch,
watchEffect,
} from '@vue/runtime-test'
import { ErrorCodes, ErrorTypeStrings } from '../src/errorHandling'
describe('error handling', () => {
test('propagation', () => {
@ -609,5 +612,63 @@ describe('error handling', () => {
expect(handler).toHaveBeenCalledTimes(1)
})
test('errors in scheduler job with owner instance should be caught', async () => {
let vnode: VNode
const x = ref(0)
const app = createApp({
render() {
return (vnode = vnode || h('div', x.value))
},
})
app.config.errorHandler = vi.fn()
app.mount(nodeOps.createElement('div'))
const error = new Error('error')
Object.defineProperty(vnode!, 'el', {
get() {
throw error
},
})
x.value++
await nextTick()
expect(app.config.errorHandler).toHaveBeenCalledWith(
error,
{},
ErrorTypeStrings[ErrorCodes.COMPONENT_UPDATE],
)
})
// #11286
test('handle error in computed', async () => {
const err = new Error()
const handler = vi.fn()
const count = ref(1)
const x = computed(() => {
if (count.value === 2) throw err
return count.value + 1
})
const app = createApp({
setup() {
return () => x.value
},
})
app.config.errorHandler = handler
app.mount(nodeOps.createElement('div'))
count.value = 2
await nextTick()
expect(handler).toHaveBeenCalledWith(
err,
{},
ErrorTypeStrings[ErrorCodes.COMPONENT_UPDATE],
)
})
// native event handler handling should be tested in respective renderers
})

View File

@ -26,13 +26,17 @@ describe('renderSlot', () => {
const vnode = renderSlot(
{ default: () => [(child = h('child'))] },
'default',
{ key: 'foo' },
)
expect(vnode.children).toEqual([child])
expect(vnode.key).toBe('foo')
})
it('should render slot fallback', () => {
const vnode = renderSlot({}, 'default', {}, () => ['fallback'])
const vnode = renderSlot({}, 'default', { key: 'foo' }, () => ['fallback'])
expect(vnode.children).toEqual(['fallback'])
// should attach fallback key postfix
expect(vnode.key).toBe('foo_fb')
})
it('should warn render ssr slot', () => {

View File

@ -0,0 +1,242 @@
/**
* @vitest-environment jsdom
*/
import {
type App,
Suspense,
createApp,
defineAsyncComponent,
defineComponent,
h,
useId,
} from 'vue'
import { renderToString } from '@vue/server-renderer'
type TestCaseFactory = () => [App, Promise<any>[]]
async function runOnClient(factory: TestCaseFactory) {
const [app, deps] = factory()
const root = document.createElement('div')
app.mount(root)
await Promise.all(deps)
await promiseWithDelay(null, 0)
return root.innerHTML
}
async function runOnServer(factory: TestCaseFactory) {
const [app, _] = factory()
return (await renderToString(app))
.replace(/<!--[\[\]]-->/g, '') // remove fragment wrappers
.trim()
}
async function getOutput(factory: TestCaseFactory) {
const clientResult = await runOnClient(factory)
const serverResult = await runOnServer(factory)
expect(serverResult).toBe(clientResult)
return clientResult
}
function promiseWithDelay(res: any, delay: number) {
return new Promise<any>(r => {
setTimeout(() => r(res), delay)
})
}
const BasicComponentWithUseId = defineComponent({
setup() {
const id1 = useId()
const id2 = useId()
return () => [id1, ' ', id2]
},
})
describe('useId', () => {
test('basic', async () => {
expect(
await getOutput(() => {
const app = createApp(BasicComponentWithUseId)
return [app, []]
}),
).toBe('v:0 v:1')
})
test('with config.idPrefix', async () => {
expect(
await getOutput(() => {
const app = createApp(BasicComponentWithUseId)
app.config.idPrefix = 'foo'
return [app, []]
}),
).toBe('foo:0 foo:1')
})
test('async component', async () => {
const factory = (
delay1: number,
delay2: number,
): ReturnType<TestCaseFactory> => {
const p1 = promiseWithDelay(BasicComponentWithUseId, delay1)
const p2 = promiseWithDelay(BasicComponentWithUseId, delay2)
const AsyncOne = defineAsyncComponent(() => p1)
const AsyncTwo = defineAsyncComponent(() => p2)
const app = createApp({
setup() {
const id1 = useId()
const id2 = useId()
return () => [id1, ' ', id2, ' ', h(AsyncOne), ' ', h(AsyncTwo)]
},
})
return [app, [p1, p2]]
}
const expected =
'v:0 v:1 ' + // root
'v:0-0 v:0-1 ' + // inside first async subtree
'v:1-0 v:1-1' // inside second async subtree
// assert different async resolution order does not affect id stable-ness
expect(await getOutput(() => factory(10, 20))).toBe(expected)
expect(await getOutput(() => factory(20, 10))).toBe(expected)
})
test('serverPrefetch', async () => {
const factory = (
delay1: number,
delay2: number,
): ReturnType<TestCaseFactory> => {
const p1 = promiseWithDelay(null, delay1)
const p2 = promiseWithDelay(null, delay2)
const SPOne = defineComponent({
async serverPrefetch() {
await p1
},
render() {
return h(BasicComponentWithUseId)
},
})
const SPTwo = defineComponent({
async serverPrefetch() {
await p2
},
render() {
return h(BasicComponentWithUseId)
},
})
const app = createApp({
setup() {
const id1 = useId()
const id2 = useId()
return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)]
},
})
return [app, [p1, p2]]
}
const expected =
'v:0 v:1 ' + // root
'v:0-0 v:0-1 ' + // inside first async subtree
'v:1-0 v:1-1' // inside second async subtree
// assert different async resolution order does not affect id stable-ness
expect(await getOutput(() => factory(10, 20))).toBe(expected)
expect(await getOutput(() => factory(20, 10))).toBe(expected)
})
test('async setup()', async () => {
const factory = (
delay1: number,
delay2: number,
): ReturnType<TestCaseFactory> => {
const p1 = promiseWithDelay(null, delay1)
const p2 = promiseWithDelay(null, delay2)
const ASOne = defineComponent({
async setup() {
await p1
return {}
},
render() {
return h(BasicComponentWithUseId)
},
})
const ASTwo = defineComponent({
async setup() {
await p2
return {}
},
render() {
return h(BasicComponentWithUseId)
},
})
const app = createApp({
setup() {
const id1 = useId()
const id2 = useId()
return () =>
h(Suspense, null, {
default: h('div', [id1, ' ', id2, ' ', h(ASOne), ' ', h(ASTwo)]),
})
},
})
return [app, [p1, p2]]
}
const expected =
'<div>' +
'v:0 v:1 ' + // root
'v:0-0 v:0-1 ' + // inside first async subtree
'v:1-0 v:1-1' + // inside second async subtree
'</div>'
// assert different async resolution order does not affect id stable-ness
expect(await getOutput(() => factory(10, 20))).toBe(expected)
expect(await getOutput(() => factory(20, 10))).toBe(expected)
})
test('deep nested', async () => {
const factory = (): ReturnType<TestCaseFactory> => {
const p = Promise.resolve()
const One = {
async setup() {
const id = useId()
await p
return () => [id, ' ', h(Two), ' ', h(Three)]
},
}
const Two = {
async setup() {
const id = useId()
await p
return () => [id, ' ', h(Three), ' ', h(Three)]
},
}
const Three = {
async setup() {
const id = useId()
return () => id
},
}
const app = createApp({
setup() {
return () =>
h(Suspense, null, {
default: h(One),
})
},
})
return [app, [p]]
}
const expected =
'v:0 ' + // One
'v:0-0 ' + // Two
'v:0-0-0 v:0-0-1 ' + // Three + Three nested in Two
'v:0-1' // Three after Two
// assert different async resolution order does not affect id stable-ness
expect(await getOutput(() => factory())).toBe(expected)
expect(await getOutput(() => factory())).toBe(expected)
})
})

View File

@ -1,6 +1,7 @@
import {
Fragment,
type Ref,
type TestElement,
createApp,
createBlock,
createElementBlock,
@ -526,4 +527,129 @@ describe('useModel', () => {
await nextTick()
expect(msg.value).toBe('UGHH')
})
// #10279
test('force local update when setter formats value to the same value', async () => {
let childMsg: Ref<string>
let childModifiers: Record<string, true | undefined>
const compRender = vi.fn()
const parentRender = vi.fn()
const Comp = defineComponent({
props: ['msg', 'msgModifiers'],
emits: ['update:msg'],
setup(props) {
;[childMsg, childModifiers] = useModel(props, 'msg', {
set(val) {
if (childModifiers.number) {
return val.replace(/\D+/g, '')
}
},
})
return () => {
compRender()
return h('input', {
// simulate how v-model works
onVnodeBeforeMount(vnode) {
;(vnode.el as TestElement).props.value = childMsg.value
},
onVnodeBeforeUpdate(vnode) {
;(vnode.el as TestElement).props.value = childMsg.value
},
onInput(value: any) {
childMsg.value = value
},
})
}
},
})
const msg = ref(1)
const Parent = defineComponent({
setup() {
return () => {
parentRender()
return h(Comp, {
msg: msg.value,
msgModifiers: { number: true },
'onUpdate:msg': val => {
msg.value = val
},
})
}
},
})
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(parentRender).toHaveBeenCalledTimes(1)
expect(compRender).toHaveBeenCalledTimes(1)
expect(serializeInner(root)).toBe('<input value=1></input>')
const input = root.children[0] as TestElement
// simulate v-model update
input.props.onInput((input.props.value = '2'))
await nextTick()
expect(msg.value).toBe(2)
expect(parentRender).toHaveBeenCalledTimes(2)
expect(compRender).toHaveBeenCalledTimes(2)
expect(serializeInner(root)).toBe('<input value=2></input>')
input.props.onInput((input.props.value = '2a'))
await nextTick()
expect(msg.value).toBe(2)
expect(parentRender).toHaveBeenCalledTimes(2)
// should force local update
expect(compRender).toHaveBeenCalledTimes(3)
expect(serializeInner(root)).toBe('<input value=2></input>')
input.props.onInput((input.props.value = '2a'))
await nextTick()
expect(parentRender).toHaveBeenCalledTimes(2)
// should not force local update if set to the same value
expect(compRender).toHaveBeenCalledTimes(3)
})
test('set no change value', async () => {
let changeChildMsg: (() => void) | null = null
const compRender = vi.fn()
const Comp = defineComponent({
props: ['msg'],
emits: ['update:msg'],
setup(props) {
const childMsg = useModel(props, 'msg')
changeChildMsg = () => {
childMsg.value = childMsg.value
}
return () => {
return childMsg.value
}
},
})
const msg = ref('HI')
const Parent = defineComponent({
setup() {
return () =>
h(Comp, {
msg: msg.value,
'onUpdate:msg': val => {
msg.value = val
compRender()
},
})
},
})
const root = nodeOps.createElement('div')
render(h(Parent), root)
expect(compRender).toBeCalledTimes(0)
changeChildMsg!()
expect(compRender).toBeCalledTimes(0)
})
})

View File

@ -0,0 +1,71 @@
import {
h,
nextTick,
nodeOps,
ref,
render,
useTemplateRef,
} from '@vue/runtime-test'
describe('useTemplateRef', () => {
test('should work', () => {
let tRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
},
render() {
return h('div', { ref: key })
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(tRef!.value).toBe(root.children[0])
})
test('should be readonly', () => {
let tRef
const key = 'refKey'
const Comp = {
setup() {
tRef = useTemplateRef(key)
},
render() {
return h('div', { ref: key })
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
// @ts-expect-error
tRef.value = 123
expect(tRef!.value).toBe(root.children[0])
expect('target is readonly').toHaveBeenWarned()
})
test('should be updated for ref of dynamic strings', async () => {
let t1, t2
const key = ref('t1')
const Comp = {
setup() {
t1 = useTemplateRef<HTMLAnchorElement>('t1')
t2 = useTemplateRef('t2')
},
render() {
return h('div', { ref: key.value })
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(t1!.value).toBe(root.children[0])
expect(t2!.value).toBe(null)
key.value = 't2'
await nextTick()
expect(t2!.value).toBe(root.children[0])
expect(t1!.value).toBe(null)
})
})

View File

@ -29,6 +29,8 @@ function compileToFunction(template: string) {
return render
}
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
describe('hot module replacement', () => {
test('inject global runtime', () => {
expect(createRecord).toBeDefined()
@ -436,18 +438,23 @@ describe('hot module replacement', () => {
const Parent: ComponentOptions = {
setup() {
const com = ref()
const changeRef = (value: any) => {
com.value = value
}
const com1 = ref()
const changeRef1 = (value: any) => (com1.value = value)
return () => [h(Child, { ref: changeRef }), com.value?.count]
const com2 = ref()
const changeRef2 = (value: any) => (com2.value = value)
return () => [
h(Child, { ref: changeRef1 }),
h(Child, { ref: changeRef2 }),
com1.value?.count,
]
},
}
render(h(Parent), root)
await nextTick()
expect(serializeInner(root)).toBe(`<div>0</div>0`)
expect(serializeInner(root)).toBe(`<div>0</div><div>0</div>0`)
reload(childId, {
__hmrId: childId,
@ -458,9 +465,9 @@ describe('hot module replacement', () => {
render: compileToFunction(`<div @click="count++">{{ count }}</div>`),
})
await nextTick()
expect(serializeInner(root)).toBe(`<div>1</div>1`)
expect(unmountSpy).toHaveBeenCalledTimes(1)
expect(mountSpy).toHaveBeenCalledTimes(1)
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>1`)
expect(unmountSpy).toHaveBeenCalledTimes(2)
expect(mountSpy).toHaveBeenCalledTimes(2)
})
// #1156 - static nodes should retain DOM element reference across updates
@ -805,4 +812,43 @@ describe('hot module replacement', () => {
`<div><div>1<p>3</p></div></div><div><div>1<p>3</p></div></div><p>2</p>`,
)
})
// #11248
test('reload async component with multiple instances', async () => {
const root = nodeOps.createElement('div')
const childId = 'test-child-id'
const Child: ComponentOptions = {
__hmrId: childId,
data() {
return { count: 0 }
},
render: compileToFunction(`<div>{{ count }}</div>`),
}
const Comp = runtimeTest.defineAsyncComponent(() => Promise.resolve(Child))
const appId = 'test-app-id'
const App: ComponentOptions = {
__hmrId: appId,
render: () => [h(Comp), h(Comp)],
}
createRecord(appId, App)
render(h(App), root)
await timeout()
expect(serializeInner(root)).toBe(`<div>0</div><div>0</div>`)
// change count to 1
reload(childId, {
__hmrId: childId,
data() {
return { count: 1 }
},
render: compileToFunction(`<div>{{ count }}</div>`),
})
await timeout()
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
})
})

View File

@ -22,6 +22,7 @@ import {
nextTick,
onMounted,
openBlock,
reactive,
ref,
renderSlot,
useCssVars,
@ -31,7 +32,7 @@ import {
withDirectives,
} from '@vue/runtime-dom'
import { type SSRContext, renderToString } from '@vue/server-renderer'
import { PatchFlags } from '@vue/shared'
import { PatchFlags, normalizeStyle } from '@vue/shared'
import { vShowOriginalDisplay } from '../../runtime-dom/src/directives/vShow'
import { expect } from 'vitest'
@ -148,6 +149,15 @@ describe('SSR hydration', () => {
expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
})
// #7285
test('element with multiple continuous text vnodes', async () => {
// should no mismatch warning
const { container } = mountWithHydration('<div>fooo</div>', () =>
h('div', ['fo', createTextVNode('o'), 'o']),
)
expect(container.textContent).toBe('fooo')
})
test('element with elements children', async () => {
const msg = ref('foo')
const fn = vi.fn()
@ -239,6 +249,17 @@ describe('SSR hydration', () => {
)
})
// #7285
test('Fragment (multiple continuous text vnodes)', async () => {
// should no mismatch warning
const { container } = mountWithHydration('<!--[-->fooo<!--]-->', () => [
'fo',
createTextVNode('o'),
'o',
])
expect(container.textContent).toBe('fooo')
})
test('Teleport', async () => {
const msg = ref('foo')
const fn = vi.fn()
@ -1176,6 +1197,38 @@ describe('SSR hydration', () => {
expect(text.nodeType).toBe(3)
})
// #11372
test('object style value tracking in prod', async () => {
__DEV__ = false
try {
const style = reactive({ color: 'red' })
const Comp = {
render(this: any) {
return (
openBlock(),
createElementBlock(
'div',
{
style: normalizeStyle(style),
},
null,
4 /* STYLE */,
)
)
},
}
const { container } = mountWithHydration(
`<div style="color: red;"></div>`,
() => h(Comp),
)
style.color = 'green'
await nextTick()
expect(container.innerHTML).toBe(`<div style="color: green;"></div>`)
} finally {
__DEV__ = true
}
})
test('app.unmount()', async () => {
const container = document.createElement('DIV')
container.innerHTML = '<button></button>'
@ -1317,76 +1370,83 @@ describe('SSR hydration', () => {
// #10607
test('update component stable slot (prod + optimized mode)', async () => {
__DEV__ = false
const container = document.createElement('div')
container.innerHTML = `<template><div show="false"><!--[--><div><div><!----></div></div><div>0</div><!--]--></div></template>`
const Comp = {
render(this: any) {
return (
openBlock(),
createElementBlock('div', null, [renderSlot(this.$slots, 'default')])
)
},
}
const show = ref(false)
const clicked = ref(false)
const Wrapper = {
setup() {
const items = ref<number[]>([])
onMounted(() => {
items.value = [1]
})
return () => {
try {
const container = document.createElement('div')
container.innerHTML = `<template><div show="false"><!--[--><div><div><!----></div></div><div>0</div><!--]--></div></template>`
const Comp = {
render(this: any) {
return (
openBlock(),
createBlock(Comp, null, {
default: withCtx(() => [
createElementVNode('div', null, [
createElementVNode('div', null, [
clicked.value
? (openBlock(),
createElementBlock('div', { key: 0 }, 'foo'))
: createCommentVNode('v-if', true),
]),
]),
createElementVNode(
'div',
null,
items.value.length,
1 /* TEXT */,
),
]),
_: 1 /* STABLE */,
})
createElementBlock('div', null, [
renderSlot(this.$slots, 'default'),
])
)
}
},
}
createSSRApp({
components: { Wrapper },
data() {
return { show }
},
template: `<Wrapper :show="show"/>`,
}).mount(container)
},
}
const show = ref(false)
const clicked = ref(false)
await nextTick()
expect(container.innerHTML).toBe(
`<div show="false"><!--[--><div><div><!----></div></div><div>1</div><!--]--></div>`,
)
const Wrapper = {
setup() {
const items = ref<number[]>([])
onMounted(() => {
items.value = [1]
})
return () => {
return (
openBlock(),
createBlock(Comp, null, {
default: withCtx(() => [
createElementVNode('div', null, [
createElementVNode('div', null, [
clicked.value
? (openBlock(),
createElementBlock('div', { key: 0 }, 'foo'))
: createCommentVNode('v-if', true),
]),
]),
createElementVNode(
'div',
null,
items.value.length,
1 /* TEXT */,
),
]),
_: 1 /* STABLE */,
})
)
}
},
}
createSSRApp({
components: { Wrapper },
data() {
return { show }
},
template: `<Wrapper :show="show"/>`,
}).mount(container)
show.value = true
await nextTick()
expect(async () => {
clicked.value = true
await nextTick()
}).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
expect(container.innerHTML).toBe(
`<div show="false"><!--[--><div><div><!----></div></div><div>1</div><!--]--></div>`,
)
await nextTick()
expect(container.innerHTML).toBe(
`<div show="true"><!--[--><div><div><div>foo</div></div></div><div>1</div><!--]--></div>`,
)
__DEV__ = true
show.value = true
await nextTick()
expect(async () => {
clicked.value = true
await nextTick()
}).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
await nextTick()
expect(container.innerHTML).toBe(
`<div show="true"><!--[--><div><div><div>foo</div></div></div><div>1</div><!--]--></div>`,
)
} catch (e) {
throw e
} finally {
__DEV__ = true
}
})
describe('mismatch handling', () => {

View File

@ -8,6 +8,8 @@ import {
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createElementVNode,
createTextVNode,
createVNode,
defineComponent,
@ -23,6 +25,7 @@ import {
renderList,
renderSlot,
serialize,
setBlockTracking,
withCtx,
} from '@vue/runtime-test'
import { PatchFlags, SlotFlags } from '@vue/shared'
@ -434,7 +437,7 @@ describe('renderer: optimized mode', () => {
const App = {
setup() {
return () => {
return createVNode(Comp, null, {
return createBlock(Comp, null, {
default: withCtx(() => [
createVNode('p', null, foo.value, PatchFlags.TEXT),
]),
@ -560,6 +563,7 @@ describe('renderer: optimized mode', () => {
const state = ref(0)
const CompA = {
name: 'A',
setup(props: any, { slots }: SetupContext) {
return () => {
return (
@ -571,6 +575,7 @@ describe('renderer: optimized mode', () => {
}
const Wrapper = {
name: 'Wrapper',
setup(props: any, { slots }: SetupContext) {
// use the manually written render function to rendering the optimized slots,
// which should make subsequent updates exit the optimized mode correctly
@ -581,6 +586,7 @@ describe('renderer: optimized mode', () => {
}
const app = createApp({
name: 'App',
setup() {
return () => {
return (
@ -612,7 +618,7 @@ describe('renderer: optimized mode', () => {
})
//#3623
test('nested teleport unmount need exit the optimization mode', () => {
test('nested teleport unmount need exit the optimization mode', async () => {
const target = nodeOps.createElement('div')
const root = nodeOps.createElement('div')
@ -641,6 +647,7 @@ describe('renderer: optimized mode', () => {
])),
root,
)
await nextTick()
expect(inner(target)).toMatchInlineSnapshot(
`"<div><!--teleport start--><!--teleport end--></div><div>foo</div>"`,
)
@ -959,4 +966,271 @@ describe('renderer: optimized mode', () => {
// should successfully unmount without error
expect(inner(root)).toBe(`<!---->`)
})
// #10870
test('should bail manually rendered compiler slots for both mount and update', async () => {
// only reproducible in prod
__DEV__ = false
function Outer(_: any, { slots }: any) {
return slots.default()
}
const Mid = {
render(ctx: any) {
return (
openBlock(),
createElementBlock('div', null, [renderSlot(ctx.$slots, 'default')])
)
},
}
const state1 = ref(true)
const state2 = ref(true)
const App = {
render() {
return (
openBlock(),
createBlock(Outer, null, {
default: withCtx(() => [
createVNode(
Mid,
{ foo: state2.value },
{
default: withCtx(() => [
createElementVNode('div', null, [
createElementVNode('div', null, [
state2.value
? (openBlock(),
createElementBlock(
'div',
{
key: 0,
id: 'if',
foo: state1.value,
},
null,
8 /* PROPS */,
['foo'],
))
: createCommentVNode('v-if', true),
]),
]),
]),
_: 1 /* STABLE */,
},
8 /* PROPS */,
['foo'],
),
]),
_: 1 /* STABLE */,
})
)
},
}
const app = createApp(App)
app.config.errorHandler = vi.fn()
try {
app.mount(root)
state1.value = false
await nextTick()
state2.value = false
await nextTick()
} finally {
__DEV__ = true
expect(app.config.errorHandler).not.toHaveBeenCalled()
}
})
// #11336
test('should bail manually rendered compiler slots for both mount and update (2)', async () => {
// only reproducible in prod
__DEV__ = false
const n = ref(0)
function Outer(_: any, { slots }: any) {
n.value // track
return slots.default()
}
const Mid = {
render(ctx: any) {
return (
openBlock(),
createElementBlock('div', null, [renderSlot(ctx.$slots, 'default')])
)
},
}
const show = ref(false)
const App = {
render() {
return (
openBlock(),
createBlock(Outer, null, {
default: withCtx(() => [
createVNode(Mid, null, {
default: withCtx(() => [
createElementVNode('div', null, [
show.value
? (openBlock(),
createElementBlock('div', { key: 0 }, '1'))
: createCommentVNode('v-if', true),
createElementVNode('div', null, '2'),
createElementVNode('div', null, '3'),
]),
createElementVNode('div', null, '4'),
]),
_: 1 /* STABLE */,
}),
]),
_: 1 /* STABLE */,
})
)
},
}
const app = createApp(App)
app.config.errorHandler = vi.fn()
try {
app.mount(root)
// force Outer update, which will assign new slots to Mid
// we want to make sure the compiled slot flag doesn't accidentally
// get assigned again
n.value++
await nextTick()
show.value = true
await nextTick()
} finally {
__DEV__ = true
expect(app.config.errorHandler).not.toHaveBeenCalled()
}
})
test('diff slot and slot fallback node', async () => {
const Comp = {
props: ['show'],
setup(props: any, { slots }: SetupContext) {
return () => {
return (
openBlock(),
createElementBlock('div', null, [
renderSlot(slots, 'default', { hide: !props.show }, () => [
(openBlock(),
(block = createElementBlock(
Fragment,
{ key: 0 },
[createTextVNode('foo')],
PatchFlags.STABLE_FRAGMENT,
))),
]),
])
)
}
},
}
const show = ref(true)
const app = createApp({
render() {
return (
openBlock(),
createBlock(
Comp,
{ show: show.value },
{
default: withCtx(({ hide }: { hide: boolean }) => [
!hide
? (openBlock(),
createElementBlock(
Fragment,
{ key: 0 },
[
createCommentVNode('comment'),
createElementVNode(
'div',
null,
'bar',
PatchFlags.CACHED,
),
],
PatchFlags.STABLE_FRAGMENT,
))
: createCommentVNode('v-if', true),
]),
_: SlotFlags.STABLE,
},
PatchFlags.PROPS,
['show'],
)
)
},
})
app.mount(root)
expect(inner(root)).toBe('<div><!--comment--><div>bar</div></div>')
expect(block).toBe(null)
show.value = false
await nextTick()
expect(inner(root)).toBe('<div>foo</div>')
show.value = true
await nextTick()
expect(inner(root)).toBe('<div><!--comment--><div>bar</div></div>')
})
test('should not take unmount children fast path if children contain cached nodes', async () => {
const show = ref(true)
const spyUnmounted = vi.fn()
const Child = {
setup() {
onUnmounted(spyUnmounted)
return () => createVNode('div', null, 'Child')
},
}
const app = createApp({
render(_: any, cache: any) {
return show.value
? (openBlock(),
createBlock('div', null, [
createVNode('div', null, [
cache[0] ||
(setBlockTracking(-1),
((cache[0] = createVNode('div', null, [
createVNode(Child),
])).cacheIndex = 0),
setBlockTracking(1),
cache[0]),
]),
]))
: createCommentVNode('v-if', true)
},
})
app.mount(root)
expect(inner(root)).toBe(
'<div><div><div><div>Child</div></div></div></div>',
)
show.value = false
await nextTick()
expect(inner(root)).toBe('<!--v-if-->')
expect(spyUnmounted).toHaveBeenCalledTimes(1)
show.value = true
await nextTick()
expect(inner(root)).toBe(
'<div><div><div><div>Child</div></div></div></div>',
)
// should unmount again, this verifies previous cache was properly cleared
show.value = false
await nextTick()
expect(inner(root)).toBe('<!--v-if-->')
expect(spyUnmounted).toHaveBeenCalledTimes(2)
})
})

View File

@ -2,6 +2,7 @@ import {
Comment,
Fragment,
Text,
type VNode,
cloneVNode,
createBlock,
createVNode,
@ -633,7 +634,9 @@ describe('vnode', () => {
setBlockTracking(1),
vnode1,
]))
expect(vnode.dynamicChildren).toStrictEqual([])
const expected: VNode['dynamicChildren'] = []
expected.hasOnce = true
expect(vnode.dynamicChildren).toStrictEqual(expected)
})
// #5657
test('error of slot function execution should not affect block tracking', () => {

View File

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

View File

@ -15,6 +15,7 @@ import { ref } from '@vue/reactivity'
import { ErrorCodes, handleError } from './errorHandling'
import { isKeepAlive } from './components/KeepAlive'
import { queueJob } from './scheduler'
import { markAsyncBoundary } from './helpers/useId'
export type AsyncComponentResolveResult<T = Component> = T | { default: T } // es modules
@ -157,6 +158,8 @@ export function defineAsyncComponent<
})
: null
})
} else {
markAsyncBoundary(instance)
}
const loaded = ref(false)

View File

@ -124,6 +124,18 @@ export interface AppConfig {
* Enable warnings for computed getters that recursively trigger itself.
*/
warnRecursiveComputed?: boolean
/**
* Whether to throw unhandled errors in production.
* Default is `false` to avoid crashing on any error (and only logs it)
* But in some cases, e.g. SSR, throwing might be more desirable.
*/
throwUnhandledErrorInProduction?: boolean
/**
* Prefix for all useId() calls within this app
*/
idPrefix?: string
}
export interface AppContext {

View File

@ -93,6 +93,7 @@ import type { SuspenseProps } from './components/Suspense'
import type { KeepAliveProps } from './components/KeepAlive'
import type { BaseTransitionProps } from './components/BaseTransition'
import type { DefineComponent } from './apiDefineComponent'
import { markAsyncBoundary } from './helpers/useId'
/**
* Public utility type for extracting the instance type of a component.
@ -100,7 +101,7 @@ import type { DefineComponent } from './apiDefineComponent'
* the usage of `InstanceType<typeof Comp>` which only works for
* constructor-based component definition types.
*
* Exmaple:
* @example
* ```ts
* const MyComp = { ... }
* declare const instance: ComponentInstance<typeof MyComp>
@ -355,6 +356,13 @@ export interface ComponentInternalInstance {
* @internal
*/
provides: Data
/**
* for tracking useId()
* first element is the current boundary prefix
* second number is the index of the useId call within that boundary
* @internal
*/
ids: [string, number, number]
/**
* Tracking reactive effects (e.g. watchers) associated with this component
* so that they can be automatically stopped on component unmount
@ -445,9 +453,6 @@ export interface ComponentInternalInstance {
refs: Data
emit: EmitFn
attrsProxy: Data | null
slotsProxy: Slots | null
/**
* used for keeping track of .once event handlers on components
* @internal
@ -618,6 +623,7 @@ export function createComponentInstance(
withProxy: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
ids: parent ? parent.ids : ['', 0, 0],
accessCache: null!,
renderCache: [],
@ -649,9 +655,6 @@ export function createComponentInstance(
setupState: EMPTY_OBJ,
setupContext: null,
attrsProxy: null,
slotsProxy: null,
// suspense related
suspense,
suspenseId: suspense ? suspense.pendingId : 0,
@ -784,13 +787,14 @@ export let isInSSRComponentSetup = false
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false,
optimized = false,
) {
isSSR && setInSSRSetupState(isSSR)
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
initSlots(instance, children, optimized)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
@ -858,6 +862,8 @@ function setupStatefulComponent(
reset()
if (isPromise(setupResult)) {
// async setup, mark as async boundary for useId()
markAsyncBoundary(instance)
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
@ -1089,15 +1095,12 @@ const attrsProxyHandlers = __DEV__
* Dev-only
*/
function getSlotsProxy(instance: ComponentInternalInstance): Slots {
return (
instance.slotsProxy ||
(instance.slotsProxy = new Proxy(instance.slots, {
get(target, key: string) {
track(instance, TrackOpTypes.GET, '$slots')
return target[key]
},
}))
)
return new Proxy(instance.slots, {
get(target, key: string) {
track(instance, TrackOpTypes.GET, '$slots')
return target[key]
},
})
}
export function createSetupContext(
@ -1131,6 +1134,7 @@ export function createSetupContext(
// We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod)
let attrsProxy: Data
let slotsProxy: Slots
return Object.freeze({
get attrs() {
return (
@ -1139,7 +1143,7 @@ export function createSetupContext(
)
},
get slots() {
return getSlotsProxy(instance)
return slotsProxy || (slotsProxy = getSlotsProxy(instance))
},
get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args)

View File

@ -30,6 +30,7 @@ import {
compatModelEventPrefix,
} from './compat/componentVModel'
import type { ComponentTypeEmits } from './apiSetupHelpers'
import { getModelModifiers } from './helpers/useModel'
export type ObjectEmitsOptions = Record<
string,
@ -145,16 +146,12 @@ export function emit(
const isModelListener = event.startsWith('update:')
// for v-model update:xxx events, apply modifiers on args
const modelArg = isModelListener && event.slice(7)
if (modelArg && modelArg in props) {
const modifiersKey = `${
modelArg === 'modelValue' ? 'model' : modelArg
}Modifiers`
const { number, trim } = props[modifiersKey] || EMPTY_OBJ
if (trim) {
const modifiers = isModelListener && getModelModifiers(props, event.slice(7))
if (modifiers) {
if (modifiers.trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
}
if (number) {
if (modifiers.number) {
args = rawArgs.map(looseToNumber)
}
}

View File

@ -85,6 +85,7 @@ import {
type ComponentTypeEmits,
normalizePropsOrEmits,
} from './apiSetupHelpers'
import { markAsyncBoundary } from './helpers/useId'
/**
* Interface for declaring custom options.
@ -772,6 +773,10 @@ export function applyOptions(instance: ComponentInternalInstance) {
) {
instance.filters = filters
}
if (__SSR__ && serverPrefetch) {
markAsyncBoundary(instance)
}
}
export function resolveInjections(

View File

@ -498,12 +498,15 @@ function resolvePropValue(
return value
}
const mixinPropsCache = new WeakMap<ConcreteComponent, NormalizedPropsOptions>()
export function normalizePropsOptions(
comp: ConcreteComponent,
appContext: AppContext,
asMixin = false,
): NormalizedPropsOptions {
const cache = appContext.propsCache
const cache =
__FEATURE_OPTIONS_API__ && asMixin ? mixinPropsCache : appContext.propsCache
const cached = cache.get(comp)
if (cached) {
return cached

View File

@ -12,7 +12,6 @@ import {
ShapeFlags,
SlotFlags,
def,
extend,
isArray,
isFunction,
} from '@vue/shared'
@ -161,17 +160,36 @@ const normalizeVNodeSlots = (
instance.slots.default = () => normalized
}
const assignSlots = (
slots: InternalSlots,
children: Slots,
optimized: boolean,
) => {
for (const key in children) {
// #2893
// when rendering the optimized slots by manually written render function,
// do not copy the `slots._` compiler flag so that `renderSlot` creates
// slot Fragment with BAIL patchFlag to force full updates
if (optimized || key !== '_') {
slots[key] = children[key]
}
}
}
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren,
optimized: boolean,
) => {
const slots = (instance.slots = createInternalObject())
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const type = (children as RawSlots)._
if (type) {
extend(slots, children as InternalSlots)
assignSlots(slots, children as Slots, optimized)
// make compiler marker non-enumerable
def(slots, '_', type, true)
if (optimized) {
def(slots, '_', type, true)
}
} else {
normalizeObjectSlots(children as RawSlots, slots, instance)
}
@ -195,7 +213,7 @@ export const updateSlots = (
if (__DEV__ && isHmrUpdating) {
// Parent was HMR updated so slot content may have changed.
// force update slots and mark instance for hmr as well
extend(slots, children as Slots)
assignSlots(slots, children as Slots, optimized)
trigger(instance, TriggerOpTypes.SET, '$slots')
} else if (optimized && type === SlotFlags.STABLE) {
// compiled AND stable.
@ -204,14 +222,7 @@ export const updateSlots = (
} else {
// compiled but dynamic (v-if/v-for on slots) - update slots, but skip
// normalization.
extend(slots, children as Slots)
// #2893
// when rendering the optimized slots by manually written render function,
// we need to delete the `slots._` flag if necessary to make subsequent updates reliable,
// i.e. let the `renderSlot` create the bailed Fragment
if (!optimized && type === SlotFlags.STABLE) {
delete slots._
}
assignSlots(slots, children as Slots, optimized)
}
} else {
needDeletionCheck = !(children as RawSlots).$stable

View File

@ -7,6 +7,7 @@ import {
type RendererInternals,
type RendererNode,
type RendererOptions,
queuePostRenderEffect,
traverseStaticChildren,
} from '../renderer'
import type { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
@ -19,13 +20,19 @@ export type TeleportVNode = VNode<RendererNode, RendererElement, TeleportProps>
export interface TeleportProps {
to: string | RendererElement | null | undefined
disabled?: boolean
defer?: boolean
}
export const TeleportEndKey = Symbol('_vte')
export const isTeleport = (type: any): boolean => type.__isTeleport
const isTeleportDisabled = (props: VNode['props']): boolean =>
props && (props.disabled || props.disabled === '')
const isTeleportDeferred = (props: VNode['props']): boolean =>
props && (props.defer || props.defer === '')
const isTargetSVG = (target: RendererElement): boolean =>
typeof SVGElement !== 'undefined' && target instanceof SVGElement
@ -105,21 +112,13 @@ export const TeleportImpl = {
const mainAnchor = (n2.anchor = __DEV__
? createComment('teleport end')
: createText(''))
const targetStart = (n2.targetStart = createText(''))
const targetAnchor = (n2.targetAnchor = createText(''))
insert(placeholder, container, anchor)
insert(mainAnchor, container, anchor)
const target = (n2.target = resolveTarget(n2.props, querySelector))
const targetAnchor = (n2.targetAnchor = createText(''))
if (target) {
insert(targetAnchor, target)
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
if (namespace === 'svg' || isTargetSVG(target)) {
namespace = 'svg'
} else if (namespace === 'mathml' || isTargetMathML(target)) {
namespace = 'mathml'
}
} else if (__DEV__ && !disabled) {
warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
}
// attach a special property so we can skip teleported content in
// renderer's nextSibling search
targetStart[TeleportEndKey] = targetAnchor
const mount = (container: RendererElement, anchor: RendererNode) => {
// Teleport *always* has Array children. This is enforced in both the
@ -138,14 +137,44 @@ export const TeleportImpl = {
}
}
const mountToTarget = () => {
const target = (n2.target = resolveTarget(n2.props, querySelector))
if (target) {
insert(targetStart, target)
insert(targetAnchor, target)
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
if (namespace !== 'svg' && isTargetSVG(target)) {
namespace = 'svg'
} else if (namespace !== 'mathml' && isTargetMathML(target)) {
namespace = 'mathml'
}
if (!disabled) {
mount(target, targetAnchor)
updateCssVars(n2)
}
} else if (__DEV__ && !disabled) {
warn(
'Invalid Teleport target on mount:',
target,
`(${typeof target})`,
)
}
}
if (disabled) {
mount(container, mainAnchor)
} else if (target) {
mount(target, targetAnchor)
updateCssVars(n2)
}
if (isTeleportDeferred(n2.props)) {
queuePostRenderEffect(mountToTarget, parentSuspense)
} else {
mountToTarget()
}
} else {
// update content
n2.el = n1.el
n2.targetStart = n1.targetStart
const mainAnchor = (n2.anchor = n1.anchor)!
const target = (n2.target = n1.target)!
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
@ -241,9 +270,8 @@ export const TeleportImpl = {
)
}
}
updateCssVars(n2)
}
updateCssVars(n2)
},
remove(
@ -253,9 +281,18 @@ export const TeleportImpl = {
{ um: unmount, o: { remove: hostRemove } }: RendererInternals,
doRemove: boolean,
) {
const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode
const {
shapeFlag,
children,
anchor,
targetStart,
targetAnchor,
target,
props,
} = vnode
if (target) {
hostRemove(targetStart!)
hostRemove(targetAnchor!)
}
@ -424,7 +461,7 @@ function updateCssVars(vnode: VNode) {
// code path here can assume browser environment.
const ctx = vnode.ctx
if (ctx && ctx.ut) {
let node = (vnode.children as VNode[])[0].el!
let node = vnode.targetStart
while (node && node !== vnode.targetAnchor) {
if (node.nodeType === 1) node.setAttribute('data-v-owner', ctx.uid)
node = node.nextSibling

View File

@ -24,6 +24,7 @@ export function initCustomFormatter() {
// custom formatter for Chrome
// https://www.mattzeunert.com/2016/02/19/custom-chrome-devtools-object-formatters.html
const formatter = {
__vue_custom_formatter: true,
header(obj: unknown) {
// TODO also format ComponentPublicInstance & ctx.slots/attrs in setup
if (!isObject(obj)) {

View File

@ -2,7 +2,7 @@ import { pauseTracking, resetTracking } from '@vue/reactivity'
import type { VNode } from './vnode'
import type { ComponentInternalInstance } from './component'
import { popWarningContext, pushWarningContext, warn } from './warning'
import { isArray, isFunction, isPromise } from '@vue/shared'
import { EMPTY_OBJ, isArray, isFunction, isPromise } from '@vue/shared'
import { LifecycleHooks } from './enums'
import { BaseWatchErrorCodes } from '@vue/reactivity'
@ -27,6 +27,7 @@ export enum ErrorCodes {
FUNCTION_REF,
ASYNC_COMPONENT_LOADER,
SCHEDULER,
COMPONENT_UPDATE,
APP_UNMOUNT_CLEANUP,
}
@ -61,15 +62,14 @@ export const ErrorTypeStrings: Record<ErrorTypes, string> = {
[ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
[ErrorCodes.FUNCTION_REF]: 'ref function',
[ErrorCodes.ASYNC_COMPONENT_LOADER]: 'async component loader',
[ErrorCodes.SCHEDULER]:
'scheduler flush. This is likely a Vue internals bug. ' +
'Please open an issue at https://github.com/vuejs/core .',
[ErrorCodes.SCHEDULER]: 'scheduler flush',
[ErrorCodes.COMPONENT_UPDATE]: 'component update',
[ErrorCodes.APP_UNMOUNT_CLEANUP]: 'app unmount cleanup function',
}
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
instance: ComponentInternalInstance | null | undefined,
type: ErrorTypes,
args?: unknown[],
) {
@ -111,11 +111,13 @@ export function callWithAsyncErrorHandling(
export function handleError(
err: unknown,
instance: ComponentInternalInstance | null,
instance: ComponentInternalInstance | null | undefined,
type: ErrorTypes,
throwInDev = true,
) {
const contextVNode = instance ? instance.vnode : null
const { errorHandler, throwUnhandledErrorInProduction } =
(instance && instance.appContext.config) || EMPTY_OBJ
if (instance) {
let cur = instance.parent
// the exposed instance is the render proxy to keep it consistent with 2.x
@ -138,20 +140,18 @@ export function handleError(
cur = cur.parent
}
// app-level handling
const appErrorHandler = instance.appContext.config.errorHandler
if (appErrorHandler) {
if (errorHandler) {
pauseTracking()
callWithErrorHandling(
appErrorHandler,
null,
ErrorCodes.APP_ERROR_HANDLER,
[err, exposedInstance, errorInfo],
)
callWithErrorHandling(errorHandler, null, ErrorCodes.APP_ERROR_HANDLER, [
err,
exposedInstance,
errorInfo,
])
resetTracking()
return
}
}
logError(err, type, contextVNode, throwInDev)
logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction)
}
function logError(
@ -159,6 +159,7 @@ function logError(
type: ErrorTypes,
contextVNode: VNode | null,
throwInDev = true,
throwInProd = false,
) {
if (__DEV__) {
const info = ErrorTypeStrings[type]
@ -175,6 +176,8 @@ function logError(
} else if (!__TEST__) {
console.error(err)
}
} else if (throwInProd) {
throw err
} else {
// recover in prod to reduce the impact on end-user
console.error(err)

View File

@ -65,11 +65,13 @@ export function renderSlot(
Fragment,
{
key:
props.key ||
// slot content array of a dynamic conditional slot may have a branch
// key attached in the `createSlots` helper, respect that
(validSlotContent && (validSlotContent as any).key) ||
`_${name}`,
(props.key ||
// slot content array of a dynamic conditional slot may have a branch
// key attached in the `createSlots` helper, respect that
(validSlotContent && (validSlotContent as any).key) ||
`_${name}`) +
// #7256 force differentiate fallback content from actual content
(!validSlotContent && fallback ? '_fb' : ''),
},
validSlotContent || (fallback ? fallback() : []),
validSlotContent && (slots as RawSlots)._ === SlotFlags.STABLE

View File

@ -0,0 +1,27 @@
import {
type ComponentInternalInstance,
getCurrentInstance,
} from '../component'
import { warn } from '../warning'
export function useId() {
const i = getCurrentInstance()
if (i) {
return (i.appContext.config.idPrefix || 'v') + ':' + i.ids[0] + i.ids[1]++
} else if (__DEV__) {
warn(
`useId() is called when there is no active component ` +
`instance to be associated with.`,
)
}
}
/**
* There are 3 types of async boundaries:
* - async components
* - components with async setup()
* - components with serverPrefetch
*/
export function markAsyncBoundary(instance: ComponentInternalInstance) {
instance.ids = [instance.ids[0] + instance.ids[2]++ + '-', 0, 0]
}

View File

@ -29,9 +29,13 @@ export function useModel(
const camelizedName = camelize(name)
const hyphenatedName = hyphenate(name)
const modifiers = getModelModifiers(props, name)
const res = customRef((track, trigger) => {
let localValue: any
let prevSetValue: any
let prevEmittedValue: any
watchSyncEffect(() => {
const propValue = props[name]
if (hasChanged(localValue, propValue)) {
@ -39,12 +43,17 @@ export function useModel(
trigger()
}
})
return {
get() {
track()
return options.get ? options.get(localValue) : localValue
},
set(value) {
if (!hasChanged(value, localValue)) {
return
}
const rawProps = i.vnode!.props
if (
!(
@ -56,27 +65,38 @@ export function useModel(
(`onUpdate:${name}` in rawProps ||
`onUpdate:${camelizedName}` in rawProps ||
`onUpdate:${hyphenatedName}` in rawProps)
) &&
hasChanged(value, localValue)
)
) {
// no v-model, local update
localValue = value
trigger()
}
i.emit(`update:${name}`, options.set ? options.set(value) : value)
const emittedValue = options.set ? options.set(value) : value
i.emit(`update:${name}`, emittedValue)
// #10279: if the local value is converted via a setter but the value
// emitted to parent was the same, the parent will not trigger any
// updates and there will be no prop sync. However the local input state
// may be out of sync, so we need to force an update here.
if (
value !== emittedValue &&
value !== prevSetValue &&
emittedValue === prevEmittedValue
) {
trigger()
}
prevSetValue = value
prevEmittedValue = emittedValue
},
}
})
const modifierKey =
name === 'modelValue' ? 'modelModifiers' : `${name}Modifiers`
// @ts-expect-error
res[Symbol.iterator] = () => {
let i = 0
return {
next() {
if (i < 2) {
return { value: i++ ? props[modifierKey] || {} : res, done: false }
return { value: i++ ? modifiers || EMPTY_OBJ : res, done: false }
} else {
return { done: true }
}
@ -86,3 +106,14 @@ export function useModel(
return res
}
export const getModelModifiers = (
props: Record<string, any>,
modelName: string,
): Record<string, boolean> | undefined => {
return modelName === 'modelValue' || modelName === 'model-value'
? props.modelModifiers
: props[`${modelName}Modifiers`] ||
props[`${camelize(modelName)}Modifiers`] ||
props[`${hyphenate(modelName)}Modifiers`]
}

View File

@ -0,0 +1,25 @@
import { type ShallowRef, readonly, shallowRef } from '@vue/reactivity'
import { getCurrentInstance } from '../component'
import { warn } from '../warning'
import { EMPTY_OBJ } from '@vue/shared'
export function useTemplateRef<T = unknown>(
key: string,
): Readonly<ShallowRef<T | null>> {
const i = getCurrentInstance()
const r = shallowRef(null)
if (i) {
const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs
Object.defineProperty(refs, key, {
enumerable: true,
get: () => r.value,
set: val => (r.value = val),
})
} else if (__DEV__) {
warn(
`useTemplateRef() is called when there is no active component ` +
`instance to be associated with.`,
)
}
return (__DEV__ ? readonly(r) : r) as any
}

View File

@ -15,7 +15,7 @@ export function withMemo(
// shallow clone
ret.memo = memo.slice()
ret.memoIndex = index
ret.cacheIndex = index
return (cache[index] = ret)
}

View File

@ -14,7 +14,10 @@ type HMRComponent = ComponentOptions | ClassComponent
export let isHmrUpdating = false
export const hmrDirtyComponents = new Set<ConcreteComponent>()
export const hmrDirtyComponents = new Map<
ConcreteComponent,
Set<ComponentInternalInstance>
>()
export interface HMRRuntime {
createRecord: typeof createRecord
@ -109,18 +112,21 @@ function reload(id: string, newComp: HMRComponent) {
// create a snapshot which avoids the set being mutated during updates
const instances = [...record.instances]
for (const instance of instances) {
for (let i = 0; i < instances.length; i++) {
const instance = instances[i]
const oldComp = normalizeClassComponent(instance.type as HMRComponent)
if (!hmrDirtyComponents.has(oldComp)) {
let dirtyInstances = hmrDirtyComponents.get(oldComp)
if (!dirtyInstances) {
// 1. Update existing comp definition to match new one
if (oldComp !== record.initialDef) {
updateComponentDef(oldComp, newComp)
}
// 2. mark definition dirty. This forces the renderer to replace the
// component on patch.
hmrDirtyComponents.add(oldComp)
hmrDirtyComponents.set(oldComp, (dirtyInstances = new Set()))
}
dirtyInstances.add(instance)
// 3. invalidate options resolution cache
instance.appContext.propsCache.delete(instance.type as any)
@ -130,17 +136,17 @@ function reload(id: string, newComp: HMRComponent) {
// 4. actually update
if (instance.ceReload) {
// custom element
hmrDirtyComponents.add(oldComp)
dirtyInstances.add(instance)
instance.ceReload((newComp as any).styles)
hmrDirtyComponents.delete(oldComp)
dirtyInstances.delete(instance)
} else if (instance.parent) {
// 4. Force the parent instance to re-render. This will cause all updated
// components to be unmounted and re-mounted. Queue the update so that we
// don't end up forcing the same parent to re-render multiple times.
queueJob(() => {
instance.parent!.update()
// #6930 avoid infinite recursion
hmrDirtyComponents.delete(oldComp)
// #6930, #11248 avoid infinite recursion
dirtyInstances.delete(instance)
})
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method
@ -157,11 +163,7 @@ function reload(id: string, newComp: HMRComponent) {
// 5. make sure to cleanup dirty hmr components after update
queuePostFlushCb(() => {
for (const instance of instances) {
hmrDirtyComponents.delete(
normalizeClassComponent(instance.type as HMRComponent),
)
}
hmrDirtyComponents.clear()
})
}

View File

@ -39,6 +39,7 @@ import {
} from './components/Suspense'
import type { TeleportImpl, TeleportVNode } from './components/Teleport'
import { isAsyncWrapper } from './apiAsyncComponent'
import { isReactive } from '@vue/reactivity'
export type RootHydrateFunction = (
vnode: VNode<Node, Element>,
@ -464,15 +465,7 @@ export function createHydrationFunctions(
// force hydrate v-bind with .prop modifiers
key[0] === '.'
) {
patchProp(
el,
key,
null,
props[key],
undefined,
undefined,
parentComponent,
)
patchProp(el, key, null, props[key], undefined, parentComponent)
}
}
} else if (props.onClick) {
@ -484,9 +477,13 @@ export function createHydrationFunctions(
null,
props.onClick,
undefined,
undefined,
parentComponent,
)
} else if (patchFlag & PatchFlags.STYLE && isReactive(props.style)) {
// #11372: object style values are iterated during patch instead of
// render/normalization phase, but style patch is skipped during
// hydration, so we need to force iterate the object to track deps
for (const key in props.style) props.style[key]
}
}
@ -531,7 +528,27 @@ export function createHydrationFunctions(
const vnode = optimized
? children[i]
: (children[i] = normalizeVNode(children[i]))
const isText = vnode.type === Text
if (node) {
if (isText && !optimized) {
// #7285 possible consecutive text vnodes from manual render fns or
// JSX-compiled fns, but on the client the browser parses only 1 text
// node.
// look ahead for next possible text vnode
let next = children[i + 1]
if (next && (next = normalizeVNode(next)).type === Text) {
// create an extra TextNode on the client for the next vnode to
// adopt
insert(
createText(
(node as Text).data.slice((vnode.children as string).length),
),
container,
nextSibling(node),
)
;(node as Text).data = vnode.children as string
}
}
node = hydrateNode(
node,
vnode,
@ -540,7 +557,7 @@ export function createHydrationFunctions(
slotScopeIds,
optimized,
)
} else if (vnode.type === Text && !vnode.children) {
} else if (isText && !vnode.children) {
// #7215 create a TextNode for empty text node
// because server rendered HTML won't contain a text node
insert((vnode.el = createText('')), container)

View File

@ -63,6 +63,8 @@ export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent'
export { useAttrs, useSlots } from './apiSetupHelpers'
export { useModel } from './helpers/useModel'
export { useTemplateRef } from './helpers/useTemplateRef'
export { useId } from './helpers/useId'
// <script setup> API ----------------------------------------------------------

View File

@ -65,7 +65,11 @@ import {
type SuspenseImpl,
queueEffectWithSuspense,
} from './components/Suspense'
import type { TeleportImpl, TeleportVNode } from './components/Teleport'
import {
TeleportEndKey,
type TeleportImpl,
type TeleportVNode,
} from './components/Teleport'
import { type KeepAliveContext, isKeepAlive } from './components/KeepAlive'
import { isHmrUpdating, registerHMR, unregisterHMR } from './hmr'
import { type RootHydrateFunction, createHydrationFunctions } from './hydration'
@ -110,10 +114,7 @@ export interface RendererOptions<
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
prevChildren?: VNode<HostNode, HostElement>[],
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
unmountChildren?: UnmountChildrenFn,
): void
insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
remove(el: HostNode): void
@ -147,7 +148,7 @@ export interface RendererOptions<
// functions provided via options, so the internal constraint is really just
// a generic object.
export interface RendererNode {
[key: string]: any
[key: string | symbol]: any
}
export interface RendererElement extends RendererNode {}
@ -685,17 +686,7 @@ function baseCreateRenderer(
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren,
)
hostPatchProp(el, key, null, props[key], namespace, parentComponent)
}
}
/**
@ -848,6 +839,15 @@ function baseCreateRenderer(
dynamicChildren = null
}
// #9135 innerHTML / textContent unset needs to happen before possible
// new children mount
if (
(oldProps.innerHTML && newProps.innerHTML == null) ||
(oldProps.textContent && newProps.textContent == null)
) {
hostSetElementText(el, '')
}
if (dynamicChildren) {
patchBlockChildren(
n1.dynamicChildren!,
@ -884,15 +884,7 @@ function baseCreateRenderer(
// (i.e. at the exact same position in the source template)
if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
namespace,
)
patchProps(el, oldProps, newProps, parentComponent, namespace)
} else {
// class
// this flag is matched when the element has dynamic class bindings.
@ -923,17 +915,7 @@ function baseCreateRenderer(
const next = newProps[key]
// #1471 force patch value
if (next !== prev || key === 'value') {
hostPatchProp(
el,
key,
prev,
next,
namespace,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren,
)
hostPatchProp(el, key, prev, next, namespace, parentComponent)
}
}
}
@ -948,15 +930,7 @@ function baseCreateRenderer(
}
} else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
namespace,
)
patchProps(el, oldProps, newProps, parentComponent, namespace)
}
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
@ -1013,11 +987,9 @@ function baseCreateRenderer(
const patchProps = (
el: RendererElement,
vnode: VNode,
oldProps: Data,
newProps: Data,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
) => {
if (oldProps !== newProps) {
@ -1030,10 +1002,7 @@ function baseCreateRenderer(
oldProps[key],
null,
namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren,
)
}
}
@ -1045,17 +1014,7 @@ function baseCreateRenderer(
const prev = oldProps[key]
// defer patching value
if (next !== prev && key !== 'value') {
hostPatchProp(
el,
key,
prev,
next,
namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren,
)
hostPatchProp(el, key, prev, next, namespace, parentComponent)
}
}
if ('value' in newProps) {
@ -1248,7 +1207,7 @@ function baseCreateRenderer(
if (__DEV__) {
startMeasure(instance, `init`)
}
setupComponent(instance)
setupComponent(instance, false, optimized)
if (__DEV__) {
endMeasure(instance, `init`)
}
@ -1599,6 +1558,7 @@ function baseCreateRenderer(
const update = (instance.update = effect.run.bind(effect))
const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
job.i = instance
job.id = instance.uid
effect.scheduler = () => queueJob(job)
@ -1613,7 +1573,6 @@ function baseCreateRenderer(
effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
job.ownerInstance = instance
}
update()
@ -2123,7 +2082,7 @@ function baseCreateRenderer(
shapeFlag,
patchFlag,
dirs,
memoIndex,
cacheIndex,
} = vnode
if (patchFlag === PatchFlags.BAIL) {
@ -2136,8 +2095,8 @@ function baseCreateRenderer(
}
// #6593 should clean memo cache when unmount
if (memoIndex != null) {
parentComponent!.renderCache[memoIndex] = undefined
if (cacheIndex != null) {
parentComponent!.renderCache[cacheIndex] = undefined
}
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
@ -2178,6 +2137,12 @@ function baseCreateRenderer(
)
} else if (
dynamicChildren &&
// #5154
// when v-once is used inside a block, setBlockTracking(-1) marks the
// parent block with hasOnce: true
// so that it doesn't take the fast path during unmount - otherwise
// components nested in v-once are never unmounted.
!dynamicChildren.hasOnce &&
// #1153: fast path should not be taken for non-stable (v-for) fragments
(type !== Fragment ||
(patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
@ -2376,7 +2341,12 @@ function baseCreateRenderer(
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
return vnode.suspense!.next()
}
return hostNextSibling((vnode.anchor || vnode.el)!)
const el = hostNextSibling((vnode.anchor || vnode.el)!)
// #9071, #9313
// teleported content can mess up nextSibling searches during patch so
// we need to skip them during nextSibling search
const teleportEnd = el && el[TeleportEndKey]
return teleportEnd ? hostNextSibling(teleportEnd) : el
}
let isFlushing = false

View File

@ -12,9 +12,8 @@ export interface SchedulerJob extends BaseSchedulerJob {
/**
* Attached by renderer.ts when setting up a component's render effect
* Used to obtain component information when reporting max recursive updates.
* dev only.
*/
ownerInstance?: ComponentInternalInstance
i?: ComponentInternalInstance
}
export type SchedulerJobs = SchedulerJob | SchedulerJob[]
@ -106,7 +105,9 @@ export function invalidateJob(job: SchedulerJob) {
export function queuePostFlushCb(cb: SchedulerJobs) {
if (!isArray(cb)) {
if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
if (activePostFlushCbs && cb.id === -1) {
activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
} else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
pendingPostFlushCbs.push(cb)
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
cb.flags! |= SchedulerJobFlags.QUEUED
@ -228,7 +229,11 @@ function flushJobs(seen?: CountMap) {
if (__DEV__ && check(job)) {
continue
}
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
callWithErrorHandling(
job,
job.i,
job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
)
job.flags! &= ~SchedulerJobFlags.QUEUED
}
}
@ -254,7 +259,7 @@ function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
} else {
const count = seen.get(fn)!
if (count > RECURSION_LIMIT) {
const instance = fn.ownerInstance
const instance = fn.i
const componentName = instance && getComponentName(instance.type)
handleError(
`Maximum recursive updates exceeded${

View File

@ -92,10 +92,22 @@ export type VNodeRef =
) => void)
export type VNodeNormalizedRefAtom = {
/**
* component instance
*/
i: ComponentInternalInstance
/**
* Actual ref
*/
r: VNodeRef
k?: string // setup ref key
f?: boolean // refInFor marker
/**
* setup ref key
*/
k?: string
/**
* refInFor marker
*/
f?: boolean
}
export type VNodeNormalizedRef =
@ -186,6 +198,7 @@ export interface VNode<
el: HostNode | null
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetStart: HostNode | null // teleport target start anchor
targetAnchor: HostNode | null // teleport target anchor
/**
* number of elements contained in a static vnode
@ -214,7 +227,7 @@ export interface VNode<
/**
* @internal
*/
dynamicChildren: VNode[] | null
dynamicChildren: (VNode[] & { hasOnce?: boolean }) | null
// application root node only
appContext: AppContext | null
@ -231,7 +244,7 @@ export interface VNode<
/**
* @internal index for cleaning v-memo cache
*/
memoIndex?: number
cacheIndex?: number
/**
* @internal __COMPAT__ only
*/
@ -247,8 +260,8 @@ export interface VNode<
// can divide a template into nested blocks, and within each block the node
// structure would be stable. This allows us to skip most children diffing
// and only worry about the dynamic nodes (indicated by patch flags).
export const blockStack: (VNode[] | null)[] = []
export let currentBlock: VNode[] | null = null
export const blockStack: VNode['dynamicChildren'][] = []
export let currentBlock: VNode['dynamicChildren'] = null
/**
* Open a block.
@ -299,6 +312,11 @@ export let isBlockTreeEnabled = 1
*/
export function setBlockTracking(value: number) {
isBlockTreeEnabled += value
if (value < 0 && currentBlock) {
// mark current block so it doesn't take fast path and skip possible
// nested components duriung unmount
currentBlock.hasOnce = true
}
}
function setupBlock(vnode: VNode) {
@ -370,17 +388,16 @@ export function isVNode(value: any): value is VNode {
}
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
if (
__DEV__ &&
n2.shapeFlag & ShapeFlags.COMPONENT &&
hmrDirtyComponents.has(n2.type as ConcreteComponent)
) {
// #7042, ensure the vnode being unmounted during HMR
// bitwise operations to remove keep alive flags
n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
// HMR only: if the component has been hot-updated, force a reload.
return false
if (__DEV__ && n2.shapeFlag & ShapeFlags.COMPONENT && n1.component) {
const dirtyInstances = hmrDirtyComponents.get(n2.type as ConcreteComponent)
if (dirtyInstances && dirtyInstances.has(n1.component)) {
// #7042, ensure the vnode being unmounted during HMR
// bitwise operations to remove keep alive flags
n1.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
n2.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
// HMR only: if the component has been hot-updated, force a reload.
return false
}
}
return n1.type === n2.type && n1.key === n2.key
}
@ -461,6 +478,7 @@ function createBaseVNode(
el: null,
anchor: null,
target: null,
targetStart: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
@ -661,6 +679,7 @@ export function cloneVNode<T, U>(
? (children as VNode[]).map(deepCloneVNode)
: children,
target: vnode.target,
targetStart: vnode.targetStart,
targetAnchor: vnode.targetAnchor,
staticCount: vnode.staticCount,
shapeFlag: vnode.shapeFlag,

View File

@ -30,7 +30,12 @@ export function popWarningContext() {
stack.pop()
}
let isWarning = false
export function warn(msg: string, ...args: any[]) {
if (isWarning) return
isWarning = true
// avoid props formatting or warn handler tracking deps that might be mutated
// during patch, leading to infinite recursion.
pauseTracking()
@ -70,6 +75,7 @@ export function warn(msg: string, ...args: any[]) {
}
resetTracking()
isWarning = false
}
export function getComponentTrace(): ComponentTraceStack {

View File

@ -1,5 +1,5 @@
import { patchProp } from '../src/patchProp'
import { h, render } from '../src'
import { h, nextTick, ref, render } from '../src'
describe('runtime-dom: props patching', () => {
test('basic', () => {
@ -133,6 +133,31 @@ describe('runtime-dom: props patching', () => {
expect(fn).toHaveBeenCalled()
})
test('patch innerHTML porp', async () => {
const root = document.createElement('div')
const state = ref(false)
const Comp = {
render: () => {
if (state.value) {
return h('div', [h('del', null, 'baz')])
} else {
return h('div', { innerHTML: 'baz' })
}
},
}
render(h(Comp), root)
expect(root.innerHTML).toBe(`<div>baz</div>`)
state.value = true
await nextTick()
expect(root.innerHTML).toBe(`<div><del>baz</del></div>`)
})
test('patch innerHTML porp w/ undefined value', async () => {
const root = document.createElement('div')
render(h('div', { innerHTML: undefined }), root)
expect(root.innerHTML).toBe(`<div></div>`)
})
test('textContent unmount prev children', () => {
const fn = vi.fn()
const comp = {

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-dom",
"version": "3.5.0-alpha.2",
"version": "3.5.0-alpha.3",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",

View File

@ -10,19 +10,13 @@ export function patchDOMProp(
el: any,
key: string,
value: any,
// the following args are passed only due to potential innerHTML/textContent
// overriding existing VNodes, in which case the old tree must be properly
// unmounted.
prevChildren: any,
parentComponent: any,
parentSuspense: any,
unmountChildren: any,
) {
if (key === 'innerHTML' || key === 'textContent') {
if (prevChildren) {
unmountChildren(prevChildren, parentComponent, parentSuspense)
}
el[key] = value == null ? '' : value
// null value case is handled in renderer patchElement before patching
// children
if (value == null) return
el[key] = value
return
}

View File

@ -20,10 +20,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
prevValue,
nextValue,
namespace,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren,
) => {
const isSVG = namespace === 'svg'
if (key === 'class') {
@ -42,15 +39,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
? ((key = key.slice(1)), false)
: shouldSetAsProp(el, key, nextValue, isSVG)
) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren,
)
patchDOMProp(el, key, nextValue, parentComponent)
// #6007 also set form state as attributes so they work with
// <input type="reset"> or libs / extensions that expect attributes
// #11163 custom elements may use value as an prop and set it as object

View File

@ -0,0 +1,65 @@
import { bench, describe } from 'vitest'
import { createBuffer } from '../src/render'
describe('createBuffer', () => {
let stringBuffer = createBuffer()
bench(
'string only',
() => {
for (let i = 0; i < 10; i += 1) {
stringBuffer.push('hello')
}
},
{
setup() {
stringBuffer = createBuffer()
},
},
)
let stringNestedBuffer = createBuffer()
bench(
'string with nested',
() => {
for (let i = 0; i < 10; i += 1) {
if (i % 3 === 0) {
stringNestedBuffer.push('hello')
} else {
const buffer = createBuffer()
buffer.push('hello')
stringNestedBuffer.push(buffer.getBuffer())
}
}
},
{
setup() {
stringNestedBuffer = createBuffer()
},
},
)
bench(
'string with nested async',
() => {
for (let i = 0; i < 10; i += 1) {
if (i % 3 === 0) {
const buffer = createBuffer()
buffer.push('hello')
stringNestedBuffer.push(Promise.resolve(buffer.getBuffer()))
} else {
const buffer = createBuffer()
buffer.push('hello')
stringNestedBuffer.push(buffer.getBuffer())
}
}
},
{
setup() {
stringNestedBuffer = createBuffer()
},
},
)
})

View File

@ -0,0 +1,74 @@
import { bench, describe } from 'vitest'
import { type SSRBuffer, createBuffer } from '../src/render'
import { unrollBuffer } from '../src/renderToString'
function createSyncBuffer(levels: number, itemsPerLevel: number): SSRBuffer {
const buffer = createBuffer()
function addItems(buf: ReturnType<typeof createBuffer>, level: number) {
for (let i = 1; i <= levels * itemsPerLevel; i++) {
buf.push(`sync${level}.${i}`)
}
if (level < levels) {
const subBuffer = createBuffer()
addItems(subBuffer, level + 1)
buf.push(subBuffer.getBuffer())
}
}
addItems(buffer, 1)
return buffer.getBuffer()
}
function createMixedBuffer(levels: number, itemsPerLevel: number): SSRBuffer {
const buffer = createBuffer()
function addItems(buf: ReturnType<typeof createBuffer>, level: number) {
for (let i = 1; i <= levels * itemsPerLevel; i++) {
if (i % 3 === 0) {
// @ts-expect-error testing...
buf.push(Promise.resolve(`async${level}.${i}`))
} else {
buf.push(`sync${level}.${i}`)
}
}
if (level < levels) {
const subBuffer = createBuffer()
addItems(subBuffer, level + 1)
buf.push(subBuffer.getBuffer())
}
}
addItems(buffer, 1)
return buffer.getBuffer()
}
describe('unrollBuffer', () => {
let syncBuffer = createBuffer().getBuffer()
let mixedBuffer = createBuffer().getBuffer()
bench(
'sync',
() => {
return unrollBuffer(syncBuffer) as any
},
{
setup() {
syncBuffer = createSyncBuffer(5, 3)
},
},
)
bench(
'mixed',
() => {
return unrollBuffer(mixedBuffer) as any
},
{
setup() {
mixedBuffer = createMixedBuffer(5, 3)
},
},
)
})

View File

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

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