Merge branch 'main' into fix/7966

This commit is contained in:
edison 2024-08-01 11:26:09 +08:00 committed by GitHub
commit a94eb54365
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
213 changed files with 10737 additions and 5299 deletions

View File

@ -1,4 +0,0 @@
node_modules
dist
temp
coverage

View File

@ -1,140 +0,0 @@
const { builtinModules } = require('node:module')
const DOMGlobals = ['window', 'document']
const NodeGlobals = ['module', 'require']
const banConstEnum = {
selector: 'TSEnumDeclaration[const=true]',
message:
'Please use non-const enums. This project automatically inlines enums.',
}
/**
* @type {import('eslint-define-config').ESLintConfig}
*/
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
},
plugins: ['jest', 'import', '@typescript-eslint'],
rules: {
'no-debugger': 'error',
'no-console': ['error', { allow: ['warn', 'error', 'info'] }],
// most of the codebase are expected to be env agnostic
'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals],
'no-restricted-syntax': [
'error',
banConstEnum,
{
selector: 'ObjectPattern > RestElement',
message:
'Our output target is ES2016, and object rest spread results in ' +
'verbose helpers and should be avoided.',
},
{
selector: 'ObjectExpression > SpreadElement',
message:
'esbuild transpiles object spread into very verbose inline helpers.\n' +
'Please use the `extend` helper from @vue/shared instead.',
},
{
selector: 'AwaitExpression',
message:
'Our output target is ES2016, so async/await syntax should be avoided.',
},
],
'sort-imports': ['error', { ignoreDeclarationSort: true }],
'import/no-nodejs-modules': [
'error',
{ allow: builtinModules.map(mod => `node:${mod}`) },
],
// This rule enforces the preference for using '@ts-expect-error' comments in TypeScript
// code to indicate intentional type errors, improving code clarity and maintainability.
'@typescript-eslint/prefer-ts-expect-error': 'error',
// Enforce the use of 'import type' for importing types
'@typescript-eslint/consistent-type-imports': [
'error',
{
fixStyle: 'inline-type-imports',
disallowTypeAnnotations: false,
},
],
// Enforce the use of top-level import type qualifier when an import only has specifiers with inline type qualifiers
'@typescript-eslint/no-import-type-side-effects': 'error',
},
overrides: [
// tests, no restrictions (runs in Node / jest with jsdom)
{
files: ['**/__tests__/**', 'packages/dts-test/**'],
rules: {
'no-console': 'off',
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error',
},
},
// shared, may be used in any env
{
files: ['packages/shared/**', '.eslintrc.cjs'],
rules: {
'no-restricted-globals': 'off',
},
},
// Packages targeting DOM
{
files: ['packages/{vue,vue-compat,runtime-dom}/**'],
rules: {
'no-restricted-globals': ['error', ...NodeGlobals],
},
},
// Packages targeting Node
{
files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
rules: {
'no-restricted-globals': ['error', ...DOMGlobals],
'no-restricted-syntax': ['error', banConstEnum],
},
},
// Private package, browser only + no syntax restrictions
{
files: ['packages/template-explorer/**', 'packages/sfc-playground/**'],
rules: {
'no-restricted-globals': ['error', ...NodeGlobals],
'no-restricted-syntax': ['error', banConstEnum],
'no-console': 'off',
},
},
// JavaScript files
{
files: ['*.js', '*.cjs'],
rules: {
// We only do `no-unused-vars` checks for js files, TS files are checked by TypeScript itself.
'no-unused-vars': ['error', { vars: 'all', args: 'none' }],
},
},
// Node scripts
{
files: [
'scripts/**',
'./*.{js,ts}',
'packages/*/*.js',
'packages/vue/*/*.js',
],
rules: {
'no-restricted-globals': 'off',
'no-restricted-syntax': ['error', banConstEnum],
'no-console': 'off',
},
},
// Import nodejs modules in compiler-sfc
{
files: ['packages/compiler-sfc/src/**'],
rules: {
'import/no-nodejs-modules': ['error', { allow: builtinModules }],
},
},
],
}

View File

@ -6,7 +6,7 @@
Messages must be matched by the following regex:
``` js
```regexp
/^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip)(\(.+\))?: .{1,50}/
```

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.
@ -61,7 +82,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
## Development Setup
You will need [Node.js](https://nodejs.org) **version 18.12+**, and [PNPM](https://pnpm.io) **version 8+**.
You will need [Node.js](https://nodejs.org) with minimum version as specified in the [`.node-version`](https://github.com/vuejs/core/blob/main/.node-version) file, and [PNPM](https://pnpm.io) with minimum version as specified in the [`"packageManager"` field in `package.json`](https://github.com/vuejs/core/blob/main/package.json#L4).
We also recommend installing [@antfu/ni](https://github.com/antfu/ni) to help switching between repos using different package managers. `ni` also provides the handy `nr` command which running npm scripts easier.

View File

@ -28,13 +28,13 @@
},
{
groupName: 'build',
matchPackageNames: ['vite', 'terser'],
matchPackageNames: ['vite', '@swc/core'],
matchPackagePrefixes: ['rollup', 'esbuild', '@rollup', '@vitejs'],
},
{
groupName: 'lint',
matchPackageNames: ['simple-git-hooks', 'lint-staged'],
matchPackagePrefixes: ['@typescript-eslint', 'eslint', 'prettier'],
matchPackagePrefixes: ['typescript-eslint', 'eslint', 'prettier'],
},
],
ignoreDeps: [

View File

@ -14,13 +14,14 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Set node version to 18
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- run: pnpm install
@ -30,4 +31,4 @@ jobs:
- name: Run prettier
run: pnpm run format
- uses: autofix-ci/action@ea32e3a12414e6d3183163c3424a7d7a8631ad84
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c

View File

@ -17,12 +17,12 @@ jobs:
ref: minor
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Set node version to 18
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'

View File

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

View File

@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4
@ -43,7 +43,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4
@ -72,7 +72,7 @@ jobs:
key: chromium-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4
@ -98,7 +98,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4

View File

@ -24,4 +24,5 @@ jobs:
with:
tag_name: ${{ github.ref }}
body: |
Please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/main/CHANGELOG.md) for details.
For stable releases, please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/main/CHANGELOG.md) for details.
For pre-releases, please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/minor/CHANGELOG.md) of the `minor` branch.

View File

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

View File

@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3.0.0
uses: pnpm/action-setup@v4.0.0
- name: Install Node.js
uses: actions/setup-node@v4
@ -36,26 +36,27 @@ jobs:
run: pnpm install
- name: Download PR number
uses: dawidd6/action-download-artifact@v3
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: ./pr.txt
path: /tmp/pr-number/pr.txt
- name: Download Size Data
uses: dawidd6/action-download-artifact@v3
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@v3
uses: dawidd6/action-download-artifact@v6
with:
branch: main
workflow: size-data.yml
@ -64,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

View File

@ -1,3 +1,239 @@
## [3.4.35](https://github.com/vuejs/core/compare/v3.4.34...v3.4.35) (2024-07-31)
### Bug Fixes
* **teleport/ssr:** fix Teleport hydration regression due to targetStart anchor addition ([7b18cdb](https://github.com/vuejs/core/commit/7b18cdb0b53a94007ca6a3675bf41b5d3153fec6))
* **teleport/ssr:** ensure targetAnchor and targetStart not null during hydration ([#11456](https://github.com/vuejs/core/issues/11456)) ([12667da](https://github.com/vuejs/core/commit/12667da4879f980dcf2c50e36f3642d085a87d71)), closes [#11400](https://github.com/vuejs/core/issues/11400)
* **types/ref:** allow getter and setter types to be unrelated ([#11442](https://github.com/vuejs/core/issues/11442)) ([e0b2975](https://github.com/vuejs/core/commit/e0b2975ef65ae6a0be0aa0a0df43fb887c665251))
### Performance Improvements
* **runtime-core:** improve efficiency of normalizePropsOptions ([#11409](https://github.com/vuejs/core/issues/11409)) ([5680142](https://github.com/vuejs/core/commit/5680142e68096c42e66da9f4c6220d040d7c56ba)), closes [#9739](https://github.com/vuejs/core/issues/9739)
## [3.4.34](https://github.com/vuejs/core/compare/v3.4.33...v3.4.34) (2024-07-24)
### Bug Fixes
* **defineModel:** correct update with multiple changes in same tick ([#11430](https://github.com/vuejs/core/issues/11430)) ([a18f1ec](https://github.com/vuejs/core/commit/a18f1ecf05842337f1eb39a6871adb8cb4024093)), closes [#11429](https://github.com/vuejs/core/issues/11429)
## [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
* **compiler-core:** should not remove slot node with `v-else` ([#11150](https://github.com/vuejs/core/issues/11150)) ([e102670](https://github.com/vuejs/core/commit/e102670bde00417c3a5b0262c855b297c0e4169e))
* **hydration:** fix css vars hydration mismatch false positive on attr-fallthrough ([#11190](https://github.com/vuejs/core/issues/11190)) ([7ad67ce](https://github.com/vuejs/core/commit/7ad67ced26e5f53a47cb42f4834496e4958cb53b)), closes [#11188](https://github.com/vuejs/core/issues/11188)
* **hydration:** skip prop mismatch check for directives that mutate DOM in created ([3169c91](https://github.com/vuejs/core/commit/3169c914939d02a013b2938aff30dac8525923f8)), closes [#11189](https://github.com/vuejs/core/issues/11189)
* **reactivity:** fix side effect computed dirty level ([#11183](https://github.com/vuejs/core/issues/11183)) ([3bd79e3](https://github.com/vuejs/core/commit/3bd79e3e5ed960fc42cbf77bc61a97d2c03557c0)), closes [#11181](https://github.com/vuejs/core/issues/11181) [#11169](https://github.com/vuejs/core/issues/11169)
* **runtime-core:** ensure unmount dynamic components in optimized mode ([#11171](https://github.com/vuejs/core/issues/11171)) ([220fe24](https://github.com/vuejs/core/commit/220fe247484209e62c7f4991902c5335e29c5007)), closes [#11168](https://github.com/vuejs/core/issues/11168)
* **runtime-core:** update devtool __vnode on patch, avoid memory leak during dev ([a959781](https://github.com/vuejs/core/commit/a959781dd6f609dcb6f16dd7fa47d3b16895e5ca)), closes [#11192](https://github.com/vuejs/core/issues/11192)
* **runtime-dom:** ensure only symbols are explicitly stringified during attribute patching ([#11182](https://github.com/vuejs/core/issues/11182)) ([a2e35d6](https://github.com/vuejs/core/commit/a2e35d682db15a592f4270bb0cde70a0e7bdc4a6)), closes [#11177](https://github.com/vuejs/core/issues/11177)
* **runtime-dom:** prevent setting state as attribute for custom elements ([#11165](https://github.com/vuejs/core/issues/11165)) ([8ae4c29](https://github.com/vuejs/core/commit/8ae4c293adcec28f18114cb6016230a86787e6a9)), closes [#11163](https://github.com/vuejs/core/issues/11163)
### Performance Improvements
* **reactivity:** cache tracking value ([#11145](https://github.com/vuejs/core/issues/11145)) ([7936dae](https://github.com/vuejs/core/commit/7936daebceab2ae9461c3b8f256e51020fb7d3ed))
## [3.4.29](https://github.com/vuejs/core/compare/v3.4.28...v3.4.29) (2024-06-14)
### Bug Fixes
* **build:** fix accidental inclusion of runtime-core in server-renderer cjs build ([11cc12b](https://github.com/vuejs/core/commit/11cc12b915edfe0e4d3175e57464f73bc2c1cb04)), closes [#11137](https://github.com/vuejs/core/issues/11137)
* **compiler-sfc:** fix missing scope for extends error message ([4ec387b](https://github.com/vuejs/core/commit/4ec387b100985b008cdcc4cd883a5b6328c05766))
* **compiler-sfc:** fix parsing of mts, d.mts, and mtsx files ([a476692](https://github.com/vuejs/core/commit/a476692ed2d7308f2742d8ff3554cf97a392b0b7))
* **compiler-sfc:** support [@vue-ignore](https://github.com/vue-ignore) comment on more type sources ([a23e99b](https://github.com/vuejs/core/commit/a23e99bedf1d65841d162951f10ce35b907a5680))
* **custom-element:** support same direct setup function signature in defineCustomElement ([7c8b126](https://github.com/vuejs/core/commit/7c8b12620aad4969b8dc4944d4fc486d16c3033c)), closes [#11116](https://github.com/vuejs/core/issues/11116)
* **reactivity:** avoid infinite loop when render access a side effect computed ([#11135](https://github.com/vuejs/core/issues/11135)) ([8296e19](https://github.com/vuejs/core/commit/8296e19855e369a7826f5ea26540a6da01dc7093)), closes [#11121](https://github.com/vuejs/core/issues/11121)
## [3.4.28](https://github.com/vuejs/core/compare/v3.4.27...v3.4.28) (2024-06-14)
### Bug Fixes
* **compat:** correctly transform non-identifier expressions in legacy filter syntax ([#10896](https://github.com/vuejs/core/issues/10896)) ([07b3c4b](https://github.com/vuejs/core/commit/07b3c4b7860009e19446f3d78571556c5737d82a)), closes [#10852](https://github.com/vuejs/core/issues/10852)
* **compat:** ensure proper handling of render fuction from SFC using Vue.extend ([#7781](https://github.com/vuejs/core/issues/7781)) ([c73847f](https://github.com/vuejs/core/commit/c73847f2becc20f03cb9c68748eea92455e688ee)), closes [#7766](https://github.com/vuejs/core/issues/7766)
* **compat:** only warn ATTR_FALSE_VALUE when enabled ([04729ba](https://github.com/vuejs/core/commit/04729ba2163d840f0ca7866bc964696eb5557804)), closes [#11126](https://github.com/vuejs/core/issues/11126)
* **compile-sfc:** register props destructure rest id as setup bindings ([#10888](https://github.com/vuejs/core/issues/10888)) ([b2b5f57](https://github.com/vuejs/core/commit/b2b5f57c2c945edd0eebc1b545ec1b7568e51484)), closes [#10885](https://github.com/vuejs/core/issues/10885)
* **compile-sfc:** Support project reference with folder, ([#10908](https://github.com/vuejs/core/issues/10908)) ([bdeac37](https://github.com/vuejs/core/commit/bdeac377c7b85888193b49ac187e927636cc40bc)), closes [#10907](https://github.com/vuejs/core/issues/10907)
* **compiler-core:** allow unicode to appear in simple identifiers ([#6765](https://github.com/vuejs/core/issues/6765)) ([3ea9644](https://github.com/vuejs/core/commit/3ea964473d3ac0ba3e7b0b2c22d71f23d0f69123)), closes [#6367](https://github.com/vuejs/core/issues/6367)
* **compiler-core:** change v-for key type to match Object.keys ([#10963](https://github.com/vuejs/core/issues/10963)) ([9fead52](https://github.com/vuejs/core/commit/9fead5234320848f8be82275c6b5dd0a290f2cca)), closes [#8819](https://github.com/vuejs/core/issues/8819)
* **compiler-core:** emit TS-compatible function declaration when requested ([#9363](https://github.com/vuejs/core/issues/9363)) ([5d25850](https://github.com/vuejs/core/commit/5d258502a0faffc8a451b8701f13a31b2566d068))
* **compiler-core:** fix :key shorthand on v-for ([#10942](https://github.com/vuejs/core/issues/10942)) ([29425df](https://github.com/vuejs/core/commit/29425df1acb9e520c6ae894d06bcff73fde90edd)), closes [#10882](https://github.com/vuejs/core/issues/10882) [#10939](https://github.com/vuejs/core/issues/10939)
* **compiler-core:** make `ForIteratorExpression`'s `returns` property optional ([#11011](https://github.com/vuejs/core/issues/11011)) ([5b8c1af](https://github.com/vuejs/core/commit/5b8c1afb74e39045fcb53a011420d26e3f67eab4))
* **compiler-core:** should set `<math>` tag as block to retain MathML namespace after patching ([#10891](https://github.com/vuejs/core/issues/10891)) ([87c5443](https://github.com/vuejs/core/commit/87c54430448005294c41803f07f517fef848f917))
* **compiler-core:** v-for expression missing source with spaces should emit error ([#5821](https://github.com/vuejs/core/issues/5821)) ([b9ca202](https://github.com/vuejs/core/commit/b9ca202f477be595477e182972ee9bae3f2b9f74)), closes [#5819](https://github.com/vuejs/core/issues/5819)
* **compiler-sfc:** improve type resolving for the keyof operator ([#10921](https://github.com/vuejs/core/issues/10921)) ([293cf4e](https://github.com/vuejs/core/commit/293cf4e131b6d4606e1de2cd7ea87814e2544952)), closes [#10920](https://github.com/vuejs/core/issues/10920) [#11002](https://github.com/vuejs/core/issues/11002)
* **compiler-sfc:** support as keyword with template literal types ([#11100](https://github.com/vuejs/core/issues/11100)) ([2594b1d](https://github.com/vuejs/core/commit/2594b1df57f672ac6621ac2880645e975fea581c)), closes [#10962](https://github.com/vuejs/core/issues/10962)
* **compiler-sfc:** support type resolve for keyof for intersection & union types ([#11132](https://github.com/vuejs/core/issues/11132)) ([495263a](https://github.com/vuejs/core/commit/495263a9cb356861e58a4364f2570608265486b5)), closes [#11129](https://github.com/vuejs/core/issues/11129)
* **compiler-sfc:** throw error when import macro as alias ([#11041](https://github.com/vuejs/core/issues/11041)) ([34a97ed](https://github.com/vuejs/core/commit/34a97edd2c8273c213599c44770accdb0846da8e))
* correct the type of `<details>`'s `onToggle` event handler ([#10938](https://github.com/vuejs/core/issues/10938)) ([fd18ce7](https://github.com/vuejs/core/commit/fd18ce70b1a260a2485c9cd7faa30193da4b79f5)), closes [#10928](https://github.com/vuejs/core/issues/10928)
* **custom-element:** disconnect MutationObserver in nextTick in case that custom elements are moved ([#10613](https://github.com/vuejs/core/issues/10613)) ([bbb5be2](https://github.com/vuejs/core/commit/bbb5be299b500a00e60c757118c846c3b5ddd8e0)), closes [#10610](https://github.com/vuejs/core/issues/10610)
* **custom-elements:** compatibility of createElement in older versions of Chrome ([#9615](https://github.com/vuejs/core/issues/9615)) ([a88295d](https://github.com/vuejs/core/commit/a88295dc076ee867939d8b0ee2225e63c5ffb0ca)), closes [#9614](https://github.com/vuejs/core/issues/9614)
* **hmr:** avoid infinite recursion when reloading hmr components ([#6936](https://github.com/vuejs/core/issues/6936)) ([36bd9b0](https://github.com/vuejs/core/commit/36bd9b0a1fb83e61731fb80d66e265dccbedcfa8)), closes [#6930](https://github.com/vuejs/core/issues/6930)
* **hydration:** log hydration error even when using async components ([#9403](https://github.com/vuejs/core/issues/9403)) ([5afc76c](https://github.com/vuejs/core/commit/5afc76c229f9ad30eef07f34c7b65e8fe427e637)), closes [#9369](https://github.com/vuejs/core/issues/9369)
* **KeepAlive:** properly cache nested Suspense subtree ([#10912](https://github.com/vuejs/core/issues/10912)) ([07764fe](https://github.com/vuejs/core/commit/07764fe330692fadf0fc9fb9e92cb5b111df33be))
* **npm:** explicitly add `@vue/reactivity` as dependency of `@vue/runtime-dom` ([#10468](https://github.com/vuejs/core/issues/10468)) ([ec424f6](https://github.com/vuejs/core/commit/ec424f6cd96b7e6ba74fc244c484c00fa5590aac))
* **reactivity:** pass oldValue in debug info when triggering refs ([#8210](https://github.com/vuejs/core/issues/8210)) ([3b0a56a](https://github.com/vuejs/core/commit/3b0a56a9c4d162ec3bd725a4f2dfd776b045e727)), closes [vuejs/pinia#2061](https://github.com/vuejs/pinia/issues/2061)
* **runtime-core:** avoid traversing static children for vnodes w/ PatchFlags.BAIL ([#11115](https://github.com/vuejs/core/issues/11115)) ([b557d3f](https://github.com/vuejs/core/commit/b557d3fb8ae1e4e926c4ad0fbb2fa7abe50fd661)), closes [#10547](https://github.com/vuejs/core/issues/10547)
* **runtime-core:** do not fire mount/activated hooks if unmounted before mounted ([#9370](https://github.com/vuejs/core/issues/9370)) ([aa156ed](https://github.com/vuejs/core/commit/aa156ed5c4dc0d33ff37e201a7e89d5e0e29160e)), closes [#8898](https://github.com/vuejs/core/issues/8898) [#9264](https://github.com/vuejs/core/issues/9264) [#9617](https://github.com/vuejs/core/issues/9617)
* **runtime-core:** ensure suspense creates dep component's render effect with correct optimized flag ([#7689](https://github.com/vuejs/core/issues/7689)) ([c521f95](https://github.com/vuejs/core/commit/c521f956e1697cda36a7f1b913599e5e2004f7ba)), closes [#7688](https://github.com/vuejs/core/issues/7688)
* **runtime-core:** fix missed updates when passing text vnode to `<component :is>` ([#8304](https://github.com/vuejs/core/issues/8304)) ([b310ec3](https://github.com/vuejs/core/commit/b310ec389d9738247e5b0f01711186216eb49955)), closes [#8298](https://github.com/vuejs/core/issues/8298)
* **runtime-core:** fix stale v-memo after v-if toggle ([#6606](https://github.com/vuejs/core/issues/6606)) ([edf2638](https://github.com/vuejs/core/commit/edf263847eddc910f4d2de68287d84b8c66c3860)), closes [#6593](https://github.com/vuejs/core/issues/6593)
* **runtime-core:** fix Transition for components with root-level v-if ([#7678](https://github.com/vuejs/core/issues/7678)) ([ef2e737](https://github.com/vuejs/core/commit/ef2e737577de42ea38771403f8a4dee8c892daa5)), closes [#7649](https://github.com/vuejs/core/issues/7649)
* **runtime-dom:** also set attribute for form element state ([537a571](https://github.com/vuejs/core/commit/537a571f8cf09dfe0a020e9e8891ecdd351fc3e4)), closes [#6007](https://github.com/vuejs/core/issues/6007) [#6012](https://github.com/vuejs/core/issues/6012)
* **runtime-dom:** support Symbol for input value bindings ([#10608](https://github.com/vuejs/core/issues/10608)) ([188f3ae](https://github.com/vuejs/core/commit/188f3ae533fd340603068a516a8fecc5d57426c5)), closes [#10597](https://github.com/vuejs/core/issues/10597)
* **shared:** ensure invokeArrayFns handles undefined arguments ([#10869](https://github.com/vuejs/core/issues/10869)) ([9b40d0f](https://github.com/vuejs/core/commit/9b40d0f25da868a83b0d6bf99dbbdb3ca68bb700)), closes [#10863](https://github.com/vuejs/core/issues/10863)
* **ssr:** directive binding.instance should respect exposed during ssr ([df686ab](https://github.com/vuejs/core/commit/df686abb4f0ac9d898e4fd93751e860f8cbbdbea)), closes [#7499](https://github.com/vuejs/core/issues/7499) [#7502](https://github.com/vuejs/core/issues/7502)
* **ssr:** fix hydration for node with empty text node ([#7216](https://github.com/vuejs/core/issues/7216)) ([d1011c0](https://github.com/vuejs/core/commit/d1011c07a957d858cb37725b13bc8e4d7a395490))
* **ssr:** fix the bug that multi slot scope id does not work on component ([#6100](https://github.com/vuejs/core/issues/6100)) ([4c74302](https://github.com/vuejs/core/commit/4c74302aae64c118752db7fc2a2c229a11ebaead)), closes [#6093](https://github.com/vuejs/core/issues/6093)
* **teleport:** do not throw target warning when teleport is disabled ([#9818](https://github.com/vuejs/core/issues/9818)) ([15ee43f](https://github.com/vuejs/core/commit/15ee43f66ad2485ac212b02b444345d867b3c060))
* **transition:** ensure Transition enterHooks are updated after clone ([#11066](https://github.com/vuejs/core/issues/11066)) ([671cf29](https://github.com/vuejs/core/commit/671cf297a550d15b19fa3fecce1b30e26cad8154)), closes [#11061](https://github.com/vuejs/core/issues/11061)
* **types/apiWatch:** correct type inference for reactive array ([#11036](https://github.com/vuejs/core/issues/11036)) ([aae2d78](https://github.com/vuejs/core/commit/aae2d78875daa476280a45e71c2f38292964efae)), closes [#9416](https://github.com/vuejs/core/issues/9416)
* **types:** improve `app.provide` type checking ([#10603](https://github.com/vuejs/core/issues/10603)) ([612bbf0](https://github.com/vuejs/core/commit/612bbf0507cbe39d701acc5dff11824802078063)), closes [#10602](https://github.com/vuejs/core/issues/10602)
* **types:** support generic argument in setup context expose method ([#8507](https://github.com/vuejs/core/issues/8507)) ([635a59b](https://github.com/vuejs/core/commit/635a59b96fe6be445525c6595ca27da7ef7c1feb))
* **v-model:** fix the lazy modifier is not reset by other modifications ([#8547](https://github.com/vuejs/core/issues/8547)) ([a52a02f](https://github.com/vuejs/core/commit/a52a02f43fdf73d8aaad99c9cafed07f12ee422a)), closes [#8546](https://github.com/vuejs/core/issues/8546) [#6564](https://github.com/vuejs/core/issues/6564) [#6773](https://github.com/vuejs/core/issues/6773)
* **watch:** support traversing symbol properties in deep watcher ([#10969](https://github.com/vuejs/core/issues/10969)) ([a3e8aaf](https://github.com/vuejs/core/commit/a3e8aafbcc82003a66caded61143eb64c4ef02cd)), closes [#402](https://github.com/vuejs/core/issues/402)
## [3.4.27](https://github.com/vuejs/core/compare/v3.4.26...v3.4.27) (2024-05-06)
### Bug Fixes
* **compat:** include legacy scoped slots ([#10868](https://github.com/vuejs/core/issues/10868)) ([8366126](https://github.com/vuejs/core/commit/83661264a4ced3cb2ff6800904a86dd9e82bbfe2)), closes [#8869](https://github.com/vuejs/core/issues/8869)
* **compiler-core:** add support for arrow aysnc function with unbracketed ([#5789](https://github.com/vuejs/core/issues/5789)) ([ca7d421](https://github.com/vuejs/core/commit/ca7d421e8775f6813f8943d32ab485e0c542f98b)), closes [#5788](https://github.com/vuejs/core/issues/5788)
* **compiler-dom:** restrict createStaticVNode usage with option elements ([#10846](https://github.com/vuejs/core/issues/10846)) ([0e3d617](https://github.com/vuejs/core/commit/0e3d6178b02d0386d779720ae2cc4eac1d1ec990)), closes [#6568](https://github.com/vuejs/core/issues/6568) [#7434](https://github.com/vuejs/core/issues/7434)
* **compiler-sfc:** handle keyof operator ([#10874](https://github.com/vuejs/core/issues/10874)) ([10d34a5](https://github.com/vuejs/core/commit/10d34a5624775f20437ccad074a97270ef74c3fb)), closes [#10871](https://github.com/vuejs/core/issues/10871)
* **hydration:** handle edge case of style mismatch without style attribute ([f2c1412](https://github.com/vuejs/core/commit/f2c1412e46a8fad3e13403bfa78335c4f704f21c)), closes [#10786](https://github.com/vuejs/core/issues/10786)
## [3.4.26](https://github.com/vuejs/core/compare/v3.4.25...v3.4.26) (2024-04-29)
### Bug Fixes
* **compiler-core:** fix bail constant for globals ([fefce06](https://github.com/vuejs/core/commit/fefce06b41e3b75de3d748dc6399628ec5056e78))
* **compiler-core:** remove unnecessary constant bail check ([09b4df8](https://github.com/vuejs/core/commit/09b4df809e59ef5f4bc91acfc56dc8f82a8e243a)), closes [#10807](https://github.com/vuejs/core/issues/10807)
* **runtime-core:** attrs should be readonly in functional components ([#10767](https://github.com/vuejs/core/issues/10767)) ([e8fd644](https://github.com/vuejs/core/commit/e8fd6446d14a6899e5e8ab1ee394d90088e01844))
* **runtime-core:** ensure slot compiler marker writable ([#10825](https://github.com/vuejs/core/issues/10825)) ([9c2de62](https://github.com/vuejs/core/commit/9c2de6244cd44bc5fbfd82b5850c710ce725044f)), closes [#10818](https://github.com/vuejs/core/issues/10818)
* **runtime-core:** properly handle inherit transition during clone VNode ([#10809](https://github.com/vuejs/core/issues/10809)) ([638a79f](https://github.com/vuejs/core/commit/638a79f64a7e184f2a2c65e21d764703f4bda561)), closes [#3716](https://github.com/vuejs/core/issues/3716) [#10497](https://github.com/vuejs/core/issues/10497) [#4091](https://github.com/vuejs/core/issues/4091)
* **Transition:** re-fix [#10620](https://github.com/vuejs/core/issues/10620) ([#10832](https://github.com/vuejs/core/issues/10832)) ([accf839](https://github.com/vuejs/core/commit/accf8396ae1c9dd49759ba0546483f1d2c70c9bc)), closes [#10632](https://github.com/vuejs/core/issues/10632) [#10827](https://github.com/vuejs/core/issues/10827)
## [3.4.25](https://github.com/vuejs/core/compare/v3.4.24...v3.4.25) (2024-04-24)
### Bug Fixes
* **defineModel:** align prod mode runtime type generation with defineProps ([4253a57](https://github.com/vuejs/core/commit/4253a57f1703a7f1ac701d77e0a235689203461d)), closes [#10769](https://github.com/vuejs/core/issues/10769)
* **runtime-core:** properly get keepAlive child ([#10772](https://github.com/vuejs/core/issues/10772)) ([3724693](https://github.com/vuejs/core/commit/3724693a25c3f2dd13d70a8a1af760b03a4fb783)), closes [#10771](https://github.com/vuejs/core/issues/10771)
* **runtime-core:** use normal object as internal prototype for attrs and slots ([064e82f](https://github.com/vuejs/core/commit/064e82f5855f30fe0b77fe9b5e4dd22700fd634d)), closes [/github.com/vuejs/core/commit/6df53d85a207986128159d88565e6e7045db2add#r141304923](https://github.com//github.com/vuejs/core/commit/6df53d85a207986128159d88565e6e7045db2add/issues/r141304923)
## [3.4.24](https://github.com/vuejs/core/compare/v3.4.23...v3.4.24) (2024-04-22)
### Bug Fixes
* **compiler-core:** handle template ref bound via v-bind object on v-for ([#10706](https://github.com/vuejs/core/issues/10706)) ([da7adef](https://github.com/vuejs/core/commit/da7adefa844265eecc9c336abfc727bc05b4f16e)), closes [#10696](https://github.com/vuejs/core/issues/10696)
* **compiler-core:** properly parse await expressions in edge cases ([b92c25f](https://github.com/vuejs/core/commit/b92c25f53dff0fc1687f57ca4033d0ac25218940)), closes [#10754](https://github.com/vuejs/core/issues/10754)
* **compiler-sfc:** handle readonly operator and ReadonlyArray/Map/Set types ([5cef52a](https://github.com/vuejs/core/commit/5cef52a5c23ba8ba3239e6def03b8ff008d3cc72)), closes [#10726](https://github.com/vuejs/core/issues/10726)
* **compiler-ssr:** fix hydration mismatch for conditional slot in transition ([f12c81e](https://github.com/vuejs/core/commit/f12c81efca3fcf9a7ce478af2261ad6ab9b0bfd7)), closes [#10743](https://github.com/vuejs/core/issues/10743)
* **compiler-ssr:** fix v-html SSR for nullish values ([1ff4076](https://github.com/vuejs/core/commit/1ff407676f9495883b459779a9b0370d7588b51f)), closes [#10725](https://github.com/vuejs/core/issues/10725)
* **deps:** update compiler ([#10760](https://github.com/vuejs/core/issues/10760)) ([15df5c1](https://github.com/vuejs/core/commit/15df5c1b261b9b471eb811fd47ab7b3cfc41cf83))
* **runtime-core:** fix edge case of KeepAlive inside Transition with slot children ([#10719](https://github.com/vuejs/core/issues/10719)) ([e51ca61](https://github.com/vuejs/core/commit/e51ca61ca060b2772e967d169548fc2f58fce6d1)), closes [#10708](https://github.com/vuejs/core/issues/10708)
* **runtime-core:** further fix slots _ctx check ([cde7f05](https://github.com/vuejs/core/commit/cde7f05787d16dbb93d9419ef5331adf992816fd)), closes [#10724](https://github.com/vuejs/core/issues/10724)
* **runtime-core:** props should be readonly via direct template access ([b93f264](https://github.com/vuejs/core/commit/b93f26464785de227b88c51a88328ae80e80d804)), closes [#8216](https://github.com/vuejs/core/issues/8216) [#10736](https://github.com/vuejs/core/issues/10736)
* **transition:** transition is breaking/flickering when enter is canceled ([#10688](https://github.com/vuejs/core/issues/10688)) ([65109a7](https://github.com/vuejs/core/commit/65109a70f187473edae8cf4df11af3c33345e6f6))
## [3.4.23](https://github.com/vuejs/core/compare/v3.4.22...v3.4.23) (2024-04-16)
### Bug Fixes
* **runtime-core:** fix regression for $attrs tracking in slots ([6930e60](https://github.com/vuejs/core/commit/6930e60787e4905a50417190263ae7dd46cf5409)), closes [#10710](https://github.com/vuejs/core/issues/10710)
* **runtime-core:** use same internal object mechanism for slots ([6df53d8](https://github.com/vuejs/core/commit/6df53d85a207986128159d88565e6e7045db2add)), closes [#10709](https://github.com/vuejs/core/issues/10709)
## [3.4.22](https://github.com/vuejs/core/compare/v3.4.21...v3.4.22) (2024-04-15)

View File

@ -5,3 +5,11 @@ To report a vulnerability, please email security@vuejs.org.
While the discovery of new vulnerabilities is rare, we also recommend always using the latest versions of Vue and its official companion libraries to ensure your application remains as secure as possible.
Please note that we do not consider XSS via template expressions a valid attack vector, because it can only happen if the user intentionally uses untrusted content as template compilation source. This is similar to knowingly pasting untrusted scripts into a browser console. We explicitly warn users against using untrusted content as template compilation source in our documentation.
## Security Hall of Fame
We would like to thank the following security researchers for responsibly disclosing security issues to us.
- Jeet Pal - [@jeetpal2007](https://github.com/jeetpal2007) | [Email](jeetpal2007@gmail.com) | [LinkedIn](https://in.linkedin.com/in/jeet-pal-22601a290 )
- Mix - [@mnixry](https://github.com/mnixry)
- Aviv Keller - [@RedYetiDev](https://github.com/redyetidev) | [LinkedIn](https://www.linkedin.com/in/redyetidev) <redyetidev@gmail.com>

174
eslint.config.js Normal file
View File

@ -0,0 +1,174 @@
import importX from 'eslint-plugin-import-x'
import tseslint from 'typescript-eslint'
import vitest from 'eslint-plugin-vitest'
import { builtinModules } from 'node:module'
const DOMGlobals = ['window', 'document']
const NodeGlobals = ['module', 'require']
const banConstEnum = {
selector: 'TSEnumDeclaration[const=true]',
message:
'Please use non-const enums. This project automatically inlines enums.',
}
export default tseslint.config(
{
files: ['**/*.js', '**/*.ts', '**/*.tsx'],
extends: [tseslint.configs.base],
plugins: {
'import-x': importX,
},
rules: {
'no-debugger': 'error',
'no-console': ['error', { allow: ['warn', 'error', 'info'] }],
// most of the codebase are expected to be env agnostic
'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals],
'no-restricted-syntax': [
'error',
banConstEnum,
{
selector: 'ObjectPattern > RestElement',
message:
'Our output target is ES2016, and object rest spread results in ' +
'verbose helpers and should be avoided.',
},
{
selector: 'ObjectExpression > SpreadElement',
message:
'esbuild transpiles object spread into very verbose inline helpers.\n' +
'Please use the `extend` helper from @vue/shared instead.',
},
{
selector: 'AwaitExpression',
message:
'Our output target is ES2016, so async/await syntax should be avoided.',
},
{
selector: 'ChainExpression',
message:
'Our output target is ES2016, and optional chaining results in ' +
'verbose helpers and should be avoided.',
},
],
'sort-imports': ['error', { ignoreDeclarationSort: true }],
'import-x/no-nodejs-modules': [
'error',
{ allow: builtinModules.map(mod => `node:${mod}`) },
],
// This rule enforces the preference for using '@ts-expect-error' comments in TypeScript
// code to indicate intentional type errors, improving code clarity and maintainability.
'@typescript-eslint/prefer-ts-expect-error': 'error',
// Enforce the use of 'import type' for importing types
'@typescript-eslint/consistent-type-imports': [
'error',
{
fixStyle: 'inline-type-imports',
disallowTypeAnnotations: false,
},
],
// Enforce the use of top-level import type qualifier when an import only has specifiers with inline type qualifiers
'@typescript-eslint/no-import-type-side-effects': 'error',
},
},
// tests, no restrictions (runs in Node / Vitest with jsdom)
{
files: ['**/__tests__/**', 'packages/dts-test/**'],
plugins: { vitest },
languageOptions: {
globals: {
...vitest.environments.env.globals,
},
},
rules: {
'no-console': 'off',
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
'vitest/no-disabled-tests': 'error',
'vitest/no-focused-tests': 'error',
},
},
// shared, may be used in any env
{
files: ['packages/shared/**', 'eslint.config.js'],
rules: {
'no-restricted-globals': 'off',
},
},
// Packages targeting DOM
{
files: ['packages/{vue,vue-compat,runtime-dom}/**'],
rules: {
'no-restricted-globals': ['error', ...NodeGlobals],
},
},
// Packages targeting Node
{
files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
rules: {
'no-restricted-globals': ['error', ...DOMGlobals],
'no-restricted-syntax': ['error', banConstEnum],
},
},
// Private package, browser only + no syntax restrictions
{
files: ['packages/template-explorer/**', 'packages/sfc-playground/**'],
rules: {
'no-restricted-globals': ['error', ...NodeGlobals],
'no-restricted-syntax': ['error', banConstEnum],
'no-console': 'off',
},
},
// JavaScript files
{
files: ['*.js'],
rules: {
// We only do `no-unused-vars` checks for js files, TS files are checked by TypeScript itself.
'no-unused-vars': ['error', { vars: 'all', args: 'none' }],
},
},
// Node scripts
{
files: [
'eslint.config.js',
'rollup*.config.js',
'scripts/**',
'./*.{js,ts}',
'packages/*/*.js',
'packages/vue/*/*.js',
],
rules: {
'no-restricted-globals': 'off',
'no-restricted-syntax': ['error', banConstEnum],
'no-console': 'off',
},
},
// Import nodejs modules in compiler-sfc
{
files: ['packages/compiler-sfc/src/**'],
rules: {
'import-x/no-nodejs-modules': ['error', { allow: builtinModules }],
},
},
{
ignores: [
'**/dist/',
'**/temp/',
'**/coverage/',
'.idea/',
'explorations/',
'dts-build/packages',
],
},
)

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "3.4.22",
"packageManager": "pnpm@8.15.6",
"version": "3.4.35",
"packageManager": "pnpm@9.6.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@ -13,7 +13,7 @@
"size-esm-runtime": "node scripts/build.js vue -f esm-bundler-runtime",
"size-esm": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler",
"check": "tsc --incremental --noEmit",
"lint": "eslint --cache --ext .js,.ts,.tsx . --ignore-path .gitignore",
"lint": "eslint --cache .",
"format": "prettier --write --cache .",
"format-check": "prettier --check --cache .",
"test": "vitest",
@ -59,60 +59,64 @@
"node": ">=18.12.0"
},
"devDependencies": {
"@babel/parser": "^7.24.1",
"@babel/types": "^7.24.0",
"@codspeed/vitest-plugin": "^3.1.0",
"@babel/parser": "catalog:",
"@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@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",
"@rollup/plugin-terser": "^0.4.4",
"@swc/core": "^1.7.3",
"@types/hash-sum": "^1.0.2",
"@types/minimist": "^1.2.5",
"@types/node": "^20.12.5",
"@types/node": "^20.14.13",
"@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"@vitest/coverage-istanbul": "^1.4.0",
"@vitest/coverage-istanbul": "^1.6.0",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^4.1.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
"esbuild": "^0.20.2",
"esbuild": "^0.23.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^8.57.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-import": "npm:eslint-plugin-i@^2.29.1",
"eslint-plugin-jest": "^27.9.0",
"estree-walker": "^2.0.2",
"execa": "^8.0.1",
"jsdom": "^24.0.0",
"lint-staged": "^15.2.2",
"eslint": "^9.8.0",
"eslint-plugin-import-x": "^3.1.0",
"eslint-plugin-vitest": "^0.5.4",
"estree-walker": "catalog:",
"jsdom": "^24.1.1",
"lint-staged": "^15.2.7",
"lodash": "^4.17.21",
"magic-string": "^0.30.8",
"magic-string": "^0.30.10",
"markdown-table": "^3.0.3",
"marked": "^12.0.1",
"minimist": "^1.2.8",
"npm-run-all2": "^6.1.2",
"picocolors": "^1.0.0",
"prettier": "^3.2.5",
"marked": "^12.0.2",
"npm-run-all2": "^6.2.2",
"picocolors": "^1.0.1",
"prettier": "^3.3.3",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.2",
"puppeteer": "~22.6.3",
"rimraf": "^5.0.5",
"rollup": "^4.13.2",
"rollup-plugin-dts": "^6.1.0",
"pug": "^3.0.3",
"puppeteer": "~22.14.0",
"rimraf": "^5.0.9",
"rollup": "^4.19.1",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.0",
"serve": "^14.2.1",
"semver": "^7.6.3",
"serve": "^14.2.3",
"simple-git-hooks": "^2.11.1",
"terser": "^5.30.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.6.2",
"tsx": "^4.7.2",
"tslib": "^2.6.3",
"tsx": "^4.16.2",
"typescript": "~5.4.5",
"vite": "^5.2.7",
"vitest": "^1.4.0"
"typescript-eslint": "^7.17.0",
"vite": "catalog:",
"vitest": "^1.6.0"
},
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"typescript-eslint>eslint": "^9.0.0",
"@typescript-eslint/eslint-plugin>eslint": "^9.0.0",
"@typescript-eslint/parser>eslint": "^9.0.0",
"@typescript-eslint/type-utils>eslint": "^9.0.0",
"@typescript-eslint/utils>eslint": "^9.0.0"
}
}
}
}

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

@ -16,6 +16,22 @@ export function render(_ctx, _cache) {
}"
`;
exports[`scopeId compiler support > should push typescript-compatible scopeId for hoisted nodes 1`] = `
"import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from "vue"
const _withScopeId = (n: any) => (_pushScopeId("test"),n=n(),_popScopeId(),n)
const _hoisted_1 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "hello", -1 /* HOISTED */))
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "world", -1 /* HOISTED */))
export function render(_ctx: any,_cache: any) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */),
_hoisted_2
]))
}"
`;
exports[`scopeId compiler support > should wrap default slot 1`] = `
"import { createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from "vue"

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: 1,
@ -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

@ -16,8 +16,6 @@ import {
import { baseParse } from '../src/parser'
import type { Program } from '@babel/types'
/* eslint jest/no-disabled-tests: "off" */
describe('compiler: parse', () => {
describe('Text', () => {
test('simple text', () => {

View File

@ -81,4 +81,29 @@ describe('scopeId compiler support', () => {
].forEach(c => expect(code).toMatch(c))
expect(code).toMatchSnapshot()
})
test('should push typescript-compatible scopeId for hoisted nodes', () => {
const { ast, code } = baseCompile(
`<div><div>hello</div>{{ foo }}<div>world</div></div>`,
{
mode: 'module',
scopeId: 'test',
hoistStatic: true,
isTS: true,
},
)
expect(ast.helpers).toContain(PUSH_SCOPE_ID)
expect(ast.helpers).toContain(POP_SCOPE_ID)
expect(ast.hoists.length).toBe(2)
;[
`const _withScopeId = (n: any) => (_pushScopeId("test"),n=n(),_popScopeId(),n)`,
`const _hoisted_1 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "hello", ${genFlagText(
PatchFlags.HOISTED,
)}))`,
`const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "world", ${genFlagText(
PatchFlags.HOISTED,
)}))`,
].forEach(c => expect(code).toMatch(c))
expect(code).toMatchSnapshot()
})
})

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

@ -0,0 +1,228 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-for > codegen > basic v-for 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createElementBlock("span"))
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > keyed template v-for 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createElementBlock(_Fragment, { key: item }, [
"hello",
_createElementVNode("span")
], 64 /* STABLE_FRAGMENT */))
}), 128 /* KEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > keyed v-for 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createElementBlock("span", { key: item }))
}), 128 /* KEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > skipped key 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item, __, index) => {
return (_openBlock(), _createElementBlock("span"))
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > skipped value & key 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (_, __, index) => {
return (_openBlock(), _createElementBlock("span"))
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > skipped value 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (_, key, index) => {
return (_openBlock(), _createElementBlock("span"))
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > template v-for 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
"hello",
_createElementVNode("span")
], 64 /* STABLE_FRAGMENT */))
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > template v-for key injection with single child 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
return (_openBlock(), _createElementBlock("span", {
key: item.id,
id: item.id
}, null, 8 /* PROPS */, ["id"]))
}), 128 /* KEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > template v-for w/ <slot/> 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, renderSlot: _renderSlot } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
return _renderSlot($slots, "default")
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > v-for on <slot/> 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, renderSlot: _renderSlot } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item) => {
return _renderSlot($slots, "default")
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > v-for on element with custom directive 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, resolveDirective: _resolveDirective, withDirectives: _withDirectives } = _Vue
const _directive_foo = _resolveDirective("foo")
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
return _withDirectives((_openBlock(), _createElementBlock("div", null, null, 512 /* NEED_PATCH */)), [
[_directive_foo]
])
}), 256 /* UNKEYED_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > v-for with constant expression 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = _Vue
return (_openBlock(), _createElementBlock(_Fragment, null, _renderList(10, (item) => {
return _createElementVNode("p", null, _toDisplayString(item), 1 /* TEXT */)
}), 64 /* STABLE_FRAGMENT */))
}
}"
`;
exports[`compiler: v-for > codegen > v-if + v-for 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue
return ok
? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock("div"))
}), 256 /* UNKEYED_FRAGMENT */))
: _createCommentVNode("v-if", true)
}
}"
`;
exports[`compiler: v-for > codegen > v-if + v-for on <template> 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue
return ok
? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [], 64 /* STABLE_FRAGMENT */))
}), 256 /* UNKEYED_FRAGMENT */))
: _createCommentVNode("v-if", true)
}
}"
`;
exports[`compiler: v-for > codegen > value + key + index 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item, key, index) => {
return (_openBlock(), _createElementBlock("span"))
}), 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'
@ -180,7 +180,7 @@ describe('compiler: hoistStatic transform', () => {
id: `[foo]`,
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.PROPS),
patchFlag: PatchFlags.PROPS,
dynamicProps: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_1`,
@ -242,7 +242,7 @@ describe('compiler: hoistStatic transform', () => {
ref: `[foo]`,
}),
children: undefined,
patchFlag: genFlagText(PatchFlags.NEED_PATCH),
patchFlag: PatchFlags.NEED_PATCH,
},
},
])
@ -263,7 +263,7 @@ describe('compiler: hoistStatic transform', () => {
content: `_hoisted_1`,
},
children: undefined,
patchFlag: genFlagText(PatchFlags.NEED_PATCH),
patchFlag: PatchFlags.NEED_PATCH,
directives: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
@ -286,7 +286,7 @@ describe('compiler: hoistStatic transform', () => {
tag: `"div"`,
props: { content: `_hoisted_1` },
children: { type: NodeTypes.INTERPOLATION },
patchFlag: genFlagText(PatchFlags.TEXT),
patchFlag: PatchFlags.TEXT,
},
},
])
@ -365,7 +365,7 @@ describe('compiler: hoistStatic 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({
@ -496,7 +496,7 @@ describe('compiler: hoistStatic transform', () => {
constType: ConstantTypes.NOT_CONSTANT,
},
},
patchFlag: `1 /* TEXT */`,
patchFlag: PatchFlags.TEXT,
},
},
],

View File

@ -37,8 +37,9 @@ 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'
function parseWithElementTransform(
template: string,
@ -520,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,
})
}
@ -587,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"`)
})
@ -611,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: [
@ -944,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"]`)
})
@ -972,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"]`)
})
@ -982,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)', () => {
@ -1024,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)', () => {
@ -1119,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" />`,
@ -1129,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
@ -1156,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', () => {
@ -1172,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)
})
})
@ -1283,6 +1276,18 @@ describe('compiler: element transform', () => {
})
})
test('<math> should be forced into blocks', () => {
const ast = parse(`<div><math/></div>`)
transform(ast, {
nodeTransforms: [transformElement],
})
expect((ast as any).children[0].children[0].codegenNode).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"math"`,
isBlock: true,
})
})
test('force block for runtime custom directive w/ children', () => {
const { node } = parseWithElementTransform(`<div v-foo>hello</div>`)
expect(node.isBlock).toBe(true)
@ -1338,4 +1343,42 @@ describe('compiler: element transform', () => {
isBlock: false,
})
})
test('ref_for marker on static ref', () => {
const { node } = parseWithForTransform(`<div v-for="i in l" ref="x"/>`)
expect((node.children[0] as any).codegenNode.props).toMatchObject(
createObjectMatcher({
ref_for: `[true]`,
ref: 'x',
}),
)
})
test('ref_for marker on dynamic ref', () => {
const { node } = parseWithForTransform(`<div v-for="i in l" :ref="x"/>`)
expect((node.children[0] as any).codegenNode.props).toMatchObject(
createObjectMatcher({
ref_for: `[true]`,
ref: '[x]',
}),
)
})
test('ref_for marker on v-bind', () => {
const { node } = parseWithForTransform(`<div v-for="i in l" v-bind="x" />`)
expect((node.children[0] as any).codegenNode.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
createObjectMatcher({
ref_for: `[true]`,
}),
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'x',
isStatic: false,
},
],
})
})
})

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 }}`,
@ -421,6 +432,31 @@ describe('compiler: expression transform', () => {
})
})
// #10807
test('should not bail constant on strings w/ ()', () => {
const node = parseWithExpressionTransform(
`{{ { foo: 'ok()' } }}`,
) as InterpolationNode
expect(node.content).toMatchObject({
constType: ConstantTypes.CAN_STRINGIFY,
})
})
test('should bail constant for global identifiers w/ new or call expressions', () => {
const node = parseWithExpressionTransform(
`{{ new Date().getFullYear() }}`,
) as InterpolationNode
expect(node.content).toMatchObject({
children: [
'new ',
{ constType: ConstantTypes.NOT_CONSTANT },
'().',
{ constType: ConstantTypes.NOT_CONSTANT },
'()',
],
})
})
describe('ES Proposals support', () => {
test('bigInt', () => {
const node = parseWithExpressionTransform(
@ -598,5 +634,33 @@ describe('compiler: expression transform', () => {
`${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */`,
)
})
// #10754
test('await expression in right hand of assignment, inline mode', () => {
const node = parseWithExpressionTransform(
`{{ (async () => { x = await bar })() }}`,
{
inline: true,
bindingMetadata: {
x: BindingTypes.SETUP_LET,
bar: BindingTypes.SETUP_CONST,
},
},
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`(async () => { `,
{
content: `_isRef(x) ? x.value = await bar : x`,
},
` = await `,
{
content: `bar`,
},
` })()`,
],
})
})
})
})

View File

@ -19,9 +19,9 @@ import { ErrorCodes } from '../../src/errors'
import { type CompilerOptions, generate } from '../../src'
import { FRAGMENT, RENDER_LIST, RENDER_SLOT } from '../../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared'
import { createObjectMatcher, genFlagText } from '../testUtils'
import { createObjectMatcher } from '../testUtils'
function parseWithForTransform(
export function parseWithForTransform(
template: string,
options: CompilerOptions = {},
) {
@ -202,6 +202,18 @@ describe('compiler: v-for', () => {
expect(forNode.valueAlias).toBeUndefined()
expect((forNode.source as SimpleExpressionNode).content).toBe('items')
})
test('source containing string expression with spaces', () => {
const { node: forNode } = parseWithForTransform(
`<span v-for="item in state ['my items']" />`,
)
expect(forNode.keyAlias).toBeUndefined()
expect(forNode.objectIndexAlias).toBeUndefined()
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
expect((forNode.source as SimpleExpressionNode).content).toBe(
"state ['my items']",
)
})
})
describe('errors', () => {
@ -253,6 +265,18 @@ describe('compiler: v-for', () => {
)
})
test('missing source and have multiple spaces with', () => {
const onError = vi.fn()
parseWithForTransform('<span v-for="item in " />', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
}),
)
})
test('missing value', () => {
const onError = vi.fn()
parseWithForTransform('<span v-for="in items" />', { onError })
@ -672,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,
@ -798,7 +822,7 @@ describe('compiler: v-for', () => {
constType: ConstantTypes.NOT_CONSTANT,
},
},
patchFlag: genFlagText(PatchFlags.TEXT),
patchFlag: PatchFlags.TEXT,
},
})
expect(generate(root).code).toMatchSnapshot()
@ -822,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()
@ -926,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()
@ -947,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,
@ -985,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,
@ -1019,5 +1043,31 @@ describe('compiler: v-for', () => {
})
expect(generate(root).code).toMatchSnapshot()
})
test('template v-for key w/ :key shorthand on div', () => {
const {
node: { codegenNode },
} = parseWithForTransform('<div v-for="key in keys" :key>test</div>')
expect(codegenNode.patchFlag).toBe(PatchFlags.KEYED_FRAGMENT)
})
test('template v-for key w/ :key shorthand on template injected to the child', () => {
const {
node: { codegenNode },
} = parseWithForTransform(
'<template v-for="key in keys" :key><div>test</div></template>',
)
expect(assertSharedCodegen(codegenNode, true)).toMatchObject({
source: { content: `keys` },
params: [{ content: `key` }],
innerVNodeCall: {
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: createObjectMatcher({
key: '[key]',
}),
},
})
})
})
})

View File

@ -10,6 +10,7 @@ import {
baseParse as parse,
transform,
} from '../../src'
import { transformFor } from '../../src/transforms/vFor'
import { transformOn } from '../../src/transforms/vOn'
import { transformElement } from '../../src/transforms/transformElement'
import { transformExpression } from '../../src/transforms/transformExpression'
@ -17,7 +18,7 @@ import { transformExpression } from '../../src/transforms/transformExpression'
function parseWithVOn(template: string, options: CompilerOptions = {}) {
const ast = parse(template, options)
transform(ast, {
nodeTransforms: [transformExpression, transformElement],
nodeTransforms: [transformExpression, transformElement, transformFor],
directiveTransforms: {
on: transformOn,
},
@ -286,6 +287,23 @@ describe('compiler: transform v-on', () => {
})
})
test('should NOT wrap as function if expression is already function expression (async)', () => {
const { node } = parseWithVOn(
`<div @click="async $event => await foo($event)"/>`,
)
expect((node.codegenNode as VNodeCall).props).toMatchObject({
properties: [
{
key: { content: `onClick` },
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `async $event => await foo($event)`,
},
},
],
})
})
test('should NOT wrap as function if expression is already function expression (with newlines)', () => {
const { node } = parseWithVOn(
`<div @click="
@ -585,6 +603,17 @@ describe('compiler: transform v-on', () => {
expect(root.cached).toBe(1)
})
test('unicode identifier should not be cached (v-for)', () => {
const { root } = parseWithVOn(
`<div v-for="项 in items" :key="value"><div v-on:click="foo(项)"/></div>`,
{
prefixIdentifiers: true,
cacheHandlers: true,
},
)
expect(root.cached).toBe(0)
})
test('inline function expression handler', () => {
const { root, node } = parseWithVOn(`<div v-on:click="() => foo()" />`, {
prefixIdentifiers: true,
@ -630,6 +659,39 @@ describe('compiler: transform v-on', () => {
})
})
test('inline async arrow function with no bracket expression handler', () => {
const { root, node } = parseWithVOn(
`<div v-on:click="async e => await foo(e)" />`,
{
prefixIdentifiers: true,
cacheHandlers: true,
},
)
expect(root.cached).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined()
expect(
(vnodeCall.props as ObjectExpression).properties[0].value,
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 0,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`async `,
{ content: `e` },
` => await `,
{ content: `_ctx.foo` },
`(`,
{ content: `e` },
`)`,
],
},
})
})
test('inline async function expression handler', () => {
const { root, node } = parseWithVOn(
`<div v-on:click="async function () { await foo() } " />`,

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,9 +691,10 @@ 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()
})
@ -743,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.4.22",
"version": "3.4.35",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
@ -46,13 +46,13 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme",
"dependencies": {
"@babel/parser": "^7.24.1",
"@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.0"
"@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,
@ -331,7 +331,7 @@ export interface VNodeCall extends Node {
| ForRenderListExpression // v-for fragment call
| SimpleExpressionNode // hoisted
| undefined
patchFlag: string | undefined
patchFlag: PatchFlags | undefined
dynamicProps: string | SimpleExpressionNode | undefined
directives: DirectiveArguments | undefined
isBlock: boolean
@ -416,7 +416,7 @@ export interface CacheExpression extends Node {
type: NodeTypes.JS_CACHE_EXPRESSION
index: number
value: JSChildNode
isVNode: boolean
isVOnce: boolean
}
export interface MemoExpression extends CallExpression {
@ -561,7 +561,7 @@ export interface ForCodegenNode extends VNodeCall {
tag: typeof FRAGMENT
props: undefined
children: ForRenderListExpression
patchFlag: string
patchFlag: PatchFlags
disableTracking: boolean
}
@ -571,7 +571,7 @@ export interface ForRenderListExpression extends CallExpression {
}
export interface ForIteratorExpression extends FunctionExpression {
returns: BlockCodegenNode
returns?: BlockCodegenNode
}
// AST Utilities ---------------------------------------------------------------
@ -771,13 +771,13 @@ export function createConditionalExpression(
export function createCacheExpression(
index: number,
value: JSChildNode,
isVNode: boolean = false,
isVOnce: boolean = false,
): CacheExpression {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
index,
value,
isVNode,
isVOnce,
loc: locStub,
}
}

View File

@ -10,11 +10,14 @@ import type {
} from '@babel/types'
import { walk } from 'estree-walker'
/**
* Return value indicates whether the AST walked can be a constant
*/
export function walkIdentifiers(
root: Node,
onIdentifier: (
node: Identifier,
parent: Node,
parent: Node | null,
parentStack: Node[],
isReference: boolean,
isLocal: boolean,
@ -33,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 &&
@ -44,12 +47,13 @@ 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' &&
// eslint-disable-next-line no-restricted-syntax
parent?.type === 'ObjectPattern'
) {
// mark property in destructure pattern
@ -75,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) {
@ -404,6 +408,7 @@ function isReferenced(node: Node, parent: Node, grandparent?: Node): boolean {
// no: export { NODE as foo } from "foo";
case 'ExportSpecifier':
// @ts-expect-error
// eslint-disable-next-line no-restricted-syntax
if (grandparent?.source) {
return false
}

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,
@ -572,8 +578,9 @@ function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) {
// generate inlined withScopeId helper
if (genScopeId) {
const param = context.isTS ? '(n: any)' : 'n'
push(
`const _withScopeId = n => (${helper(
`const _withScopeId = ${param} => (${helper(
PUSH_SCOPE_ID,
)}("${scopeId}"),n=n(),${helper(POP_SCOPE_ID)}(),n)`,
)
@ -842,6 +849,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) + `(`)
}
@ -856,7 +885,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(`)`)
@ -1008,15 +1037,16 @@ function genConditionalExpression(
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
const { push, helper, indent, deindent, newline } = context
push(`_cache[${node.index}] || (`)
if (node.isVNode) {
if (node.isVOnce) {
indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
newline()
push(`(`)
}
push(`_cache[${node.index}] = `)
genNode(node.value, context)
if (node.isVNode) {
push(`,`)
if (node.isVOnce) {
push(`).cacheIndex = ${node.index},`)
newline()
push(`${helper(SET_BLOCK_TRACKING)}(1),`)
newline()

View File

@ -25,9 +25,7 @@ export const transformFilter: NodeTransform = (node, context) => {
// filter rewrite is applied before expression transform so only
// simple expressions are possible at this stage
rewriteFilter(node.content, context)
}
if (node.type === NodeTypes.ELEMENT) {
} else if (node.type === NodeTypes.ELEMENT) {
node.props.forEach((prop: AttributeNode | DirectiveNode) => {
if (
prop.type === NodeTypes.DIRECTIVE &&
@ -168,6 +166,8 @@ function parseFilter(node: SimpleExpressionNode, context: TransformContext) {
expression = wrapFilter(expression, filters[i], context)
}
node.content = expression
// reset ast since the content is replaced
node.ast = undefined
}
}

View File

@ -179,7 +179,7 @@ const tokenizer = new Tokenizer(stack, {
const name = currentOpenTag!.tag
currentOpenTag!.isSelfClosing = true
endOpenTag(end)
if (stack[0]?.tag === name) {
if (stack[0] && stack[0].tag === name) {
onCloseTag(stack.shift()!, end)
}
},
@ -587,14 +587,14 @@ function endOpenTag(end: number) {
function onText(content: string, start: number, end: number) {
if (__BROWSER__) {
const tag = stack[0]?.tag
const tag = stack[0] && stack[0].tag
if (tag !== 'script' && tag !== 'style' && content.includes('&')) {
content = currentOptions.decodeEntities!(content, false)
}
}
const parent = stack[0] || currentRoot
const lastNode = parent.children[parent.children.length - 1]
if (lastNode?.type === NodeTypes.TEXT) {
if (lastNode && lastNode.type === NodeTypes.TEXT) {
// merge
lastNode.content += content
setLocEnd(lastNode.loc, end)
@ -771,7 +771,8 @@ function isComponent({ tag, props }: ElementNode): boolean {
tag === 'component' ||
isUpperCase(tag.charCodeAt(0)) ||
isCoreComponent(tag) ||
currentOptions.isBuiltInComponent?.(tag) ||
(currentOptions.isBuiltInComponent &&
currentOptions.isBuiltInComponent(tag)) ||
(currentOptions.isNativeTag && !currentOptions.isNativeTag(tag))
) {
return true
@ -828,8 +829,8 @@ function condenseWhitespace(
if (node.type === NodeTypes.TEXT) {
if (!inPre) {
if (isAllWhitespace(node.content)) {
const prev = nodes[i - 1]?.type
const next = nodes[i + 1]?.type
const prev = nodes[i - 1] && nodes[i - 1].type
const next = nodes[i + 1] && nodes[i + 1].type
// Remove if:
// - the whitespace is the first or last node, or:
// - (condense mode) the whitespace is between two comments, or:
@ -1063,7 +1064,7 @@ export function baseParse(input: string, options?: ParserOptions): RootNode {
currentOptions.ns === Namespaces.SVG ||
currentOptions.ns === Namespaces.MATH_ML
const delimiters = options?.delimiters
const delimiters = options && options.delimiters
if (delimiters) {
tokenizer.delimiterOpen = toCharCodes(delimiters[0])
tokenizer.delimiterClose = toCharCodes(delimiters[1])

View File

@ -102,6 +102,9 @@ export interface TransformContext
vOnce: number
}
parent: ParentNode | null
// we could use a stack but in practice we've only ever needed two layers up
// so this is more efficient
grandParent: ParentNode | null
childIndex: number
currentNode: RootNode | TemplateChildNode | null
inVOnce: boolean
@ -193,6 +196,7 @@ export function createTransformContext(
vOnce: 0,
},
parent: null,
grandParent: null,
currentNode: root,
childIndex: 0,
inVOnce: false,
@ -378,7 +382,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
helper(FRAGMENT),
undefined,
root.children,
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
patchFlag,
undefined,
undefined,
true,
@ -401,6 +405,7 @@ export function traverseChildren(
for (; i < parent.children.length; i++) {
const child = parent.children[i]
if (isString(child)) continue
context.grandParent = context.parent
context.parent = parent
context.childIndex = i
context.onNodeRemoved = nodeRemoved

View File

@ -70,8 +70,7 @@ function walk(
: getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_HOIST) {
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED
child.codegenNode = context.hoist(child.codegenNode!)
hoistedCount++
continue
@ -81,9 +80,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) >=
@ -174,12 +173,12 @@ export function getConstantType(
if (
codegenNode.isBlock &&
node.tag !== 'svg' &&
node.tag !== 'foreignObject'
node.tag !== 'foreignObject' &&
node.tag !== 'math'
) {
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:
@ -364,8 +363,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']
@ -117,7 +115,7 @@ export const transformElement: NodeTransform = (node, context) => {
// updates inside get proper isSVG flag at runtime. (#639, #643)
// This is technically web-specific, but splitting the logic out of core
// leads to too much unnecessary complexity.
(tag === 'svg' || tag === 'foreignObject'))
(tag === 'svg' || tag === 'foreignObject' || tag === 'math'))
// props
if (props.length > 0) {
@ -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,
@ -433,6 +412,18 @@ export function buildProps(
if (arg) mergeArgs.push(arg)
}
// mark template ref on v-for
const pushRefVForMarker = () => {
if (context.scopes.vFor > 0) {
properties.push(
createObjectProperty(
createSimpleExpression('ref_for', true),
createSimpleExpression('true'),
),
)
}
}
const analyzePatchFlag = ({ key, value }: Property) => {
if (isStaticExp(key)) {
const name = key.content
@ -502,14 +493,7 @@ export function buildProps(
let isStatic = true
if (name === 'ref') {
hasRef = true
if (context.scopes.vFor > 0) {
properties.push(
createObjectProperty(
createSimpleExpression('ref_for', true),
createSimpleExpression('true'),
),
)
}
pushRefVForMarker()
// in inline mode there is no setupState object, so we can't use string
// keys to set the ref. Instead, we need to transform it to pass the
// actual ref instead.
@ -601,13 +585,8 @@ export function buildProps(
shouldUseBlock = true
}
if (isVBind && isStaticArgOf(arg, 'ref') && context.scopes.vFor > 0) {
properties.push(
createObjectProperty(
createSimpleExpression('ref_for', true),
createSimpleExpression('true'),
),
)
if (isVBind && isStaticArgOf(arg, 'ref')) {
pushRefVForMarker()
}
// special case for v-bind and v-on with no argument
@ -615,6 +594,8 @@ export function buildProps(
hasDynamicKeys = true
if (exp) {
if (isVBind) {
// #10696 in case a v-bind object contains ref
pushRefVForMarker()
// have to merge early for compat build check
pushMergeArg()
if (__COMPAT__) {

View File

@ -40,16 +40,12 @@ import type {
UpdateExpression,
} from '@babel/types'
import { validateBrowserExpression } from '../validateExpression'
import { parse } from '@babel/parser'
import { parseExpression } from '@babel/parser'
import { IS_REF, UNREF } from '../runtimeHelpers'
import { BindingTypes } from '../options'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
// a heuristic safeguard to bail constant expressions on presence of
// likely function invocation and member access
const constantBailRE = /\w\s*\(|\.[^\d]/
export const transformExpression: NodeTransform = (node, context) => {
if (node.type === NodeTypes.INTERPOLATION) {
node.content = processExpression(
@ -120,7 +116,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
@ -226,8 +226,6 @@ export function processExpression(
// fast path if expression is a simple identifier.
const rawExp = node.content
// bail constant on parens (function invocation) and dot (member access)
const bailConstant = constantBailRE.test(rawExp)
let ast = node.ast
@ -272,9 +270,10 @@ export function processExpression(
? ` ${rawExp} `
: `(${rawExp})${asParams ? `=>{}` : ``}`
try {
ast = parse(source, {
ast = parseExpression(source, {
sourceType: 'module',
plugins: context.expressionPlugins,
}).program
})
} catch (e: any) {
context.onError(
createCompilerError(
@ -316,7 +315,13 @@ export function processExpression(
} else {
// The identifier is considered constant unless it's pointing to a
// local scope variable (a v-for alias, or a v-slot prop)
if (!(needPrefix && isLocal) && !bailConstant) {
if (
!(needPrefix && isLocal) &&
(!parent ||
(parent.type !== 'CallExpression' &&
parent.type !== 'NewExpression' &&
parent.type !== 'MemberExpression'))
) {
;(node as QualifiedId).isConstant = true
}
// also generate sub-expressions for other identifiers for better
@ -370,9 +375,7 @@ export function processExpression(
ret.ast = ast
} else {
ret = node
ret.constType = bailConstant
? ConstantTypes.NOT_CONSTANT
: ConstantTypes.CAN_STRINGIFY
ret.constType = ConstantTypes.CAN_STRINGIFY
}
ret.identifiers = Object.keys(knownIds)
return ret

View File

@ -1,5 +1,6 @@
import type { DirectiveTransform } from '../transform'
import type { DirectiveTransform, TransformContext } from '../transform'
import {
type DirectiveNode,
type ExpressionNode,
NodeTypes,
type SimpleExpressionNode,
@ -56,11 +57,8 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
}
}
const propName = camelize((arg as SimpleExpressionNode).content)
exp = dir.exp = createSimpleExpression(propName, false, arg.loc)
if (!__BROWSER__) {
exp = dir.exp = processExpression(exp, context)
}
transformBindShorthand(dir, context)
exp = dir.exp!
}
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) {
@ -98,6 +96,19 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
}
}
export const transformBindShorthand = (
dir: DirectiveNode,
context: TransformContext,
) => {
const arg = dir.arg!
const propName = camelize((arg as SimpleExpressionNode).content)
dir.exp = createSimpleExpression(propName, false, arg.loc)
if (!__BROWSER__) {
dir.exp = processExpression(dir.exp, context)
}
}
const injectPrefix = (arg: ExpressionNode, prefix: string) => {
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
if (arg.isStatic) {

View File

@ -46,7 +46,8 @@ 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(
'for',
@ -60,13 +61,20 @@ export const transformFor = createStructuralDirectiveTransform(
]) as ForRenderListExpression
const isTemplate = isTemplateNode(node)
const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`)
const keyProp = findProp(node, `key`, false, true)
if (keyProp && keyProp.type === NodeTypes.DIRECTIVE && !keyProp.exp) {
// resolve :key shorthand #10882
transformBindShorthand(keyProp, context)
}
const keyExp =
keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!)
const keyProperty = keyProp ? createObjectProperty(`key`, keyExp!) : null
? keyProp.value
? createSimpleExpression(keyProp.value.content, true)
: undefined
: keyProp.exp)
const keyProperty =
keyProp && keyExp ? createObjectProperty(`key`, keyExp) : null
if (!__BROWSER__ && isTemplate) {
// #2085 / #5288 process :key and v-memo expressions need to be
@ -101,8 +109,7 @@ export const transformFor = createStructuralDirectiveTransform(
helper(FRAGMENT),
undefined,
renderExp,
fragmentFlag +
(__DEV__ ? ` /* ${PatchFlagNames[fragmentFlag]} */` : ``),
fragmentFlag,
undefined,
undefined,
true /* isBlock */,
@ -161,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

@ -17,7 +17,7 @@ import { hasScopeRef, isMemberExpression } from '../utils'
import { TO_HANDLER_KEY } from '../runtimeHelpers'
const fnExpRE =
/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
/^\s*(async\s*)?(\([^)]*?\)|[\w$_]+)\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
export interface VOnDirectiveNode extends DirectiveNode {
// v-on without arg is handled directly in ./transformElements.ts due to it affecting

View File

@ -226,10 +226,7 @@ export function buildSlots(
break
}
}
if (prev && isTemplateNode(prev) && findDir(prev, 'if')) {
// remove node
children.splice(i, 1)
i--
if (prev && isTemplateNode(prev) && findDir(prev, /^(else-)?if$/)) {
__TEST__ && assert(dynamicSlots.length > 0)
// attach this slot to previous conditional
let conditional = dynamicSlots[

View File

@ -62,7 +62,7 @@ export function isCoreComponent(tag: string): symbol | void {
}
}
const nonIdentifierRE = /^\d|[^\$\w]/
const nonIdentifierRE = /^\d|[^\$\w\xA0-\uFFFF]/
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)
@ -499,4 +499,4 @@ export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) {
}
}
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/

View File

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

View File

@ -1,5 +1,24 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`stringify static html > should bail for <option> elements with number values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("select", null, [
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 })
], -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}"
`;
exports[`stringify static html > should bail on bindings that are hoisted but not stringifiable 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
@ -20,6 +39,19 @@ return function render(_ctx, _cache) {
}"
`;
exports[`stringify static html > should work for <option> elements with string values 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}"
`;
exports[`stringify static html > should work with bindings that are non-static but stringifiable 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

View File

@ -485,4 +485,51 @@ describe('stringify static html', () => {
expect(code).toMatch(`<code>text1</code>`)
expect(code).toMatchSnapshot()
})
test('should work for <option> elements with string values', () => {
const { ast, code } = compileWithStringify(
`<div><select>${repeat(
`<option value="1" />`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select></div>`,
)
// should be optimized now
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<select>${repeat(
`<option value="1"></option>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select>`,
),
'1',
],
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(code).toMatchSnapshot()
})
test('should bail for <option> elements with number values', () => {
const { ast, code } = compileWithStringify(
`<div><select>${repeat(
`<option :value="1" />`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select></div>`,
)
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(code).toMatchSnapshot()
})
})

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.4.22",
"version": "3.4.35",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",

View File

@ -17,6 +17,7 @@ import {
type TextCallNode,
type TransformContext,
createCallExpression,
isStaticArgOf,
} from '@vue/compiler-core'
import {
escapeHtml,
@ -200,6 +201,7 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
// probably only need to check for most common case
// i.e. non-phrasing-content tags inside `<p>`
function walk(node: ElementNode): boolean {
const isOptionTag = node.tag === 'option' && node.ns === Namespaces.HTML
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
// bail on non-attr bindings
@ -225,6 +227,16 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
) {
return bail()
}
// <option :value="1"> cannot be safely stringified
if (
isOptionTag &&
isStaticArgOf(p.arg, 'value') &&
p.exp &&
p.exp.ast &&
p.exp.ast.type !== 'StringLiteral'
) {
return bail()
}
}
}
for (let i = 0; i < node.children.length; i++) {

View File

@ -226,3 +226,43 @@ return { modelValue, fn, fnWithDefault, str, optional }
})"
`;
exports[`defineModel() > w/ types, production mode, boolean + multiple types 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
props: {
"modelValue": { type: [Boolean, String, Object] },
"modelModifiers": {},
},
emits: ["update:modelValue"],
setup(__props, { expose: __expose }) {
__expose();
const modelValue = _useModel<boolean | string | {}>(__props, "modelValue")
return { modelValue }
}
})"
`;
exports[`defineModel() > w/ types, production mode, function + runtime opts + multiple types 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
props: {
"modelValue": { type: [Number, Function], ...{ default: () => 1 } },
"modelModifiers": {},
},
emits: ["update:modelValue"],
setup(__props, { expose: __expose }) {
__expose();
const modelValue = _useModel<number | (() => number)>(__props, "modelValue")
return { modelValue }
}
})"
`;

View File

@ -304,3 +304,19 @@ return () => {}
}"
`;
exports[`sfc reactive props destructure > rest spread non-inline 1`] = `
"import { createPropsRestProxy as _createPropsRestProxy } from 'vue'
export default {
props: ['foo', 'bar'],
setup(__props, { expose: __expose }) {
__expose();
const rest = _createPropsRestProxy(__props, ["foo"])
return { rest }
}
}"
`;

View File

@ -161,6 +161,34 @@ describe('defineModel()', () => {
})
})
test('w/ types, production mode, boolean + multiple types', () => {
const { content } = compile(
`
<script setup lang="ts">
const modelValue = defineModel<boolean | string | {}>()
</script>
`,
{ isProd: true },
)
assertCode(content)
expect(content).toMatch('"modelValue": { type: [Boolean, String, Object] }')
})
test('w/ types, production mode, function + runtime opts + multiple types', () => {
const { content } = compile(
`
<script setup lang="ts">
const modelValue = defineModel<number | (() => number)>({ default: () => 1 })
</script>
`,
{ isProd: true },
)
assertCode(content)
expect(content).toMatch(
'"modelValue": { type: [Number, Function], ...{ default: () => 1 } }',
)
})
test('get / set transformers', () => {
const { content } = compile(
`

View File

@ -265,6 +265,27 @@ describe('sfc reactive props destructure', () => {
})
})
test('rest spread non-inline', () => {
const { content, bindings } = compile(
`
<script setup>
const { foo, ...rest } = defineProps(['foo', 'bar'])
</script>
<template>{{ rest.bar }}</template>
`,
{ inlineTemplate: false },
)
expect(content).toMatch(
`const rest = _createPropsRestProxy(__props, ["foo"])`,
)
assertCode(content)
expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS,
bar: BindingTypes.PROPS,
rest: BindingTypes.SETUP_REACTIVE_CONST,
})
})
// #6960
test('computed static key', () => {
const { content, bindings } = compile(`

View File

@ -9,8 +9,9 @@ import {
registerTS,
resolveTypeElements,
} from '../../src/script/resolveType'
import { UNKNOWN_TYPE } from '../../src/script/utils'
import ts from 'typescript'
registerTS(() => ts)
describe('resolveType', () => {
@ -128,7 +129,7 @@ describe('resolveType', () => {
defineProps<{ self: any } & Foo & Bar & Baz>()
`).props,
).toStrictEqual({
self: ['Unknown'],
self: [UNKNOWN_TYPE],
foo: ['Number'],
// both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
// preferred
@ -136,6 +137,18 @@ describe('resolveType', () => {
})
})
test('intersection type with ignore', () => {
expect(
resolve(`
type Foo = { foo: number }
type Bar = { bar: string }
defineProps<Foo & /* @vue-ignore */ Bar>()
`).props,
).toStrictEqual({
foo: ['Number'],
})
})
// #7553
test('union type', () => {
expect(
@ -265,6 +278,27 @@ describe('resolveType', () => {
})
})
test('utility type: ReadonlyArray', () => {
expect(
resolve(`
defineProps<{ foo: ReadonlyArray<string> }>()
`).props,
).toStrictEqual({
foo: ['Array'],
})
})
test('utility type: ReadonlyMap & Readonly Set', () => {
expect(
resolve(`
defineProps<{ foo: ReadonlyMap<string, unknown>, bar: ReadonlySet<string> }>()
`).props,
).toStrictEqual({
foo: ['Map'],
bar: ['Set'],
})
})
test('indexed access type (literal)', () => {
expect(
resolve(`
@ -416,6 +450,152 @@ describe('resolveType', () => {
})
})
test('readonly', () => {
expect(
resolve(`
defineProps<{ foo: readonly unknown[] }>()
`).props,
).toStrictEqual({
foo: ['Array'],
})
})
test('keyof', () => {
const files = {
'/foo.ts': `export type IMP = { ${1}: 1 };`,
}
const { props } = resolve(
`
import { IMP } from './foo'
interface Foo { foo: 1, ${1}: 1 }
type Bar = { bar: 1 }
declare const obj: Bar
declare const set: Set<any>
declare const arr: Array<any>
defineProps<{
imp: keyof IMP,
foo: keyof Foo,
bar: keyof Bar,
obj: keyof typeof obj,
set: keyof typeof set,
arr: keyof typeof arr
}>()
`,
files,
)
expect(props).toStrictEqual({
imp: ['Number'],
foo: ['String', 'Number'],
bar: ['String'],
obj: ['String'],
set: ['String'],
arr: ['String', 'Number'],
})
})
test('keyof: index signature', () => {
const { props } = resolve(
`
declare const num: number;
interface Foo {
[key: symbol]: 1
[key: string]: 1
[key: typeof num]: 1,
}
type Test<T> = T
type Bar = {
[key: string]: 1
[key: Test<number>]: 1
}
defineProps<{
foo: keyof Foo
bar: keyof Bar
}>()
`,
)
expect(props).toStrictEqual({
foo: ['Symbol', 'String', 'Number'],
bar: [UNKNOWN_TYPE],
})
})
// #11129
test('keyof: intersection type', () => {
const { props } = resolve(`
type A = { name: string }
type B = A & { [key: number]: string }
defineProps<{
foo: keyof B
}>()`)
expect(props).toStrictEqual({
foo: ['String', 'Number'],
})
})
test('keyof: union type', () => {
const { props } = resolve(`
type A = { name: string }
type B = A | { [key: number]: string }
defineProps<{
foo: keyof B
}>()`)
expect(props).toStrictEqual({
foo: ['String', 'Number'],
})
})
test('keyof: utility type', () => {
const { props } = resolve(
`
type Foo = Record<symbol | string, any>
type Bar = { [key: string]: any }
type AnyRecord = Record<keyof any, any>
type Baz = { a: 1, ${1}: 2, b: 3}
defineProps<{
record: keyof Foo,
anyRecord: keyof AnyRecord
partial: keyof Partial<Bar>,
required: keyof Required<Bar>,
readonly: keyof Readonly<Bar>,
pick: keyof Pick<Baz, 'a' | 1>
extract: keyof Extract<keyof Baz, 'a' | 1>
}>()
`,
)
expect(props).toStrictEqual({
record: ['Symbol', 'String'],
anyRecord: ['String', 'Number', 'Symbol'],
partial: ['String'],
required: ['String'],
readonly: ['String'],
pick: ['String', 'Number'],
extract: ['String', 'Number'],
})
})
test('keyof: fallback to Unknown', () => {
const { props } = resolve(
`
interface Barr {}
interface Bar extends Barr {}
type Foo = keyof Bar
defineProps<{ foo: Foo }>()
`,
)
expect(props).toStrictEqual({
foo: [UNKNOWN_TYPE],
})
})
test('ExtractPropTypes (element-plus)', () => {
const { props, raw } = resolve(
`
@ -455,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(
@ -879,6 +1079,53 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
test('ts module resolve w/ project reference folder', () => {
const files = {
'/tsconfig.json': JSON.stringify({
references: [
{
path: './web',
},
{
path: './empty',
},
{
path: './noexists-should-ignore',
},
],
}),
'/web/tsconfig.json': JSON.stringify({
include: ['../**/*.ts', '../**/*.vue'],
compilerOptions: {
composite: true,
paths: {
bar: ['../user.ts'],
},
},
}),
// tsconfig with no include / paths defined, should match nothing
'/empty/tsconfig.json': JSON.stringify({
compilerOptions: {
composite: true,
},
}),
'/user.ts': 'export type User = { bar: string }',
}
const { props, deps } = resolve(
`
import { User } from 'bar'
defineProps<User>()
`,
files,
)
expect(props).toStrictEqual({
bar: ['String'],
})
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})
test('ts module resolve w/ path aliased vue file', () => {
const files = {
'/tsconfig.json': JSON.stringify({
@ -1055,6 +1302,37 @@ describe('resolveType', () => {
})
})
})
describe('template literals', () => {
test('mapped types with string type', () => {
expect(
resolve(`
type X = 'a' | 'b'
defineProps<{[K in X as \`\${K}_foo\`]: string}>()
`).props,
).toStrictEqual({
a_foo: ['String'],
b_foo: ['String'],
})
})
// #10962
test('mapped types with generic parameters', () => {
const { props } = resolve(`
type Breakpoints = 'sm' | 'md' | 'lg'
type BreakpointFactory<T extends string, V> = {
[K in Breakpoints as \`\${T}\${Capitalize<K>}\`]: V
}
type ColsBreakpoints = BreakpointFactory<'cols', number>
defineProps<ColsBreakpoints>()
`)
expect(props).toStrictEqual({
colsSm: ['Number'],
colsMd: ['Number'],
colsLg: ['Number'],
})
})
})
})
function resolve(

View File

@ -1,4 +1,5 @@
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
import { parse as babelParse } from '@babel/parser'
import {
type SFCTemplateCompileOptions,
compileTemplate,
@ -452,6 +453,36 @@ test('prefixing edge case for reused AST ssr mode', () => {
).not.toThrowError()
})
// #10852
test('non-identifier expression in legacy filter syntax', () => {
const src = `
<template>
<div>
Today is
{{ new Date() | formatDate }}
</div>
</template>
`
const { descriptor } = parse(src)
const compilationResult = compileTemplate({
id: 'xxx',
filename: 'test.vue',
ast: descriptor.template!.ast,
source: descriptor.template!.content,
ssr: false,
compilerOptions: {
compatConfig: {
MODE: 2,
},
},
})
expect(() => {
babelParse(compilationResult.code, { sourceType: 'module' })
}).not.toThrow()
})
interface Pos {
line: number
column: number

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.4.22",
"version": "3.4.35",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@ -42,26 +42,26 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme",
"dependencies": {
"@babel/parser": "^7.24.1",
"@babel/parser": "catalog:",
"@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*",
"@vue/shared": "workspace:*",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.8",
"postcss": "^8.4.38",
"source-map-js": "^1.2.0"
"estree-walker": "catalog:",
"magic-string": "catalog:",
"postcss": "^8.4.40",
"source-map-js": "catalog:"
},
"devDependencies": {
"@babel/types": "^7.24.0",
"@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.0.16",
"pug": "^3.0.2",
"sass": "^1.74.1"
"postcss-selector-parser": "^6.1.1",
"pug": "^3.0.3",
"sass": "^1.77.8"
}
}

View File

@ -49,7 +49,7 @@ import {
} from './script/defineEmits'
import { DEFINE_EXPOSE, processDefineExpose } from './script/defineExpose'
import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
import { processDefineSlots } from './script/defineSlots'
import { DEFINE_SLOTS, processDefineSlots } from './script/defineSlots'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
import { getImportedName, isCallOf, isLiteralNode } from './script/utils'
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
@ -135,6 +135,16 @@ export interface ImportBinding {
isUsedInTemplate: boolean
}
const MACROS = [
DEFINE_PROPS,
DEFINE_EMITS,
DEFINE_EXPOSE,
DEFINE_OPTIONS,
DEFINE_SLOTS,
DEFINE_MODEL,
WITH_DEFAULTS,
]
/**
* Compile `<script setup>`
* It requires the whole SFC descriptor because we need to handle and merge
@ -317,15 +327,18 @@ export function compileScript(
const imported = getImportedName(specifier)
const source = node.source.value
const existing = ctx.userImports[local]
if (
source === 'vue' &&
(imported === DEFINE_PROPS ||
imported === DEFINE_EMITS ||
imported === DEFINE_EXPOSE)
) {
warnOnce(
`\`${imported}\` is a compiler macro and no longer needs to be imported.`,
)
if (source === 'vue' && MACROS.includes(imported)) {
if (local === imported) {
warnOnce(
`\`${imported}\` is a compiler macro and no longer needs to be imported.`,
)
} else {
ctx.error(
`\`${imported}\` is a compiler macro and cannot be aliased to ` +
`a different name.`,
specifier,
)
}
removeSpecifier(i)
} else if (existing) {
if (existing.source === source && existing.imported === imported) {
@ -523,8 +536,14 @@ export function compileScript(
)
}
// defineProps / defineEmits
// defineProps
const isDefineProps = processDefineProps(ctx, init, decl.id)
if (ctx.propsDestructureRestId) {
setupBindings[ctx.propsDestructureRestId] =
BindingTypes.SETUP_REACTIVE_CONST
}
// defineEmits
const isDefineEmits =
!isDefineProps && processDefineEmits(ctx, init, decl.id)
!isDefineEmits &&
@ -597,7 +616,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()
}
@ -1048,13 +1067,16 @@ function walkDeclaration(
// export const foo = ...
for (const { id, init: _init } of node.declarations) {
const init = _init && unwrapTSNode(_init)
const isDefineCall = !!(
const isConstMacroCall =
isConst &&
isCallOf(
init,
c => c === DEFINE_PROPS || c === DEFINE_EMITS || c === WITH_DEFAULTS,
c =>
c === DEFINE_PROPS ||
c === DEFINE_EMITS ||
c === WITH_DEFAULTS ||
c === DEFINE_SLOTS,
)
)
if (id.type === 'Identifier') {
let bindingType
const userReactiveBinding = userImportAliases['reactive']
@ -1071,7 +1093,7 @@ function walkDeclaration(
} else if (
// if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it
isDefineCall ||
isConstMacroCall ||
(isConst && canNeverBeRef(init!, userReactiveBinding))
) {
bindingType = isCallOf(init, DEFINE_PROPS)
@ -1103,9 +1125,9 @@ function walkDeclaration(
continue
}
if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst, isDefineCall)
walkObjectPattern(id, bindings, isConst, isConstMacroCall)
} else if (id.type === 'ArrayPattern') {
walkArrayPattern(id, bindings, isConst, isDefineCall)
walkArrayPattern(id, bindings, isConst, isConstMacroCall)
}
}
}

View File

@ -191,11 +191,11 @@ function doCompileTemplate({
if (ssr && !ssrCssVars) {
warnOnce(
`compileTemplate is called with \`ssr: true\` but no ` +
`corresponding \`cssVars\` option.\`.`,
`corresponding \`cssVars\` option.`,
)
}
if (!id) {
warnOnce(`compileTemplate now requires the \`id\` option.\`.`)
warnOnce(`compileTemplate now requires the \`id\` option.`)
id = ''
}

View File

@ -175,14 +175,14 @@ export function resolveParserPlugins(
) {
plugins.push('importAttributes')
}
if (lang === 'jsx' || lang === 'tsx') {
if (lang === 'jsx' || lang === 'tsx' || lang === 'mtsx') {
plugins.push('jsx')
} else if (userPlugins) {
// If don't match the case of adding jsx
// should remove the jsx from user options
userPlugins = userPlugins.filter(p => p !== 'jsx')
}
if (lang === 'ts' || lang === 'tsx') {
if (lang === 'ts' || lang === 'mts' || lang === 'tsx' || lang === 'mtsx') {
plugins.push(['typescript', { dts }], 'explicitResourceManagement')
if (!userPlugins || !userPlugins.includes('decorators')) {
plugins.push('decorators-legacy')

View File

@ -1,12 +1,7 @@
import type { LVal, Node, TSType } from '@babel/types'
import type { ScriptCompileContext } from './context'
import { inferRuntimeType } from './resolveType'
import {
UNKNOWN_TYPE,
concatStrings,
isCallOf,
toRuntimeTypeString,
} from './utils'
import { UNKNOWN_TYPE, isCallOf, toRuntimeTypeString } from './utils'
import { BindingTypes, unwrapTSNode } from '@vue/compiler-dom'
export const DEFINE_MODEL = 'defineModel'
@ -124,44 +119,50 @@ export function genModelProps(ctx: ScriptCompileContext) {
const isProd = !!ctx.options.isProd
let modelPropsDecl = ''
for (const [name, { type, options }] of Object.entries(ctx.modelDecls)) {
for (const [name, { type, options: runtimeOptions }] of Object.entries(
ctx.modelDecls,
)) {
let skipCheck = false
let codegenOptions = ``
let runtimeTypes = type && inferRuntimeType(ctx, type)
if (runtimeTypes) {
const hasBoolean = runtimeTypes.includes('Boolean')
const hasFunction = runtimeTypes.includes('Function')
const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)
if (isProd || hasUnknownType) {
runtimeTypes = runtimeTypes.filter(
t =>
t === 'Boolean' ||
(hasBoolean && t === 'String') ||
(t === 'Function' && options),
)
if (hasUnknownType) {
if (hasBoolean || hasFunction) {
runtimeTypes = runtimeTypes.filter(t => t !== UNKNOWN_TYPE)
skipCheck = true
} else {
runtimeTypes = ['null']
}
}
skipCheck = !isProd && hasUnknownType && runtimeTypes.length > 0
if (!isProd) {
codegenOptions =
`type: ${toRuntimeTypeString(runtimeTypes)}` +
(skipCheck ? ', skipCheck: true' : '')
} else if (hasBoolean || (runtimeOptions && hasFunction)) {
// preserve types if contains boolean, or
// function w/ runtime options that may contain default
codegenOptions = `type: ${toRuntimeTypeString(runtimeTypes)}`
} else {
// able to drop types in production
}
}
let runtimeType =
(runtimeTypes &&
runtimeTypes.length > 0 &&
toRuntimeTypeString(runtimeTypes)) ||
undefined
const codegenOptions = concatStrings([
runtimeType && `type: ${runtimeType}`,
skipCheck && 'skipCheck: true',
])
let decl: string
if (runtimeType && options) {
if (codegenOptions && runtimeOptions) {
decl = ctx.isTS
? `{ ${codegenOptions}, ...${options} }`
: `Object.assign({ ${codegenOptions} }, ${options})`
? `{ ${codegenOptions}, ...${runtimeOptions} }`
: `Object.assign({ ${codegenOptions} }, ${runtimeOptions})`
} else if (codegenOptions) {
decl = `{ ${codegenOptions} }`
} else if (runtimeOptions) {
decl = runtimeOptions
} else {
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
decl = `{}`
}
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`

View File

@ -37,10 +37,23 @@ export function processDefineOptions(
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
prop.key.type === 'Identifier'
) {
if (prop.key.name === 'props') propsOption = prop
if (prop.key.name === 'emits') emitsOption = prop
if (prop.key.name === 'expose') exposeOption = prop
if (prop.key.name === 'slots') slotsOption = prop
switch (prop.key.name) {
case 'props':
propsOption = prop
break
case 'emits':
emitsOption = prop
break
case 'expose':
exposeOption = prop
break
case 'slots':
slotsOption = prop
break
}
}
}
}

View File

@ -239,7 +239,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
@ -294,7 +294,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

@ -165,12 +165,19 @@ function innerResolveTypeElements(
scope: TypeScope,
typeParameters?: Record<string, Node>,
): ResolvedElements {
if (
node.leadingComments &&
node.leadingComments.some(c => c.value.includes('@vue-ignore'))
) {
return { props: {} }
}
switch (node.type) {
case 'TSTypeLiteral':
return typeElementsToMap(ctx, node.members, scope, typeParameters)
case 'TSInterfaceDeclaration':
return resolveInterfaceMembers(ctx, node, scope, typeParameters)
case 'TSTypeAliasDeclaration':
case 'TSTypeAnnotation':
case 'TSParenthesizedType':
return resolveTypeElements(
ctx,
@ -188,7 +195,7 @@ function innerResolveTypeElements(
node.type,
)
case 'TSMappedType':
return resolveMappedType(ctx, node, scope)
return resolveMappedType(ctx, node, scope, typeParameters)
case 'TSIndexedAccessType': {
const types = resolveIndexType(ctx, node, scope)
return mergeElements(
@ -414,12 +421,6 @@ function resolveInterfaceMembers(
)
if (node.extends) {
for (const ext of node.extends) {
if (
ext.leadingComments &&
ext.leadingComments.some(c => c.value.includes('@vue-ignore'))
) {
continue
}
try {
const { props, calls } = resolveTypeElements(ctx, ext, scope)
for (const key in props) {
@ -439,6 +440,7 @@ function resolveInterfaceMembers(
`Note: both in 3.2 or with the ignore, the properties in the base ` +
`type are treated as fallthrough attrs at runtime.`,
ext,
scope,
)
}
}
@ -450,9 +452,18 @@ function resolveMappedType(
ctx: TypeResolveContext,
node: TSMappedType,
scope: TypeScope,
typeParameters?: Record<string, Node>,
): ResolvedElements {
const res: ResolvedElements = { props: {} }
const keys = resolveStringType(ctx, node.typeParameter.constraint!, scope)
let keys: string[]
if (node.nameType) {
const { name, constraint } = node.typeParameter
scope = createChildScope(scope)
Object.assign(scope.types, { ...typeParameters, [name]: constraint })
keys = resolveStringType(ctx, node.nameType, scope)
} else {
keys = resolveStringType(ctx, node.typeParameter.constraint!, scope)
}
for (const key of keys) {
res.props[key] = createProperty(
{
@ -903,7 +914,7 @@ function importSourceToScope(
const filename = osSpecificJoinFn(dirname(scope.filename), source)
resolved = resolveExt(filename, fs)
} else if (source.startsWith('.')) {
} else if (source[0] === '.') {
// relative import - fast path
const filename = joinPaths(dirname(scope.filename), source)
resolved = resolveExt(filename, fs)
@ -1005,11 +1016,11 @@ function resolveWithTS(
(c.config.options.pathsBasePath as string) ||
dirname(c.config.options.configFilePath as string),
)
const included: string[] = c.config.raw?.include
const excluded: string[] = c.config.raw?.exclude
const included: string[] | undefined = c.config.raw?.include
const excluded: string[] | undefined = c.config.raw?.exclude
if (
(!included && (!base || containingFile.startsWith(base))) ||
included.some(p => isMatch(containingFile, joinPaths(base, p)))
included?.some(p => isMatch(containingFile, joinPaths(base, p)))
) {
if (
excluded &&
@ -1080,8 +1091,12 @@ function loadTSConfig(
const res = [config]
if (config.projectReferences) {
for (const ref of config.projectReferences) {
tsConfigRefMap.set(ref.path, configPath)
res.unshift(...loadTSConfig(ref.path, ts, fs))
const refPath = ts.resolveProjectReferencePath(ref)
if (!fs.fileExists(refPath)) {
continue
}
tsConfigRefMap.set(refPath, configPath)
res.unshift(...loadTSConfig(refPath, ts, fs))
}
}
return res
@ -1125,12 +1140,12 @@ function parseFile(
parserPlugins?: SFCScriptCompileOptions['babelParserPlugins'],
): Statement[] {
const ext = extname(filename)
if (ext === '.ts' || ext === '.tsx') {
if (ext === '.ts' || ext === '.mts' || ext === '.tsx' || ext === '.mtsx') {
return babelParse(content, {
plugins: resolveParserPlugins(
ext.slice(1),
parserPlugins,
filename.endsWith('.d.ts'),
/\.d\.m?ts$/.test(filename),
),
sourceType: 'module',
}).program.body
@ -1448,6 +1463,7 @@ export function inferRuntimeType(
ctx: TypeResolveContext,
node: Node & MaybeWithScope,
scope = node._ownerScope || ctxToScope(ctx),
isKeyOf = false,
): string[] {
try {
switch (node.type) {
@ -1467,8 +1483,29 @@ export function inferRuntimeType(
const types = new Set<string>()
const members =
node.type === 'TSTypeLiteral' ? node.members : node.body.body
for (const m of members) {
if (
if (isKeyOf) {
if (
m.type === 'TSPropertySignature' &&
m.key.type === 'NumericLiteral'
) {
types.add('Number')
} else if (m.type === 'TSIndexSignature') {
const annotation = m.parameters[0].typeAnnotation
if (annotation && annotation.type !== 'Noop') {
const type = inferRuntimeType(
ctx,
annotation.typeAnnotation,
scope,
)[0]
if (type === UNKNOWN_TYPE) return [UNKNOWN_TYPE]
types.add(type)
}
} else {
types.add('String')
}
} else if (
m.type === 'TSCallSignatureDeclaration' ||
m.type === 'TSConstructSignatureDeclaration'
) {
@ -1477,7 +1514,10 @@ export function inferRuntimeType(
types.add('Object')
}
}
return types.size ? Array.from(types) : ['Object']
return types.size
? Array.from(types)
: [isKeyOf ? UNKNOWN_TYPE : 'Object']
}
case 'TSPropertySignature':
if (node.typeAnnotation) {
@ -1512,71 +1552,132 @@ export function inferRuntimeType(
case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
}
if (node.typeName.type === 'Identifier') {
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
case 'Error':
return [node.typeName.name]
if (isKeyOf) {
switch (node.typeName.name) {
case 'String':
case 'Array':
case 'ArrayLike':
case 'Parameters':
case 'ConstructorParameters':
case 'ReadonlyArray':
return ['String', 'Number']
// TS built-in utility types
// https://www.typescriptlang.org/docs/handbook/utility-types.html
case 'Partial':
case 'Required':
case 'Readonly':
case 'Record':
case 'Pick':
case 'Omit':
case 'InstanceType':
return ['Object']
// TS built-in utility types
case 'Record':
case 'Partial':
case 'Required':
case 'Readonly':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
true,
)
}
break
case 'Pick':
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[1],
scope,
)
}
break
case 'Uppercase':
case 'Lowercase':
case 'Capitalize':
case 'Uncapitalize':
return ['String']
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
case 'Error':
case 'Uppercase':
case 'Lowercase':
case 'Capitalize':
case 'Uncapitalize':
case 'ReadonlyMap':
case 'ReadonlySet':
return ['String']
}
} else {
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
case 'Error':
return [node.typeName.name]
case 'Parameters':
case 'ConstructorParameters':
return ['Array']
// TS built-in utility types
// https://www.typescriptlang.org/docs/handbook/utility-types.html
case 'Partial':
case 'Required':
case 'Readonly':
case 'Record':
case 'Pick':
case 'Omit':
case 'InstanceType':
return ['Object']
case 'NonNullable':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
).filter(t => t !== 'null')
}
break
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[1],
scope,
)
}
break
case 'Exclude':
case 'OmitThisParameter':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
)
}
break
case 'Uppercase':
case 'Lowercase':
case 'Capitalize':
case 'Uncapitalize':
return ['String']
case 'Parameters':
case 'ConstructorParameters':
case 'ReadonlyArray':
return ['Array']
case 'ReadonlyMap':
return ['Map']
case 'ReadonlySet':
return ['Set']
case 'NonNullable':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
).filter(t => t !== 'null')
}
break
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[1],
scope,
)
}
break
case 'Exclude':
case 'OmitThisParameter':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope,
)
}
break
}
}
}
// cannot infer, fallback to UNKNOWN: ThisParameterType
@ -1587,9 +1688,9 @@ export function inferRuntimeType(
return inferRuntimeType(ctx, node.typeAnnotation, scope)
case 'TSUnionType':
return flattenTypes(ctx, node.types, scope)
return flattenTypes(ctx, node.types, scope, isKeyOf)
case 'TSIntersectionType': {
return flattenTypes(ctx, node.types, scope).filter(
return flattenTypes(ctx, node.types, scope, isKeyOf).filter(
t => t !== UNKNOWN_TYPE,
)
}
@ -1628,11 +1729,28 @@ export function inferRuntimeType(
// typeof only support identifier in local scope
const matched = scope.declares[id.name]
if (matched) {
return inferRuntimeType(ctx, matched, matched._ownerScope)
return inferRuntimeType(ctx, matched, matched._ownerScope, isKeyOf)
}
}
break
}
// e.g. readonly
case 'TSTypeOperator': {
return inferRuntimeType(
ctx,
node.typeAnnotation,
scope,
node.operator === 'keyof',
)
}
case 'TSAnyKeyword': {
if (isKeyOf) {
return ['String', 'Number', 'Symbol']
}
break
}
}
} catch (e) {
// always soft fail on failed runtime type inference
@ -1644,14 +1762,15 @@ function flattenTypes(
ctx: TypeResolveContext,
types: TSType[],
scope: TypeScope,
isKeyOf: boolean = false,
): string[] {
if (types.length === 1) {
return inferRuntimeType(ctx, types[0], scope)
return inferRuntimeType(ctx, types[0], scope, isKeyOf)
}
return [
...new Set(
([] as string[]).concat(
...types.map(t => inferRuntimeType(ctx, t, scope)),
...types.map(t => inferRuntimeType(ctx, t, scope, isKeyOf)),
),
),
]

View File

@ -25,7 +25,7 @@ describe('ssr: element', () => {
test('v-html', () => {
expect(getCompiledString(`<div v-html="foo"/>`)).toMatchInlineSnapshot(`
"\`<div>\${
_ctx.foo
(_ctx.foo) ?? ''
}</div>\`"
`)
})

View File

@ -143,4 +143,20 @@ describe('ssr: <slot>', () => {
}"
`)
})
test('with v-if inside transition', () => {
const { code } = compile(`<transition><slot v-if="true"/></transition>`)
expect(code).toMatch(ssrHelpers[SSR_RENDER_SLOT_INNER])
expect(code).toMatchInlineSnapshot(`
"const { ssrRenderSlotInner: _ssrRenderSlotInner } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (true) {
_ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
} else {
_push(\`<!---->\`)
}
}"
`)
})
})

View File

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

View File

@ -22,6 +22,7 @@ import {
createAssignmentExpression,
createCallExpression,
createCompilerError,
createCompoundExpression,
createConditionalExpression,
createInterpolation,
createSequenceExpression,
@ -188,7 +189,10 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
// special cases with children override
if (prop.type === NodeTypes.DIRECTIVE) {
if (prop.name === 'html' && prop.exp) {
rawChildrenMap.set(node, prop.exp)
rawChildrenMap.set(
node,
createCompoundExpression([`(`, prop.exp, `) ?? ''`]),
)
} else if (prop.name === 'text' && prop.exp) {
node.children = [createInterpolation(prop.exp, prop.loc)]
} else if (prop.name === 'slot') {

View File

@ -40,24 +40,30 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
// #3989, #9933
// check if this is a single slot inside a transition wrapper - since
// transition/transition-group will unwrap the slot fragment into vnode(s) at runtime,
// we need to avoid rendering the slot as a fragment.
const parent = context.parent
let componentType
if (
parent &&
parent.type === NodeTypes.ELEMENT &&
parent.tagType === ElementTypes.COMPONENT &&
((componentType = resolveComponentType(parent, context, true)) ===
TRANSITION ||
componentType === TRANSITION_GROUP) &&
parent.children.filter(c => c.type === NodeTypes.ELEMENT).length === 1
) {
method = SSR_RENDER_SLOT_INNER
if (!(context.scopeId && context.slotted !== false)) {
args.push('null')
// transition/transition-group will unwrap the slot fragment into vnode(s)
// at runtime, we need to avoid rendering the slot as a fragment.
let parent = context.parent!
if (parent) {
const children = parent.children
// #10743 <slot v-if> in <Transition>
if (parent.type === NodeTypes.IF_BRANCH) {
parent = context.grandParent!
}
let componentType
if (
parent.type === NodeTypes.ELEMENT &&
parent.tagType === ElementTypes.COMPONENT &&
((componentType = resolveComponentType(parent, context, true)) ===
TRANSITION ||
componentType === TRANSITION_GROUP) &&
children.filter(c => c.type === NodeTypes.ELEMENT).length === 1
) {
method = SSR_RENDER_SLOT_INNER
if (!(context.scopeId && context.slotted !== false)) {
args.push('null')
}
args.push('true')
}
args.push('true')
}
node.ssrCodegenNode = createCallExpression(context.helper(method), args)

View File

@ -1501,7 +1501,7 @@ describe('should work when props type is incompatible with setup returned type '
describe('withKeys and withModifiers as pro', () => {
const onKeydown = withKeys(e => {}, [''])
const onClick = withModifiers(e => {}, [''])
const onClick = withModifiers(e => {}, [])
;<input onKeydown={onKeydown} onClick={onClick} />
})

View File

@ -1,4 +1,11 @@
import { type InjectionKey, type Ref, inject, provide, ref } from 'vue'
import {
type InjectionKey,
type Ref,
createApp,
inject,
provide,
ref,
} from 'vue'
import { expectType } from './utils'
// non-symbol keys
@ -40,3 +47,8 @@ provide<Cube>(injectionKeyRef, { size: 123 })
provide<Cube>('cube', { size: 'foo' })
// @ts-expect-error
provide<Cube>(123, { size: 'foo' })
// #10602
const app = createApp({})
// @ts-expect-error
app.provide(injectionKeyRef, ref({}))

View File

@ -120,3 +120,13 @@ describe('should unwrap extended Set correctly', () => {
expectType<string>(eset1.foo)
expectType<number>(eset1.bar)
})
describe('should not error when assignment', () => {
const arr = reactive([''])
let record: Record<number, string>
record = arr
expectType<string>(record[0])
let record2: { [key: number]: string }
record2 = arr
expectType<string>(record2[0])
})

View File

@ -172,6 +172,16 @@ describe('ref with generic', <T extends { name: string }>() => {
expectType<string>(ss.value.name)
})
describe('allow getter and setter types to be unrelated', <T>() => {
const a = { b: ref(0) }
const c = ref(a)
c.value = a
const d = {} as T
const e = ref(d)
e.value = d
})
// shallowRef
type Status = 'initial' | 'ready' | 'invalidating'
const shallowStatus = shallowRef<Status>('initial')
@ -452,3 +462,7 @@ describe('toRef <-> toValue', () => {
),
)
})
// unref
declare const text: ShallowRef<string> | ComputedRef<string> | MaybeRef<string>
expectType<string>(unref(text))

View File

@ -1,7 +1,11 @@
import {
type ComputedRef,
type MaybeRef,
type Ref,
computed,
defineComponent,
defineModel,
reactive,
ref,
shallowRef,
watch,
@ -12,10 +16,17 @@ const source = ref('foo')
const source2 = computed(() => source.value)
const source3 = () => 1
type Bar = Ref<string> | ComputedRef<string> | (() => number)
type Foo = readonly [Ref<string>, ComputedRef<string>, () => number]
type OnCleanup = (fn: () => void) => void
const readonlyArr: Foo = [source, source2, source3]
// lazy watcher will have consistent types for oldValue.
watch(source, (value, oldValue) => {
watch(source, (value, oldValue, onCleanup) => {
expectType<string>(value)
expectType<string>(oldValue)
expectType<OnCleanup>(onCleanup)
})
watch([source, source2, source3], (values, oldValues) => {
@ -29,6 +40,29 @@ watch([source, source2, source3] as const, (values, oldValues) => {
expectType<Readonly<[string, string, number]>>(oldValues)
})
// reactive array
watch(reactive([source, source2, source3]), (value, oldValues) => {
expectType<Bar[]>(value)
expectType<Bar[]>(oldValues)
})
// reactive w/ readonly tuple
watch(reactive([source, source2, source3] as const), (value, oldValues) => {
expectType<Foo>(value)
expectType<Foo>(oldValues)
})
// readonly array
watch(readonlyArr, (values, oldValues) => {
expectType<Readonly<[string, string, number]>>(values)
expectType<Readonly<[string, string, number]>>(oldValues)
})
// no type error, case from vueuse
declare const aAny: any
watch(aAny, (v, ov) => {})
watch(aAny, (v, ov) => {}, { immediate: true })
// immediate watcher's oldValue will be undefined on first run.
watch(
source,
@ -62,6 +96,34 @@ watch(
{ immediate: true },
)
// reactive array
watch(
reactive([source, source2, source3]),
(value, oldVals) => {
expectType<Bar[]>(value)
expectType<Bar[] | undefined>(oldVals)
},
{ immediate: true },
)
// reactive w/ readonly tuple
watch(reactive([source, source2, source3] as const), (value, oldVals) => {
expectType<Foo>(value)
expectType<Foo | undefined>(oldVals)
})
// readonly array
watch(
readonlyArr,
(values, oldValues) => {
expectType<Readonly<[string, string, number]>>(values)
expectType<
Readonly<[string | undefined, string | undefined, number | undefined]>
>(oldValues)
},
{ immediate: true },
)
// should provide correct ref.value inner type to callbacks
const nestedRefSource = ref({
foo: ref(1),
@ -92,9 +154,10 @@ defineComponent({
created() {
this.$watch(
() => this.a,
(v, ov) => {
(v, ov, onCleanup) => {
expectType<number>(v)
expectType<number>(ov)
expectType<OnCleanup>(onCleanup)
},
)
},
@ -141,3 +204,10 @@ defineComponent({
expectType<{ foo: string }>(value)
})
}
{
const css: MaybeRef<string> = ''
watch(ref(css), value => {
expectType<string>(value)
})
}

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

@ -258,7 +258,7 @@ describe('reactivity/computed', () => {
])
})
it('debug: onTrigger', () => {
it('debug: onTrigger (reactive)', () => {
let events: DebuggerEvent[] = []
const onTrigger = vi.fn((e: DebuggerEvent) => {
events.push(e)
@ -618,4 +618,45 @@ describe('reactivity/computed', () => {
expect(serializeInner(root)).toBe('Hello World World World World')
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
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) => {
events.push(e)
})
const obj = ref(1)
const c = computed(() => obj.value, { onTrigger })
// computed won't trigger compute until accessed
c.value
obj.value++
expect(c.value).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1)
expect(events[0]).toEqual({
effect: c.effect,
target: toRaw(obj),
type: TriggerOpTypes.SET,
key: 'value',
oldValue: 1,
newValue: 2,
})
})
})

View File

@ -252,6 +252,22 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(undefined)
})
it('should not observe well-known symbol keyed properties in has operation', () => {
const key = Symbol.isConcatSpreadable
const obj = reactive({
[key]: true,
}) as any
const spy = vi.fn(() => {
key in obj
})
effect(spy)
expect(spy).toHaveBeenCalledTimes(1)
obj[key] = false
expect(spy).toHaveBeenCalledTimes(1)
})
it('should support manipulating an array while observing symbol keyed properties', () => {
const key = Symbol()
let dummy
@ -830,6 +846,31 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(3)
})
it('stop with multiple dependencies', () => {
let dummy1, dummy2
const obj1 = reactive({ prop: 1 })
const obj2 = reactive({ prop: 1 })
const runner = effect(() => {
dummy1 = obj1.prop
dummy2 = obj2.prop
})
obj1.prop = 2
expect(dummy1).toBe(2)
obj2.prop = 3
expect(dummy2).toBe(3)
stop(runner)
obj1.prop = 4
obj2.prop = 5
// Check that both dependencies have been cleared
expect(dummy1).toBe(2)
expect(dummy2).toBe(3)
})
it('events: onStop', () => {
const onStop = vi.fn()
const runner = effect(() => {}, {

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

@ -450,7 +450,7 @@ describe('reactivity/readonly', () => {
bar: markRaw({ b: 2 }),
})
expect(isReadonly(obj.foo)).toBe(true)
expect(isReactive(obj.bar)).toBe(false)
expect(isReadonly(obj.bar)).toBe(false)
})
test('should make ref readonly', () => {

View File

@ -7,6 +7,7 @@ import {
ref,
toRef,
toRefs,
toValue,
} from '../src/index'
import { computed } from '@vue/runtime-dom'
import { customRef, shallowRef, triggerRef, unref } from '../src/ref'
@ -251,6 +252,18 @@ describe('reactivity/ref', () => {
x: 1,
})
const x = toRef(a, 'x')
const b = ref({ y: 1 })
const c = toRef(b)
const d = toRef({ z: 1 })
expect(isRef(d)).toBe(true)
expect(d.value.z).toBe(1)
expect(c).toBe(b)
expect(isRef(x)).toBe(true)
expect(x.value).toBe(1)
@ -442,4 +455,16 @@ describe('reactivity/ref', () => {
expect(a.value).toBe(rr)
expect(a.value).not.toBe(r)
})
test('toValue', () => {
const a = ref(1)
const b = computed(() => a.value + 1)
const c = () => a.value + 2
const d = 4
expect(toValue(a)).toBe(1)
expect(toValue(b)).toBe(2)
expect(toValue(c)).toBe(3)
expect(toValue(d)).toBe(4)
})
})

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.4.22",
"version": "3.4.35",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",

View File

@ -1,4 +1,10 @@
import { toRaw, toReactive, toReadonly } from './reactive'
import {
isReadonly,
isShallow,
toRaw,
toReactive,
toReadonly,
} from './reactive'
import {
ITERATE_KEY,
MAP_KEY_ITERATE_KEY,
@ -72,8 +78,10 @@ function size(target: IterableCollections, isReadonly = false) {
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)
@ -84,8 +92,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)
@ -163,19 +173,6 @@ function createForEach(isReadonly: boolean, isShallow: boolean) {
}
}
interface Iterable {
[Symbol.iterator](): Iterator
}
interface Iterator {
next(value?: any): IterationResult
}
interface IterationResult {
value: any
done: boolean
}
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
@ -184,7 +181,7 @@ function createIterableMethod(
return function (
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
): Iterable<unknown> & Iterator<unknown> {
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)
@ -263,8 +260,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

@ -128,7 +128,7 @@ export class ReactiveEffect<T = any> {
if (this.active) {
preCleanupEffect(this)
postCleanupEffect(this)
this.onStop?.()
this.onStop && this.onStop()
this.active = false
}
}
@ -281,6 +281,7 @@ export function trackEffect(
effect._depsLength++
}
if (__DEV__) {
// eslint-disable-next-line no-restricted-syntax
effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
}
}
@ -309,6 +310,7 @@ export function triggerEffects(
(tracking ??= dep.get(effect) === effect._trackId)
) {
if (__DEV__) {
// eslint-disable-next-line no-restricted-syntax
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
}
effect.trigger()

View File

@ -35,6 +35,8 @@ export {
type DeepReadonly,
type ShallowReactive,
type UnwrapNestedRefs,
type Reactive,
type ReactiveMarker,
} from './reactive'
export {
computed,

View File

@ -58,6 +58,15 @@ function getTargetType(value: Target) {
// only unwrap nested ref
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
declare const ReactiveMarkerSymbol: unique symbol
export declare class ReactiveMarker {
private [ReactiveMarkerSymbol]?: void
}
export type Reactive<T> = UnwrapNestedRefs<T> &
(T extends readonly any[] ? ReactiveMarker : {})
/**
* Returns a reactive proxy of the object.
*
@ -73,7 +82,7 @@ export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
* @param target - The source object.
* @see {@link https://vuejs.org/api/reactivity-core.html#reactive}
*/
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive<T extends object>(target: T): Reactive<T>
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
@ -135,7 +144,7 @@ export function shallowReactive<T extends object>(
}
type Primitive = string | number | boolean | bigint | symbol | undefined | null
type Builtin = Primitive | Function | Date | Error | RegExp
export type Builtin = Primitive | Function | Date | Error | RegExp
export type DeepReadonly<T> = T extends Builtin
? T
: T extends Map<infer K, infer V>
@ -248,7 +257,11 @@ function createReactiveObject(
) {
if (!isObject(target)) {
if (__DEV__) {
warn(`value cannot be made reactive: ${String(target)}`)
warn(
`value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
target,
)}`,
)
}
return target
}

View File

@ -145,6 +145,7 @@ export function trigger(
resetScheduling()
}
export function getDepFromReactive(object: any, key: string | number | symbol) {
return targetMap.get(object)?.get(key)
export function getDepFromReactive(object: any, key: PropertyKey) {
const depsMap = targetMap.get(object)
return depsMap && depsMap.get(key)
}

View File

@ -21,7 +21,7 @@ import {
toRaw,
toReactive,
} from './reactive'
import type { ShallowReactiveMarker } from './reactive'
import type { Builtin, ShallowReactiveMarker } from './reactive'
import { type Dep, createDep } from './dep'
import { ComputedRefImpl } from './computed'
import { getDepFromReactive } from './reactiveEffect'
@ -30,8 +30,9 @@ import { warn } from './warning'
declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol
export interface Ref<T = any> {
value: T
export interface Ref<T = any, S = T> {
get value(): T
set value(_: S)
/**
* Type differentiator only.
* We need this to be in public d.ts but don't want it to show up in IDE
@ -69,6 +70,7 @@ export function triggerRefValue(
ref: RefBase<any>,
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
newVal?: any,
oldVal?: any,
) {
ref = toRaw(ref)
const dep = ref.dep
@ -82,6 +84,7 @@ export function triggerRefValue(
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal,
oldValue: oldVal,
}
: void 0,
)
@ -106,7 +109,7 @@ export function isRef(r: any): r is Ref {
* @param value - The object to wrap in the ref.
* @see {@link https://vuejs.org/api/reactivity-core.html#ref}
*/
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>, UnwrapRef<T> | T>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)
@ -177,9 +180,10 @@ class RefImpl<T> {
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
const oldVal = this._rawValue
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, DirtyLevels.Dirty, newVal)
triggerRefValue(this, DirtyLevels.Dirty, newVal, oldVal)
}
}
}
@ -232,7 +236,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
}
@ -252,7 +256,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)
}
@ -475,11 +481,6 @@ function propertyToRef(
: (new ObjectRefImpl(source, key, defaultValue) as any)
}
// corner case when use narrows type
// Ex. type RelativePath = string & { __brand: unknown }
// RelativePath extends object -> true
type BaseTypes = string | number | boolean
/**
* This is a special exported interface for other packages to declare
* additional types that should bail out for ref unwrapping. For example
@ -496,10 +497,10 @@ type BaseTypes = string | number | boolean
export interface RefUnwrapBailTypes {}
export type ShallowUnwrapRef<T> = {
[K in keyof T]: DistrubuteRef<T[K]>
[K in keyof T]: DistributeRef<T[K]>
}
type DistrubuteRef<T> = T extends Ref<infer V> ? V : T
type DistributeRef<T> = T extends Ref<infer V> ? V : T
export type UnwrapRef<T> =
T extends ShallowRef<infer V>
@ -509,8 +510,7 @@ export type UnwrapRef<T> =
: UnwrapRefSimple<T>
export type UnwrapRefSimple<T> = T extends
| Function
| BaseTypes
| Builtin
| Ref
| RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
| { [RawSymbol]?: true }

View File

@ -13,7 +13,6 @@ import {
} from '../src/index'
import { createApp, nodeOps, render, serialize } from '@vue/runtime-test'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject
describe('api: provide/inject', () => {
it('string keys', () => {
const Provider = {

View File

@ -1,8 +1,10 @@
import {
KeepAlive,
TrackOpTypes,
h,
nextTick,
nodeOps,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
@ -22,8 +24,6 @@ import {
TriggerOpTypes,
} from '@vue/reactivity'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks
describe('api: lifecycle hooks', () => {
it('onBeforeMount', () => {
const root = nodeOps.createElement('div')
@ -40,6 +40,8 @@ describe('api: lifecycle hooks', () => {
}
render(h(Comp), root)
expect(fn).toHaveBeenCalledTimes(1)
// #10863
expect(fn).toHaveBeenCalledWith()
})
it('onMounted', () => {
@ -405,4 +407,60 @@ describe('api: lifecycle hooks', () => {
await nextTick()
expect(fn).toHaveBeenCalledTimes(4)
})
it('immediately trigger unmount during rendering', async () => {
const fn = vi.fn()
const toggle = ref(false)
const Child = {
setup() {
onMounted(fn)
// trigger unmount immediately
toggle.value = false
return () => h('div')
},
}
const Comp = {
setup() {
return () => (toggle.value ? [h(Child)] : null)
},
}
render(h(Comp), nodeOps.createElement('div'))
toggle.value = true
await nextTick()
expect(fn).toHaveBeenCalledTimes(0)
})
it('immediately trigger unmount during rendering(with KeepAlive)', async () => {
const mountedSpy = vi.fn()
const activeSpy = vi.fn()
const toggle = ref(false)
const Child = {
setup() {
onMounted(mountedSpy)
onActivated(activeSpy)
// trigger unmount immediately
toggle.value = false
return () => h('div')
},
}
const Comp = {
setup() {
return () => h(KeepAlive, [toggle.value ? h(Child) : null])
},
}
render(h(Comp), nodeOps.createElement('div'))
toggle.value = true
await nextTick()
expect(mountedSpy).toHaveBeenCalledTimes(0)
expect(activeSpy).toHaveBeenCalledTimes(0)
})
})

View File

@ -12,8 +12,6 @@ import {
watchEffect,
} from '@vue/runtime-test'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#setup
describe('api: setup context', () => {
it('should expose return values to template render context', () => {
const Comp = defineComponent({

View File

@ -5,6 +5,7 @@ import {
defineComponent,
getCurrentInstance,
nextTick,
onErrorCaptured,
reactive,
ref,
watch,
@ -35,8 +36,6 @@ import {
triggerRef,
} from '@vue/reactivity'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
describe('api: watch', () => {
it('effect', async () => {
const state = reactive({ count: 0 })
@ -96,6 +95,30 @@ describe('api: watch', () => {
expect(spy).toBeCalledWith([1], [1], expect.anything())
})
it('should not call functions inside a reactive source array', () => {
const spy1 = vi.fn()
const array = reactive([spy1])
const spy2 = vi.fn()
watch(array, spy2, { immediate: true })
expect(spy1).toBeCalledTimes(0)
expect(spy2).toBeCalledWith([spy1], undefined, expect.anything())
})
it('should not unwrap refs in a reactive source array', async () => {
const val = ref({ foo: 1 })
const array = reactive([val])
const spy = vi.fn()
watch(array, spy, { immediate: true })
expect(spy).toBeCalledTimes(1)
expect(spy).toBeCalledWith([val], undefined, expect.anything())
// deep by default
val.value.foo++
await nextTick()
expect(spy).toBeCalledTimes(2)
expect(spy).toBeCalledWith([val], [val], expect.anything())
})
it('should not fire if watched getter result did not change', async () => {
const spy = vi.fn()
const n = ref(0)
@ -186,6 +209,24 @@ describe('api: watch', () => {
expect(dummy).toBe(1)
})
it('directly watching reactive array with explicit deep: false', async () => {
const val = ref(1)
const array: any[] = reactive([val])
const spy = vi.fn()
watch(array, spy, { immediate: true, deep: false })
expect(spy).toBeCalledTimes(1)
expect(spy).toBeCalledWith([val], undefined, expect.anything())
val.value++
await nextTick()
expect(spy).toBeCalledTimes(1)
array[1] = 2
await nextTick()
expect(spy).toBeCalledTimes(2)
expect(spy).toBeCalledWith([val, 2], [val, 2], expect.anything())
})
// #9916
it('watching shallow reactive array with deep: false', async () => {
class foo {
@ -890,6 +931,52 @@ describe('api: watch', () => {
expect(dummy).toEqual([1, 2])
})
it('deep with symbols', async () => {
const symbol1 = Symbol()
const symbol2 = Symbol()
const symbol3 = Symbol()
const symbol4 = Symbol()
const raw: any = {
[symbol1]: {
[symbol2]: 1,
},
}
Object.defineProperty(raw, symbol3, {
writable: true,
enumerable: false,
value: 1,
})
const state = reactive(raw)
const spy = vi.fn()
watch(() => state, spy, { deep: true })
await nextTick()
expect(spy).toHaveBeenCalledTimes(0)
state[symbol1][symbol2] = 2
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
// Non-enumerable properties don't trigger deep watchers
state[symbol3] = 3
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
// Adding a new symbol property
state[symbol4] = 1
await nextTick()
expect(spy).toHaveBeenCalledTimes(2)
// Removing a symbol property
delete state[symbol4]
await nextTick()
expect(spy).toHaveBeenCalledTimes(3)
})
it('immediate', async () => {
const count = ref(0)
const cb = vi.fn()
@ -1474,4 +1561,76 @@ describe('api: watch', () => {
unwatch!()
expect(scope.effects.length).toBe(0)
})
test('circular reference', async () => {
const obj = { a: 1 }
// @ts-expect-error
obj.b = obj
const foo = ref(obj)
const spy = vi.fn()
watch(foo, spy, { deep: true })
// @ts-expect-error
foo.value.b.a = 2
await nextTick()
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

@ -11,6 +11,7 @@ import {
getCurrentInstance,
h,
inject,
nextTick,
nodeOps,
provide,
ref,
@ -19,7 +20,7 @@ import {
toRefs,
watch,
} from '@vue/runtime-test'
import { render as domRender, nextTick } from 'vue'
import { render as domRender } from 'vue'
describe('component props', () => {
test('stateful', () => {
@ -537,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: {
@ -748,4 +839,39 @@ describe('component props', () => {
expect(`Invalid prop name: "ref"`).toHaveBeenWarned()
expect(`Invalid prop name: "$foo"`).toHaveBeenWarned()
})
// #5517
test('events should not be props when component updating', async () => {
let props: any
function eventHandler() {}
const foo = ref(1)
const Child = defineComponent({
setup(_props) {
props = _props
},
emits: ['event'],
props: ['foo'],
template: `<div>{{ foo }}</div>`,
})
const Comp = defineComponent({
setup() {
return {
foo,
eventHandler,
}
},
components: { Child },
template: `<Child @event="eventHandler" :foo="foo" />`,
})
const root = document.createElement('div')
domRender(h(Comp), root)
expect(props).not.toHaveProperty('onEvent')
foo.value++
await nextTick()
expect(props).not.toHaveProperty('onEvent')
})
})

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)', () => {

View File

@ -7,7 +7,6 @@ import {
h,
nextTick,
nodeOps,
onUnmounted,
ref,
render,
serialize,
@ -769,42 +768,6 @@ describe('BaseTransition', () => {
test('w/ KeepAlive', async () => {
await runTestWithKeepAlive(testOutIn)
})
test('w/ KeepAlive + unmount innerChild', async () => {
const unmountSpy = vi.fn()
const includeRef = ref(['TrueBranch'])
const trueComp = {
name: 'TrueBranch',
setup() {
onUnmounted(unmountSpy)
const count = ref(0)
return () => h('div', count.value)
},
}
const toggle = ref(true)
const { props } = mockProps({ mode: 'out-in' }, true /*withKeepAlive*/)
const root = nodeOps.createElement('div')
const App = {
render() {
return h(BaseTransition, props, () => {
return h(
KeepAlive,
{ include: includeRef.value },
toggle.value ? h(trueComp) : h('div'),
)
})
},
}
render(h(App), root)
// trigger toggle
toggle.value = false
includeRef.value = []
await nextTick()
expect(unmountSpy).toHaveBeenCalledTimes(1)
})
})
// #6835
@ -1230,4 +1193,9 @@ describe('BaseTransition', () => {
await runTestWithKeepAlive(testInOutBeforeFinish)
})
})
// #10719
test('should not error on KeepAlive w/ function children', () => {
expect(() => mount({}, () => () => h('div'), true)).not.toThrow()
})
})

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