Merge branch 'main' into feat/reactiveity-transform-shorthands

This commit is contained in:
Anthony Fu 2022-05-13 16:19:09 +08:00
commit 3a8cc1f211
130 changed files with 2547 additions and 1095 deletions

View File

@ -7,6 +7,7 @@ module.exports = {
sourceType: 'module' sourceType: 'module'
}, },
rules: { rules: {
'no-debugger': 'error',
'no-unused-vars': [ 'no-unused-vars': [
'error', 'error',
// we are only using this rule to check for unused arguments since TS // we are only using this rule to check for unused arguments since TS
@ -16,10 +17,11 @@ module.exports = {
// most of the codebase are expected to be env agnostic // most of the codebase are expected to be env agnostic
'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals], 'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals],
// since we target ES2015 for baseline support, we need to forbid object // since we target ES2015 for baseline support, we need to forbid object
// rest spread usage (both assign and destructure) // rest spread usage in destructure as it compiles into a verbose helper.
// TS now compiles assignment spread into Object.assign() calls so that
// is allowed.
'no-restricted-syntax': [ 'no-restricted-syntax': [
'error', 'error',
'ObjectExpression > SpreadElement',
'ObjectPattern > RestElement', 'ObjectPattern > RestElement',
'AwaitExpression' 'AwaitExpression'
] ]

68
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,68 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve
body:
- type: markdown
attributes:
value: |
**Before You Start...**
This form is only for submitting bug reports. If you have a usage question
or are unsure if this is really a bug, make sure to:
- Read the [docs](https://vuejs.org/)
- Ask on [Discord Chat](https://chat.vuejs.org/)
- Ask on [GitHub Discussions](https://github.com/vuejs/core/discussions)
- Look for / ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=vue.js)
Also try to search for your issue - it may have already been answered or even fixed in the development branch.
However, if you find that an old, closed issue still persists in the latest version,
you should open a new issue using the form below instead of commenting on the old issue.
- type: input
id: reproduction-link
attributes:
label: Link to minimal reproduction
description: |
The easiest way to provide a reproduction is by showing the bug in [The SFC Playground](https://sfc.vuejs.org/).
If it cannot be reproduced in the playground and requires a proper build setup, try [StackBlitz](https://vite.new/vue).
If neither of these are suitable, you can always provide a GitHub reporistory.
The reproduction should be **minimal** - i.e. it should contain only the bare minimum amount of code needed
to show the bug. See [Bug Reproduction Guidelines](https://github.com/vuejs/core/blob/main/.github/bug-repro-guidelines.md) for more details.
Please do not just fill in a random link. The issue will be closed if no valid reproduction is provided.
placeholder: Reproduction Link
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
placeholder: Steps to reproduce
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is expected?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: What is actually happening?
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --npmPackages vue --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
- type: textarea
id: additional-comments
attributes:
label: Any additional comments?
description: e.g. some background/context of how you ran into this bug.

View File

@ -1,8 +1,11 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Create new issue - name: Discord Chat
url: https://new-issue.vuejs.org/?repo=vuejs/core url: https://chat.vuejs.org
about: Please use the following link to create a new issue. about: Ask questions and discuss with other Vue users in real time.
- name: Questions & Discussions
url: https://github.com/vuejs/core/discussions
about: Use GitHub discussions for message-board style questions and discussions.
- name: Patreon - name: Patreon
url: https://www.patreon.com/evanyou url: https://www.patreon.com/evanyou
about: Love Vue.js? Please consider supporting us via Patreon. about: Love Vue.js? Please consider supporting us via Patreon.

View File

@ -0,0 +1,39 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
labels: [":sparkles: feature request"]
body:
- type: markdown
attributes:
value: |
**Before You Start...**
This form is only for submitting feature requests. If you have a usage question
or are unsure if this is really a bug, make sure to:
- Read the [docs](https://vuejs.org/)
- Ask on [Discord Chat](https://chat.vuejs.org/)
- Ask on [GitHub Discussions](https://github.com/vuejs/core/discussions)
- Look for / ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=vue.js)
Also try to search for your issue - another user may have already requested something similar!
- type: textarea
id: problem-description
attributes:
label: What problem does this feature solve?
description: |
Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?
An important design goal of Vue is keeping the API surface small and straightforward. In general, we only consider adding new features that solve a problem that cannot be easily dealt with using existing APIs (i.e. not just an alternative way of doing things that can already be done). The problem should also be common enough to justify the addition.
placeholder: Problem description
validations:
required: true
- type: textarea
id: proposed-API
attributes:
label: What does the proposed API look like?
description: |
Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format your code blocks.
placeholder: Steps to reproduce
validations:
required: true

29
.github/bug-repro-guidelines.md vendored Normal file
View File

@ -0,0 +1,29 @@
## About Bug Reproductions
A bug reproduction is a piece of code that can run and demonstrate how a bug can happen.
### Text is not enough
It's impossible to fix a bug from mere text descriptions. First, it's very difficult to precisely describe a technical problem while keeping it easy to follow; Second, the real cause may very well be something that you forgot to even mention. A reproduction is the only way that can reliably help us understand what is going on, so please provide one.
### A repro must be runnable
Screenshots or videos are NOT reproductions! They only show that the bug exists, but do not provide enough information on why it happens. Only runnable code provides the most complete context and allows us to properly debug the scenario. That said, in some cases videos/gifs can help explain interaction issues that are hard to describe in text.
### A repro should be minimal
Some users would give us a link to a real project and hope we can help them figure out what is wrong. We generally do not accept such requests because:
You are already familiar with your codebase, but we are not. It is extremely time-consuming to hunt a bug in a big and unfamiliar codebase.
The problematic behavior may very well be caused by your code rather than by a bug in Vue.
A minimal reproduction means it demonstrates the bug, and the bug only. It should only contain the bare minimum amount of code that can reliably cause the bug. Try your best to get rid of anything that aren't directly related to the problem.
### How to create a repro
For Vue 3 core reproductions, try reproducing it in [The SFC Playground](https://sfc.vuejs.org/).
If it cannot be reproduced in the playground and requires a proper build setup, try [StackBlitz](https://vite.new/vue).
If neither of these are suitable, you can always provide a GitHub repository.

View File

@ -17,7 +17,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
## Pull Request Guidelines ## Pull Request Guidelines
- Checkout a topic branch from a base branch, e.g. `master`, and merge back against that branch. - Checkout a topic branch from a base branch, e.g. `main`, and merge back against that branch.
- If adding a new feature: - If adding a new feature:
@ -40,7 +40,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
## Development Setup ## Development Setup
You will need [Node.js](https://nodejs.org) **version 16+**, and [PNPM](https://pnpm.io). You will need [Node.js](https://nodejs.org) **version 16+**, and [PNPM](https://pnpm.io) **version 7+**.
We also recommend installing [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. We also recommend installing [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

@ -7,7 +7,7 @@ on:
branches: branches:
- main - main
jobs: jobs:
test: unit-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -15,7 +15,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2.0.1 uses: pnpm/action-setup@v2.0.1
with: with:
version: 6.15.1 version: 7.0.1
- name: Set node version to 16 - name: Set node version to 16
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -26,9 +26,9 @@ jobs:
- run: pnpm install - run: pnpm install
- name: Run unit tests - name: Run unit tests
run: pnpm run test run: pnpm run test-unit
test-dts: e2e-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -36,7 +36,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2.0.1 uses: pnpm/action-setup@v2.0.1
with: with:
version: 6.15.1 version: 7.0.1
- name: Set node version to 16 - name: Set node version to 16
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -46,6 +46,30 @@ jobs:
- run: pnpm install - run: pnpm install
- name: Run e2e tests
run: pnpm run test-e2e
lint-and-test-dts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install pnpm
uses: pnpm/action-setup@v2.0.1
with:
version: 7.0.1
- name: Set node version to 16
uses: actions/setup-node@v2
with:
node-version: 16
cache: 'pnpm'
- run: pnpm install
- name: Run eslint
run: pnpm run lint
- name: Run type declaration tests - name: Run type declaration tests
run: pnpm run test-dts run: pnpm run test-dts
@ -59,7 +83,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2.0.1 uses: pnpm/action-setup@v2.0.1
with: with:
version: 6.15.1 version: 7.0.1
- name: Set node version to 16 - name: Set node version to 16
uses: actions/setup-node@v2 uses: actions/setup-node@v2

View File

@ -9,7 +9,7 @@
"size-baseline": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler && cd packages/size-check && vite build && node brotli", "size-baseline": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler && cd packages/size-check && vite build && node brotli",
"lint": "eslint --ext .ts packages/*/src/**.ts", "lint": "eslint --ext .ts packages/*/src/**.ts",
"format": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"", "format": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"",
"test": "run-s \"test-unit -- {@}\" \"test-e2e -- {@}\" --", "test": "run-s \"test-unit {@}\" \"test-e2e {@}\"",
"test-unit": "jest --filter ./scripts/filter-unit.js", "test-unit": "jest --filter ./scripts/filter-unit.js",
"test-e2e": "node scripts/build.js vue -f global -d && jest --filter ./scripts/filter-e2e.js --runInBand", "test-e2e": "node scripts/build.js vue -f global -d && jest --filter ./scripts/filter-e2e.js --runInBand",
"test-dts": "node scripts/build.js shared reactivity runtime-core runtime-dom -dt -f esm-bundler && npm run test-dts-only", "test-dts": "node scripts/build.js shared reactivity runtime-core runtime-dom -dt -f esm-bundler && npm run test-dts-only",
@ -18,7 +18,7 @@
"release": "node scripts/release.js", "release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"dev-compiler": "run-p \"dev template-explorer\" serve", "dev-compiler": "run-p \"dev template-explorer\" serve",
"dev-sfc": "run-p \"dev compiler-sfc -- -f esm-browser\" \"dev vue -- -if esm-bundler-runtime \" serve-sfc-playground", "dev-sfc": "run-p \"dev compiler-sfc -f esm-browser\" \"dev vue -if esm-bundler-runtime \" serve-sfc-playground",
"serve-sfc-playground": "vite packages/sfc-playground --host", "serve-sfc-playground": "vite packages/sfc-playground --host",
"serve": "serve", "serve": "serve",
"open": "open http://localhost:5000/packages/template-explorer/local.html", "open": "open http://localhost:5000/packages/template-explorer/local.html",
@ -58,7 +58,7 @@
"@types/jest": "^27.0.1", "@types/jest": "^27.0.1",
"@types/node": "^16.4.7", "@types/node": "^16.4.7",
"@types/puppeteer": "^5.0.0", "@types/puppeteer": "^5.0.0",
"@typescript-eslint/parser": "^4.1.1", "@typescript-eslint/parser": "^5.23.0",
"@vue/reactivity": "workspace:*", "@vue/reactivity": "workspace:*",
"@vue/runtime-core": "workspace:*", "@vue/runtime-core": "workspace:*",
"@vue/runtime-dom": "workspace:*", "@vue/runtime-dom": "workspace:*",
@ -89,9 +89,9 @@
"serve": "^12.0.0", "serve": "^12.0.0",
"todomvc-app-css": "^2.3.0", "todomvc-app-css": "^2.3.0",
"ts-jest": "^27.0.5", "ts-jest": "^27.0.5",
"tslib": "^2.3.1", "tslib": "^2.4.0",
"typescript": "^4.2.2", "typescript": "^4.6.4",
"vite": "^2.9.0", "vite": "^2.9.8",
"vue": "workspace:*", "vue": "workspace:*",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }

View File

@ -16,7 +16,7 @@ return function render(_ctx, _cache) {
? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, \\"yes\\")) ? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, \\"yes\\"))
: (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [ : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
_createTextVNode(\\"no\\") _createTextVNode(\\"no\\")
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)), ], 64 /* STABLE_FRAGMENT */)),
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (value, index) => { (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (value, index) => {
return (_openBlock(), _createElementBlock(\\"div\\", null, [ return (_openBlock(), _createElementBlock(\\"div\\", null, [
_createElementVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */) _createElementVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */)
@ -40,7 +40,7 @@ return function render(_ctx, _cache) {
? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, \\"yes\\")) ? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, \\"yes\\"))
: (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [ : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
_createTextVNode(\\"no\\") _createTextVNode(\\"no\\")
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)), ], 64 /* STABLE_FRAGMENT */)),
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => { (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => {
return (_openBlock(), _createElementBlock(\\"div\\", null, [ return (_openBlock(), _createElementBlock(\\"div\\", null, [
_createElementVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */) _createElementVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */)
@ -63,7 +63,7 @@ export function render(_ctx, _cache) {
? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, \\"yes\\")) ? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, \\"yes\\"))
: (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [ : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
_createTextVNode(\\"no\\") _createTextVNode(\\"no\\")
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)), ], 64 /* STABLE_FRAGMENT */)),
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => { (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (value, index) => {
return (_openBlock(), _createElementBlock(\\"div\\", null, [ return (_openBlock(), _createElementBlock(\\"div\\", null, [
_createElementVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */) _createElementVNode(\\"span\\", null, _toDisplayString(value + index), 1 /* TEXT */)

View File

@ -1037,7 +1037,7 @@ describe('compiler: parse', () => {
offset: 0 offset: 0
} }
}, },
ns: 0, ns: Namespaces.HTML,
props: [ props: [
{ {
loc: { loc: {
@ -1054,7 +1054,7 @@ describe('compiler: parse', () => {
} }
}, },
name: 'class', name: 'class',
type: 6, type: NodeTypes.ATTRIBUTE,
value: { value: {
content: 'c', content: 'c',
loc: { loc: {
@ -1070,13 +1070,13 @@ describe('compiler: parse', () => {
offset: 11 offset: 11
} }
}, },
type: 2 type: NodeTypes.TEXT
} }
} }
], ],
tag: 'div', tag: 'div',
tagType: 0, tagType: ElementTypes.ELEMENT,
type: 1 type: NodeTypes.ELEMENT
}) })
}) })
@ -2023,7 +2023,7 @@ foo
isPreTag: tag => tag === 'pre' isPreTag: tag => tag === 'pre'
}) })
const elementAfterPre = ast.children[1] as ElementNode const elementAfterPre = ast.children[1] as ElementNode
// should not affect the <span> and condense its whitepsace inside // should not affect the <span> and condense its whitespace inside
expect((elementAfterPre.children[0] as TextNode).content).toBe(` foo bar`) expect((elementAfterPre.children[0] as TextNode).content).toBe(` foo bar`)
}) })

View File

@ -111,7 +111,7 @@ return function render(_ctx, _cache) {
? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 })) ? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }))
: orNot : orNot
? (_openBlock(), _createElementBlock(\\"p\\", { key: 1 })) ? (_openBlock(), _createElementBlock(\\"p\\", { key: 1 }))
: (_openBlock(), _createElementBlock(_Fragment, { key: 2 }, [\\"fine\\"], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)) : (_openBlock(), _createElementBlock(_Fragment, { key: 2 }, [\\"fine\\"], 64 /* STABLE_FRAGMENT */))
} }
}" }"
`; `;

View File

@ -80,7 +80,7 @@ describe('compiler: element transform', () => {
expect(root.components).toContain(`Foo`) expect(root.components).toContain(`Foo`)
}) })
test('resolve implcitly self-referencing component', () => { test('resolve implicitly self-referencing component', () => {
const { root } = parseWithElementTransform(`<Example/>`, { const { root } = parseWithElementTransform(`<Example/>`, {
filename: `/foo/bar/Example.vue?vue&type=template` filename: `/foo/bar/Example.vue?vue&type=template`
}) })
@ -807,6 +807,37 @@ describe('compiler: element transform', () => {
}) })
}) })
test(':style with array literal', () => {
const { node, root } = parseWithElementTransform(
`<div :style="[{ color: 'red' }]" />`,
{
nodeTransforms: [transformExpression, transformStyle, transformElement],
directiveTransforms: {
bind: transformBind
},
prefixIdentifiers: true
}
)
expect(root.helpers).toContain(NORMALIZE_STYLE)
expect(node.props).toMatchObject({
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
type: NodeTypes.JS_PROPERTY,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `style`,
isStatic: true
},
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: NORMALIZE_STYLE
}
}
]
})
})
test(`props merging: class`, () => { test(`props merging: class`, () => {
const { node, root } = parseWithElementTransform( const { node, root } = parseWithElementTransform(
`<div class="foo" :class="{ bar: isBar }" />`, `<div class="foo" :class="{ bar: isBar }" />`,

View File

@ -259,6 +259,7 @@ export interface IfBranchNode extends Node {
condition: ExpressionNode | undefined // else condition: ExpressionNode | undefined // else
children: TemplateChildNode[] children: TemplateChildNode[]
userKey?: AttributeNode | DirectiveNode userKey?: AttributeNode | DirectiveNode
isTemplateIf?: boolean
} }
export interface ForNode extends Node { export interface ForNode extends Node {

View File

@ -58,6 +58,8 @@ import { ImportItem } from './transform'
const PURE_ANNOTATION = `/*#__PURE__*/` const PURE_ANNOTATION = `/*#__PURE__*/`
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
export interface CodegenResult { export interface CodegenResult {
@ -247,11 +249,7 @@ export function generate(
// function mode const declarations should be inside with block // function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties // also they should be renamed to avoid collision with user properties
if (hasHelpers) { if (hasHelpers) {
push( push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = _Vue`)
`const { ${ast.helpers
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue`
)
push(`\n`) push(`\n`)
newline() newline()
} }
@ -328,7 +326,6 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
!__BROWSER__ && ssr !__BROWSER__ && ssr
? `require(${JSON.stringify(runtimeModuleName)})` ? `require(${JSON.stringify(runtimeModuleName)})`
: runtimeGlobalName : runtimeGlobalName
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
// Generate const declaration for helpers // Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done // In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the // only once; But if we not prefixing, we place the declaration inside the

View File

@ -59,6 +59,7 @@ export {
PropsExpression PropsExpression
} from './transforms/transformElement' } from './transforms/transformElement'
export { processSlotOutlet } from './transforms/transformSlotOutlet' export { processSlotOutlet } from './transforms/transformSlotOutlet'
export { getConstantType } from './transforms/hoistStatic'
export { generateCodeFrame } from '@vue/shared' export { generateCodeFrame } from '@vue/shared'
// v2 compat only // v2 compat only

View File

@ -97,6 +97,10 @@ export const enum BindingTypes {
* template expressions. * template expressions.
*/ */
SETUP_CONST = 'setup-const', SETUP_CONST = 'setup-const',
/**
* a const binding that does not need `unref()`, but may be mutated.
*/
SETUP_REACTIVE_CONST = 'setup-reactive-const',
/** /**
* a const binding that may be a ref. * a const binding that may be a ref.
*/ */

View File

@ -352,7 +352,9 @@ function resolveSetupReference(name: string, context: TransformContext) {
} }
} }
const fromConst = checkType(BindingTypes.SETUP_CONST) const fromConst =
checkType(BindingTypes.SETUP_CONST) ||
checkType(BindingTypes.SETUP_REACTIVE_CONST)
if (fromConst) { if (fromConst) {
return context.inline return context.inline
? // in inline mode, const setup bindings (e.g. imports) can be used as-is ? // in inline mode, const setup bindings (e.g. imports) can be used as-is
@ -765,10 +767,11 @@ export function buildProps(
} }
if ( if (
styleProp && styleProp &&
!isStaticExp(styleProp.value) &&
// the static style is compiled into an object, // the static style is compiled into an object,
// so use `hasStyleBinding` to ensure that it is a dynamic style binding // so use `hasStyleBinding` to ensure that it is a dynamic style binding
(hasStyleBinding || (hasStyleBinding ||
(styleProp.value.type === NodeTypes.SIMPLE_EXPRESSION &&
styleProp.value.content.trim()[0] === `[`) ||
// v-bind:style and style both exist, // v-bind:style and style both exist,
// v-bind:style with static literal object // v-bind:style with static literal object
styleProp.value.type === NodeTypes.JS_ARRAY_EXPRESSION) styleProp.value.type === NodeTypes.JS_ARRAY_EXPRESSION)

View File

@ -24,7 +24,13 @@ import {
walkIdentifiers walkIdentifiers
} from '../babelUtils' } from '../babelUtils'
import { advancePositionWithClone, isSimpleIdentifier } from '../utils' import { advancePositionWithClone, isSimpleIdentifier } from '../utils'
import { isGloballyWhitelisted, makeMap, hasOwn, isString } from '@vue/shared' import {
isGloballyWhitelisted,
makeMap,
hasOwn,
isString,
genPropsAccessExp
} from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
Node, Node,
@ -122,7 +128,11 @@ export function processExpression(
const isDestructureAssignment = const isDestructureAssignment =
parent && isInDestructureAssignment(parent, parentStack) parent && isInDestructureAssignment(parent, parentStack)
if (type === BindingTypes.SETUP_CONST || localVars[raw]) { if (
type === BindingTypes.SETUP_CONST ||
type === BindingTypes.SETUP_REACTIVE_CONST ||
localVars[raw]
) {
return raw return raw
} else if (type === BindingTypes.SETUP_REF) { } else if (type === BindingTypes.SETUP_REF) {
return `${raw}.value` return `${raw}.value`
@ -181,17 +191,17 @@ export function processExpression(
} else if (type === BindingTypes.PROPS) { } else if (type === BindingTypes.PROPS) {
// use __props which is generated by compileScript so in ts mode // use __props which is generated by compileScript so in ts mode
// it gets correct type // it gets correct type
return `__props.${raw}` return genPropsAccessExp(raw)
} else if (type === BindingTypes.PROPS_ALIASED) { } else if (type === BindingTypes.PROPS_ALIASED) {
// prop with a different local alias (from defineProps() destructure) // prop with a different local alias (from defineProps() destructure)
return `__props.${bindingMetadata.__propsAliases![raw]}` return genPropsAccessExp(bindingMetadata.__propsAliases![raw])
} }
} else { } else {
if (type && type.startsWith('setup')) { if (type && type.startsWith('setup')) {
// setup bindings in non-inline mode // setup bindings in non-inline mode
return `$setup.${raw}` return `$setup.${raw}`
} else if (type === BindingTypes.PROPS_ALIASED) { } else if (type === BindingTypes.PROPS_ALIASED) {
return `$props.${bindingMetadata.__propsAliases![raw]}` return `$props['${bindingMetadata.__propsAliases![raw]}']`
} else if (type) { } else if (type) {
return `$${type}.${raw}` return `$${type}.${raw}`
} }

View File

@ -209,15 +209,14 @@ export function processIf(
} }
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode { function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
const isTemplateIf = node.tagType === ElementTypes.TEMPLATE
return { return {
type: NodeTypes.IF_BRANCH, type: NodeTypes.IF_BRANCH,
loc: node.loc, loc: node.loc,
condition: dir.name === 'else' ? undefined : dir.exp, condition: dir.name === 'else' ? undefined : dir.exp,
children: children: isTemplateIf && !findDir(node, 'for') ? node.children : [node],
node.tagType === ElementTypes.TEMPLATE && !findDir(node, 'for') userKey: findProp(node, `key`),
? node.children isTemplateIf
: [node],
userKey: findProp(node, `key`)
} }
} }
@ -274,6 +273,7 @@ function createChildrenCodegenNode(
// the rest being comments // the rest being comments
if ( if (
__DEV__ && __DEV__ &&
!branch.isTemplateIf &&
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1 children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) { ) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT

View File

@ -0,0 +1,166 @@
import { compile } from '../../src'
describe('Transition multi children warnings', () => {
function checkWarning(
template: string,
shouldWarn: boolean,
message = `<Transition> expects exactly one child element or component.`
) {
const spy = jest.fn()
compile(template.trim(), {
hoistStatic: true,
transformHoist: null,
onError: err => {
spy(err.message)
}
})
if (shouldWarn) expect(spy).toHaveBeenCalledWith(message)
else expect(spy).not.toHaveBeenCalled()
}
test('warns if multiple children', () => {
checkWarning(
`
<transition>
<div>hey</div>
<div>hey</div>
</transition>
`,
true
)
})
test('warns with v-for', () => {
checkWarning(
`
<transition>
<div v-for="i in items">hey</div>
</transition>
`,
true
)
})
test('warns with multiple v-if + v-for', () => {
checkWarning(
`
<transition>
<div v-if="a" v-for="i in items">hey</div>
<div v-else v-for="i in items">hey</div>
</transition>
`,
true
)
})
test('warns with template v-if', () => {
checkWarning(
`
<transition>
<template v-if="ok"></template>
</transition>
`,
true
)
})
test('warns with multiple templates', () => {
checkWarning(
`
<transition>
<template v-if="a"></template>
<template v-else></template>
</transition>
`,
true
)
})
test('warns if multiple children with v-if', () => {
checkWarning(
`
<transition>
<div v-if="one">hey</div>
<div v-if="other">hey</div>
</transition>
`,
true
)
})
test('does not warn with regular element', () => {
checkWarning(
`
<transition>
<div>hey</div>
</transition>
`,
false
)
})
test('does not warn with one single v-if', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
</transition>
`,
false
)
})
test('does not warn with v-if v-else-if v-else', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
<div v-else-if="b">hey</div>
<div v-else>hey</div>
</transition>
`,
false
)
})
test('does not warn with v-if v-else', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
<div v-else>hey</div>
</transition>
`,
false
)
})
})
test('inject persisted when child has v-show', () => {
expect(
compile(`
<transition>
<div v-show="ok" />
</transition>
`).code
).toMatchSnapshot()
})
test('the v-if/else-if/else branches in Transition should ignore comments', () => {
expect(
compile(`
<transition>
<div v-if="a">hey</div>
<!-- this should be ignored -->
<div v-else-if="b">hey</div>
<!-- this should be ignored -->
<div v-else>
<p v-if="c"/>
<!-- this should not be ignored -->
<p v-else/>
</div>
</transition>
`).code
).toMatchSnapshot()
})

View File

@ -1,6 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`the v-if/else-if/else branchs in Transition should ignore comments 1`] = ` exports[`inject persisted when child has v-show 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { vShow: _vShow, createElementVNode: _createElementVNode, withDirectives: _withDirectives, Transition: _Transition, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(_Transition, { persisted: \\"\\" }, {
default: _withCtx(() => [
_withDirectives(_createElementVNode(\\"div\\", null, null, 512 /* NEED_PATCH */), [
[_vShow, ok]
])
]),
_: 1 /* STABLE */
}))
}
}"
`;
exports[`the v-if/else-if/else branches in Transition should ignore comments 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {

View File

@ -32,3 +32,23 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2)) return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
}" }"
`; `;
exports[`stringify static html stringify v-html 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"<pre data-type=\\\\\\"js\\\\\\"><code><span>show-it </span></code></pre><div class><span class>1</span><span class>2</span></div>\\", 2)
return function render(_ctx, _cache) {
return _hoisted_1
}"
`;
exports[`stringify static html stringify v-text 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"<pre data-type=\\\\\\"js\\\\\\"><code>&lt;span&gt;show-it &lt;/span&gt;</code></pre><div class><span class>1</span><span class>2</span></div>\\", 2)
return function render(_ctx, _cache) {
return _hoisted_1
}"
`;

View File

@ -433,4 +433,25 @@ describe('stringify static html', () => {
] ]
}) })
}) })
// #5439
test('stringify v-html', () => {
const { code } = compileWithStringify(`
<pre data-type="js"><code v-html="'&lt;span&gt;show-it &lt;/span&gt;'"></code></pre>
<div class>
<span class>1</span><span class>2</span>
</div>`)
expect(code).toMatch(`<code><span>show-it </span></code>`)
expect(code).toMatchSnapshot()
})
test('stringify v-text', () => {
const { code } = compileWithStringify(`
<pre data-type="js"><code v-text="'&lt;span&gt;show-it &lt;/span&gt;'"></code></pre>
<div class>
<span class>1</span><span class>2</span>
</div>`)
expect(code).toMatch(`<code>&lt;span&gt;show-it &lt;/span&gt;</code>`)
expect(code).toMatchSnapshot()
})
}) })

View File

@ -1,158 +0,0 @@
import { compile } from '../../src'
describe('compiler warnings', () => {
describe('Transition', () => {
function checkWarning(
template: string,
shouldWarn: boolean,
message = `<Transition> expects exactly one child element or component.`
) {
const spy = jest.fn()
compile(template.trim(), {
hoistStatic: true,
transformHoist: null,
onError: err => {
spy(err.message)
}
})
if (shouldWarn) expect(spy).toHaveBeenCalledWith(message)
else expect(spy).not.toHaveBeenCalled()
}
test('warns if multiple children', () => {
checkWarning(
`
<transition>
<div>hey</div>
<div>hey</div>
</transition>
`,
true
)
})
test('warns with v-for', () => {
checkWarning(
`
<transition>
<div v-for="i in items">hey</div>
</transition>
`,
true
)
})
test('warns with multiple v-if + v-for', () => {
checkWarning(
`
<transition>
<div v-if="a" v-for="i in items">hey</div>
<div v-else v-for="i in items">hey</div>
</transition>
`,
true
)
})
test('warns with template v-if', () => {
checkWarning(
`
<transition>
<template v-if="ok"></template>
</transition>
`,
true
)
})
test('warns with multiple templates', () => {
checkWarning(
`
<transition>
<template v-if="a"></template>
<template v-else></template>
</transition>
`,
true
)
})
test('warns if multiple children with v-if', () => {
checkWarning(
`
<transition>
<div v-if="one">hey</div>
<div v-if="other">hey</div>
</transition>
`,
true
)
})
test('does not warn with regular element', () => {
checkWarning(
`
<transition>
<div>hey</div>
</transition>
`,
false
)
})
test('does not warn with one single v-if', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
</transition>
`,
false
)
})
test('does not warn with v-if v-else-if v-else', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
<div v-else-if="b">hey</div>
<div v-else>hey</div>
</transition>
`,
false
)
})
test('does not warn with v-if v-else', () => {
checkWarning(
`
<transition>
<div v-if="a">hey</div>
<div v-else>hey</div>
</transition>
`,
false
)
})
})
})
test('the v-if/else-if/else branchs in Transition should ignore comments', () => {
expect(
compile(`
<transition>
<div v-if="a">hey</div>
<!-- this should be ignored -->
<div v-else-if="b">hey</div>
<!-- this should be ignored -->
<div v-else>
<p v-if="c"/>
<!-- this should not be ignored -->
<p v-else/>
</div>
</transition>
`).code
).toMatchSnapshot()
})

View File

@ -16,7 +16,7 @@ import { transformVText } from './transforms/vText'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
import { transformOn } from './transforms/vOn' import { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow' import { transformShow } from './transforms/vShow'
import { warnTransitionChildren } from './transforms/warnTransitionChildren' import { transformTransition } from './transforms/Transition'
import { stringifyStatic } from './transforms/stringifyStatic' import { stringifyStatic } from './transforms/stringifyStatic'
import { ignoreSideEffectTags } from './transforms/ignoreSideEffectTags' import { ignoreSideEffectTags } from './transforms/ignoreSideEffectTags'
import { extend } from '@vue/shared' import { extend } from '@vue/shared'
@ -25,7 +25,7 @@ export { parserOptions }
export const DOMNodeTransforms: NodeTransform[] = [ export const DOMNodeTransforms: NodeTransform[] = [
transformStyle, transformStyle,
...(__DEV__ ? [warnTransitionChildren] : []) ...(__DEV__ ? [transformTransition] : [])
] ]
export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = { export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {

View File

@ -8,7 +8,7 @@ import {
import { TRANSITION } from '../runtimeHelpers' import { TRANSITION } from '../runtimeHelpers'
import { createDOMCompilerError, DOMErrorCodes } from '../errors' import { createDOMCompilerError, DOMErrorCodes } from '../errors'
export const warnTransitionChildren: NodeTransform = (node, context) => { export const transformTransition: NodeTransform = (node, context) => {
if ( if (
node.type === NodeTypes.ELEMENT && node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT node.tagType === ElementTypes.COMPONENT
@ -16,7 +16,12 @@ export const warnTransitionChildren: NodeTransform = (node, context) => {
const component = context.isBuiltInComponent(node.tag) const component = context.isBuiltInComponent(node.tag)
if (component === TRANSITION) { if (component === TRANSITION) {
return () => { return () => {
if (node.children.length && hasMultipleChildren(node)) { if (!node.children.length) {
return
}
// warn multiple transition children
if (hasMultipleChildren(node)) {
context.onError( context.onError(
createDOMCompilerError( createDOMCompilerError(
DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN, DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN,
@ -28,6 +33,22 @@ export const warnTransitionChildren: NodeTransform = (node, context) => {
) )
) )
} }
// check if it's s single child w/ v-show
// if yes, inject "persisted: true" to the transition props
const child = node.children[0]
if (child.type === NodeTypes.ELEMENT) {
for (const p of child.props) {
if (p.type === NodeTypes.DIRECTIVE && p.name === 'show') {
node.props.push({
type: NodeTypes.ATTRIBUTE,
name: 'persisted',
value: undefined,
loc: node.loc
})
}
}
}
} }
} }
} }

View File

@ -279,6 +279,7 @@ function stringifyElement(
context: TransformContext context: TransformContext
): string { ): string {
let res = `<${node.tag}` let res = `<${node.tag}`
let innerHTML = ''
for (let i = 0; i < node.props.length; i++) { for (let i = 0; i < node.props.length; i++) {
const p = node.props[i] const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) { if (p.type === NodeTypes.ATTRIBUTE) {
@ -286,28 +287,38 @@ function stringifyElement(
if (p.value) { if (p.value) {
res += `="${escapeHtml(p.value.content)}"` res += `="${escapeHtml(p.value.content)}"`
} }
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') { } else if (p.type === NodeTypes.DIRECTIVE) {
const exp = p.exp as SimpleExpressionNode if (p.name === 'bind') {
if (exp.content[0] === '_') { const exp = p.exp as SimpleExpressionNode
// internally generated string constant references if (exp.content[0] === '_') {
// e.g. imported URL strings via compiler-sfc transformAssetUrl plugin // internally generated string constant references
res += ` ${(p.arg as SimpleExpressionNode).content}="__VUE_EXP_START__${ // e.g. imported URL strings via compiler-sfc transformAssetUrl plugin
exp.content res += ` ${
}__VUE_EXP_END__"` (p.arg as SimpleExpressionNode).content
continue }="__VUE_EXP_START__${exp.content}__VUE_EXP_END__"`
} continue
// constant v-bind, e.g. :foo="1"
let evaluated = evaluateConstant(exp)
if (evaluated != null) {
const arg = p.arg && (p.arg as SimpleExpressionNode).content
if (arg === 'class') {
evaluated = normalizeClass(evaluated)
} else if (arg === 'style') {
evaluated = stringifyStyle(normalizeStyle(evaluated))
} }
res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml( // constant v-bind, e.g. :foo="1"
evaluated let evaluated = evaluateConstant(exp)
)}"` if (evaluated != null) {
const arg = p.arg && (p.arg as SimpleExpressionNode).content
if (arg === 'class') {
evaluated = normalizeClass(evaluated)
} else if (arg === 'style') {
evaluated = stringifyStyle(normalizeStyle(evaluated))
}
res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml(
evaluated
)}"`
}
} else if (p.name === 'html') {
// #5439 v-html with constant value
// not sure why would anyone do this but it can happen
innerHTML = evaluateConstant(p.exp as SimpleExpressionNode)
} else if (p.name === 'text') {
innerHTML = escapeHtml(
toDisplayString(evaluateConstant(p.exp as SimpleExpressionNode))
)
} }
} }
} }
@ -315,8 +326,12 @@ function stringifyElement(
res += ` ${context.scopeId}` res += ` ${context.scopeId}`
} }
res += `>` res += `>`
for (let i = 0; i < node.children.length; i++) { if (innerHTML) {
res += stringifyNode(node.children[i], context) res += innerHTML
} else {
for (let i = 0; i < node.children.length; i++) {
res += stringifyNode(node.children[i], context)
}
} }
if (!isVoidTag(node.tag)) { if (!isVoidTag(node.tag)) {
res += `</${node.tag}>` res += `</${node.tag}>`
@ -330,7 +345,7 @@ function stringifyElement(
// here, e.g. `{{ 1 }}` or `{{ 'foo' }}` // here, e.g. `{{ 1 }}` or `{{ 'foo' }}`
// in addition, constant exps bail on presence of parens so you can't even // in addition, constant exps bail on presence of parens so you can't even
// run JSFuck in here. But we mark it unsafe for security review purposes. // run JSFuck in here. But we mark it unsafe for security review purposes.
// (see compiler-core/src/transformExpressions) // (see compiler-core/src/transforms/transformExpression)
function evaluateConstant(exp: ExpressionNode): string { function evaluateConstant(exp: ExpressionNode): string {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
return new Function(`return ${exp.content}`)() return new Function(`return ${exp.content}`)()

View File

@ -3,7 +3,8 @@ import {
createObjectProperty, createObjectProperty,
createSimpleExpression, createSimpleExpression,
TO_DISPLAY_STRING, TO_DISPLAY_STRING,
createCallExpression createCallExpression,
getConstantType
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { createDOMCompilerError, DOMErrorCodes } from '../errors' import { createDOMCompilerError, DOMErrorCodes } from '../errors'
@ -25,11 +26,13 @@ export const transformVText: DirectiveTransform = (dir, node, context) => {
createObjectProperty( createObjectProperty(
createSimpleExpression(`textContent`, true), createSimpleExpression(`textContent`, true),
exp exp
? createCallExpression( ? getConstantType(exp, context) > 0
context.helperString(TO_DISPLAY_STRING), ? exp
[exp], : createCallExpression(
loc context.helperString(TO_DISPLAY_STRING),
) [exp],
loc
)
: createSimpleExpression('', true) : createSimpleExpression('', true)
) )
] ]

View File

@ -1,5 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SFC analyze <script> bindings auto name inference basic 1`] = `
"export default {
name: 'FooBar',
setup(__props, { expose }) {
expose();
const a = 1
return { a }
}
}"
`;
exports[`SFC analyze <script> bindings auto name inference do not overwrite manual name (call) 1`] = `
"import { defineComponent } from 'vue'
const __default__ = defineComponent({
name: 'Baz'
})
export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
expose();
const a = 1
return { a, defineComponent }
}
})"
`;
exports[`SFC analyze <script> bindings auto name inference do not overwrite manual name (object) 1`] = `
"const __default__ = {
name: 'Baz'
}
export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
expose();
const a = 1
return { a }
}
})"
`;
exports[`SFC compile <script setup> <script> after <script setup> the script content not end with \`\\n\` 1`] = `
"const n = 1
import { x } from './x'
export default {
setup(__props, { expose }) {
expose();
return { n, x }
}
}"
`;
exports[`SFC compile <script setup> <script> and <script setup> co-usage script first 1`] = ` exports[`SFC compile <script setup> <script> and <script setup> co-usage script first 1`] = `
"import { x } from './x' "import { x } from './x'
@ -22,7 +80,8 @@ return { n, x }
exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first 1`] = ` exports[`SFC compile <script setup> <script> and <script setup> co-usage script setup first 1`] = `
"export const n = 1 "export const n = 1
const __default__ = {} const __default__ = {}
import { x } from './x'
import { x } from './x'
export default /*#__PURE__*/Object.assign(__default__, { export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) { setup(__props, { expose }) {
@ -42,7 +101,8 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage script
const __default__ = { const __default__ = {
name: \\"test\\" name: \\"test\\"
} }
import { x } from './x'
import { x } from './x'
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
...__default__, ...__default__,
@ -63,6 +123,7 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage script
const __default__ = def const __default__ = def
import { x } from './x' import { x } from './x'
export default /*#__PURE__*/Object.assign(__default__, { export default /*#__PURE__*/Object.assign(__default__, {
@ -136,6 +197,173 @@ return { }
}" }"
`; `;
exports[`SFC compile <script setup> async/await detection multiple \`if for\` nested statements 1`] = `
"import { withAsyncContext as _withAsyncContext } from 'vue'
export default {
async setup(__props, { expose }) {
expose();
let __temp, __restore
if (ok) {
for (let a of [1,2,3]) {
(
([__temp,__restore] = _withAsyncContext(() => a)),
await __temp,
__restore()
)
}
for (let a of [1,2,3]) {
(
([__temp,__restore] = _withAsyncContext(() => a)),
await __temp,
__restore()
)
;(
([__temp,__restore] = _withAsyncContext(() => a)),
await __temp,
__restore()
)
}
}
return { }
}
}"
`;
exports[`SFC compile <script setup> async/await detection multiple \`if while\` nested statements 1`] = `
"import { withAsyncContext as _withAsyncContext } from 'vue'
export default {
async setup(__props, { expose }) {
expose();
let __temp, __restore
if (ok) {
while (d) {
(
([__temp,__restore] = _withAsyncContext(() => 5)),
await __temp,
__restore()
)
}
while (d) {
(
([__temp,__restore] = _withAsyncContext(() => 5)),
await __temp,
__restore()
)
;(
([__temp,__restore] = _withAsyncContext(() => 6)),
await __temp,
__restore()
)
if (c) {
let f = 10
10 + (
([__temp,__restore] = _withAsyncContext(() => 7)),
__temp = await __temp,
__restore(),
__temp
)
} else {
(
([__temp,__restore] = _withAsyncContext(() => 8)),
await __temp,
__restore()
)
;(
([__temp,__restore] = _withAsyncContext(() => 9)),
await __temp,
__restore()
)
}
}
}
return { }
}
}"
`;
exports[`SFC compile <script setup> async/await detection multiple \`if\` nested statements 1`] = `
"import { withAsyncContext as _withAsyncContext } from 'vue'
export default {
async setup(__props, { expose }) {
expose();
let __temp, __restore
if (ok) {
let a = 'foo'
;(
([__temp,__restore] = _withAsyncContext(() => 0)),
__temp = await __temp,
__restore(),
__temp
) + (
([__temp,__restore] = _withAsyncContext(() => 1)),
__temp = await __temp,
__restore(),
__temp
)
;(
([__temp,__restore] = _withAsyncContext(() => 2)),
await __temp,
__restore()
)
} else if (a) {
(
([__temp,__restore] = _withAsyncContext(() => 10)),
await __temp,
__restore()
)
if (b) {
(
([__temp,__restore] = _withAsyncContext(() => 0)),
__temp = await __temp,
__restore(),
__temp
) + (
([__temp,__restore] = _withAsyncContext(() => 1)),
__temp = await __temp,
__restore(),
__temp
)
} else {
let a = 'foo'
;(
([__temp,__restore] = _withAsyncContext(() => 2)),
await __temp,
__restore()
)
}
if (b) {
(
([__temp,__restore] = _withAsyncContext(() => 3)),
await __temp,
__restore()
)
;(
([__temp,__restore] = _withAsyncContext(() => 4)),
await __temp,
__restore()
)
}
} else {
(
([__temp,__restore] = _withAsyncContext(() => 5)),
await __temp,
__restore()
)
}
return { }
}
}"
`;
exports[`SFC compile <script setup> async/await detection nested await 1`] = ` exports[`SFC compile <script setup> async/await detection nested await 1`] = `
"import { withAsyncContext as _withAsyncContext } from 'vue' "import { withAsyncContext as _withAsyncContext } from 'vue'
@ -492,6 +720,23 @@ return { props, a, emit }
}" }"
`; `;
exports[`SFC compile <script setup> dev mode import usage check TS annotations 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { Foo, Bar, Baz } from './x'
export default /*#__PURE__*/_defineComponent({
setup(__props, { expose }) {
expose();
const a = 1
function b() {}
return { a, b, Baz }
}
})"
`;
exports[`SFC compile <script setup> dev mode import usage check attribute expressions 1`] = ` exports[`SFC compile <script setup> dev mode import usage check attribute expressions 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { bar, baz } from './x' import { bar, baz } from './x'
@ -717,6 +962,7 @@ exports[`SFC compile <script setup> inlineTemplate mode avoid unref() when neces
import { ref } from 'vue' import { ref } from 'vue'
import Foo, { bar } from './Foo.vue' import Foo, { bar } from './Foo.vue'
import other from './util' import other from './util'
import * as tree from './tree'
export default { export default {
setup(__props) { setup(__props) {
@ -735,7 +981,8 @@ return (_ctx, _cache) => {
]), ]),
_: 1 /* STABLE */ _: 1 /* STABLE */
}), }),
_createElementVNode(\\"div\\", { onClick: fn }, _toDisplayString(count.value) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(maybe)) + \\" \\" + _toDisplayString(_unref(lett)) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */) _createElementVNode(\\"div\\", { onClick: fn }, _toDisplayString(count.value) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(maybe)) + \\" \\" + _toDisplayString(_unref(lett)) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */),
_createTextVNode(\\" \\" + _toDisplayString(tree.foo()), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
} }
} }
@ -1006,7 +1253,8 @@ exports[`SFC compile <script setup> should expose top level declarations 1`] = `
const bb = 2 const bb = 2
function cc() {} function cc() {}
class dd {} class dd {}
import { x } from './x'
import { x } from './x'
export default { export default {
setup(__props, { expose }) { setup(__props, { expose }) {

View File

@ -65,7 +65,7 @@ exports[`sfc props transform default values w/ runtime declaration 1`] = `
export default { export default {
props: _mergeDefaults(['foo', 'bar'], { props: _mergeDefaults(['foo', 'bar'], {
foo: 1, foo: 1,
bar: () => {} bar: () => ({})
}), }),
setup(__props) { setup(__props) {
@ -83,7 +83,7 @@ exports[`sfc props transform default values w/ type declaration 1`] = `
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
props: { props: {
foo: { type: Number, required: false, default: 1 }, foo: { type: Number, required: false, default: 1 },
bar: { type: Object, required: false, default: () => {} } bar: { type: Object, required: false, default: () => ({}) }
}, },
setup(__props: any) { setup(__props: any) {
@ -101,11 +101,11 @@ exports[`sfc props transform default values w/ type declaration, prod mode 1`] =
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
props: { props: {
foo: { default: 1 }, foo: { default: 1 },
bar: { default: () => {} }, bar: { default: () => ({}) },
baz: null, baz: null,
boola: { type: Boolean }, boola: { type: Boolean },
boolb: { type: [Boolean, Number] }, boolb: { type: [Boolean, Number] },
func: { type: Function, default: () => () => {} } func: { type: Function, default: () => (() => {}) }
}, },
setup(__props: any) { setup(__props: any) {
@ -134,6 +134,25 @@ return () => {}
}" }"
`; `;
exports[`sfc props transform non-identifier prop names 1`] = `
"import { toDisplayString as _toDisplayString } from \\"vue\\"
export default {
props: { 'foo.bar': Function },
setup(__props) {
let x = __props[\\"foo.bar\\"]
return (_ctx, _cache) => {
return _toDisplayString(__props[\\"foo.bar\\"])
}
}
}"
`;
exports[`sfc props transform rest spread 1`] = ` exports[`sfc props transform rest spread 1`] = `
"import { createPropsRestProxy as _createPropsRestProxy } from 'vue' "import { createPropsRestProxy as _createPropsRestProxy } from 'vue'

View File

@ -1,5 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should not hoist srcset URLs in SSR mode 1`] = `
"import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode } from \\"vue\\"
import { ssrRenderAttr as _ssrRenderAttr, ssrRenderComponent as _ssrRenderComponent } from \\"vue/server-renderer\\"
import _imports_0 from './img/foo.svg'
import _imports_1 from './img/bar.svg'
export function ssrRender(_ctx, _push, _parent, _attrs) {
const _component_router_link = _resolveComponent(\\"router-link\\")
_push(\`<!--[--><picture><source\${
_ssrRenderAttr(\\"srcset\\", _imports_0)
}><img\${
_ssrRenderAttr(\\"src\\", _imports_0)
}></picture>\`)
_push(_ssrRenderComponent(_component_router_link, null, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`<picture\${
_scopeId
}><source\${
_ssrRenderAttr(\\"srcset\\", _imports_1)
}\${
_scopeId
}><img\${
_ssrRenderAttr(\\"src\\", _imports_1)
}\${
_scopeId
}></picture>\`)
} else {
return [
_createVNode(\\"picture\\", null, [
_createVNode(\\"source\\", {
srcset: _imports_1
}),
_createVNode(\\"img\\", { src: _imports_1 })
])
]
}
}),
_: 1 /* STABLE */
}, _parent))
_push(\`<!--]-->\`)
}"
`;
exports[`source map 1`] = ` exports[`source map 1`] = `
Object { Object {
"mappings": ";;;wBACE,oBAA8B;IAAzB,oBAAmB,4BAAbA,WAAM", "mappings": ";;;wBACE,oBAA8B;IAAzB,oBAAmB,4BAAbA,WAAM",

View File

@ -38,11 +38,13 @@ import _imports_0 from '@svg/file.svg'
const _hoisted_1 = _imports_0 + '#fragment' const _hoisted_1 = _imports_0 + '#fragment'
const _hoisted_2 = /*#__PURE__*/_createElementVNode(\\"use\\", { href: _hoisted_1 }, null, -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode(\\"use\\", { href: _hoisted_1 }, null, -1 /* HOISTED */)
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"use\\", { href: _hoisted_1 }), _hoisted_2,
_createElementVNode(\\"use\\", { href: _hoisted_1 }) _hoisted_3
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;

View File

@ -1,5 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler sfc: transform srcset srcset w/ explicit base option 1`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
import _imports_0 from '@/logo.png'
const _hoisted_1 = _imports_0 + ', ' + _imports_0 + ' 2x'
const _hoisted_2 = _imports_0 + ' 1x, ' + \\"/foo/logo.png\\" + ' 2x'
const _hoisted_3 = /*#__PURE__*/_createElementVNode(\\"img\\", { srcset: _hoisted_1 }, null, -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createElementVNode(\\"img\\", { srcset: _hoisted_2 }, null, -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_3,
_hoisted_4
], 64 /* STABLE_FRAGMENT */))
}"
`;
exports[`compiler sfc: transform srcset transform srcset 1`] = ` exports[`compiler sfc: transform srcset transform srcset 1`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\" "import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
import _imports_0 from './logo.png' import _imports_0 from './logo.png'
@ -13,57 +31,69 @@ const _hoisted_5 = _imports_0 + ' 2x, ' + _imports_0
const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x' const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_8 = \\"/logo.png\\" + ', ' + _imports_0 + ' 2x' const _hoisted_8 = \\"/logo.png\\" + ', ' + _imports_0 + ' 2x'
const _hoisted_9 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"\\"
}, null, -1 /* HOISTED */)
const _hoisted_10 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_1
}, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_2
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_3
}, null, -1 /* HOISTED */)
const _hoisted_13 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_4
}, null, -1 /* HOISTED */)
const _hoisted_14 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_5
}, null, -1 /* HOISTED */)
const _hoisted_15 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_6
}, null, -1 /* HOISTED */)
const _hoisted_16 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_7
}, null, -1 /* HOISTED */)
const _hoisted_17 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: \\"/logo.png, /logo.png 2x\\"
}, null, -1 /* HOISTED */)
const _hoisted_18 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"https://example.com/logo.png\\",
srcset: \\"https://example.com/logo.png, https://example.com/logo.png 2x\\"
}, null, -1 /* HOISTED */)
const _hoisted_19 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: _hoisted_8
}, null, -1 /* HOISTED */)
const _hoisted_20 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"data:image/png;base64,i\\",
srcset: \\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"img\\", { _hoisted_9,
src: \\"./logo.png\\", _hoisted_10,
srcset: \\"\\" _hoisted_11,
}), _hoisted_12,
_createElementVNode(\\"img\\", { _hoisted_13,
src: \\"./logo.png\\", _hoisted_14,
srcset: _hoisted_1 _hoisted_15,
}), _hoisted_16,
_createElementVNode(\\"img\\", { _hoisted_17,
src: \\"./logo.png\\", _hoisted_18,
srcset: _hoisted_2 _hoisted_19,
}), _hoisted_20
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_3
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_4
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_5
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_6
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_7
}),
_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: \\"/logo.png, /logo.png 2x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"https://example.com/logo.png\\",
srcset: \\"https://example.com/logo.png, https://example.com/logo.png 2x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: _hoisted_8
}),
_createElementVNode(\\"img\\", {
src: \\"data:image/png;base64,i\\",
srcset: \\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\"
})
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;
@ -71,56 +101,69 @@ export function render(_ctx, _cache) {
exports[`compiler sfc: transform srcset transform srcset w/ base 1`] = ` exports[`compiler sfc: transform srcset transform srcset w/ base 1`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\" "import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"\\"
}, null, -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png\\"
}, null, -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png 2x\\"
}, null, -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png 2x\\"
}, null, -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png, /foo/logo.png 2x\\"
}, null, -1 /* HOISTED */)
const _hoisted_6 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png 2x, /foo/logo.png\\"
}, null, -1 /* HOISTED */)
const _hoisted_7 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png 2x, /foo/logo.png 3x\\"
}, null, -1 /* HOISTED */)
const _hoisted_8 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x\\"
}, null, -1 /* HOISTED */)
const _hoisted_9 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: \\"/logo.png, /logo.png 2x\\"
}, null, -1 /* HOISTED */)
const _hoisted_10 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"https://example.com/logo.png\\",
srcset: \\"https://example.com/logo.png, https://example.com/logo.png 2x\\"
}, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: \\"/logo.png, /foo/logo.png 2x\\"
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"data:image/png;base64,i\\",
srcset: \\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"img\\", { _hoisted_1,
src: \\"./logo.png\\", _hoisted_2,
srcset: \\"\\" _hoisted_3,
}), _hoisted_4,
_createElementVNode(\\"img\\", { _hoisted_5,
src: \\"./logo.png\\", _hoisted_6,
srcset: \\"/foo/logo.png\\" _hoisted_7,
}), _hoisted_8,
_createElementVNode(\\"img\\", { _hoisted_9,
src: \\"./logo.png\\", _hoisted_10,
srcset: \\"/foo/logo.png 2x\\" _hoisted_11,
}), _hoisted_12
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png 2x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png, /foo/logo.png 2x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png 2x, /foo/logo.png\\"
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png 2x, /foo/logo.png 3x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: \\"/logo.png, /logo.png 2x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"https://example.com/logo.png\\",
srcset: \\"https://example.com/logo.png, https://example.com/logo.png 2x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: \\"/logo.png, /foo/logo.png 2x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"data:image/png;base64,i\\",
srcset: \\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\"
})
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;
@ -140,57 +183,69 @@ const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x' const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x' const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
const _hoisted_10 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"\\"
}, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_1
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_2
}, null, -1 /* HOISTED */)
const _hoisted_13 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_3
}, null, -1 /* HOISTED */)
const _hoisted_14 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_4
}, null, -1 /* HOISTED */)
const _hoisted_15 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_5
}, null, -1 /* HOISTED */)
const _hoisted_16 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_6
}, null, -1 /* HOISTED */)
const _hoisted_17 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_7
}, null, -1 /* HOISTED */)
const _hoisted_18 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: _hoisted_8
}, null, -1 /* HOISTED */)
const _hoisted_19 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"https://example.com/logo.png\\",
srcset: \\"https://example.com/logo.png, https://example.com/logo.png 2x\\"
}, null, -1 /* HOISTED */)
const _hoisted_20 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: _hoisted_9
}, null, -1 /* HOISTED */)
const _hoisted_21 = /*#__PURE__*/_createElementVNode(\\"img\\", {
src: \\"data:image/png;base64,i\\",
srcset: \\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"img\\", { _hoisted_10,
src: \\"./logo.png\\", _hoisted_11,
srcset: \\"\\" _hoisted_12,
}), _hoisted_13,
_createElementVNode(\\"img\\", { _hoisted_14,
src: \\"./logo.png\\", _hoisted_15,
srcset: _hoisted_1 _hoisted_16,
}), _hoisted_17,
_createElementVNode(\\"img\\", { _hoisted_18,
src: \\"./logo.png\\", _hoisted_19,
srcset: _hoisted_2 _hoisted_20,
}), _hoisted_21
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_3
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_4
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_5
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_6
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_7
}),
_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: _hoisted_8
}),
_createElementVNode(\\"img\\", {
src: \\"https://example.com/logo.png\\",
srcset: \\"https://example.com/logo.png, https://example.com/logo.png 2x\\"
}),
_createElementVNode(\\"img\\", {
src: \\"/logo.png\\",
srcset: _hoisted_9
}),
_createElementVNode(\\"img\\", {
src: \\"data:image/png;base64,i\\",
srcset: \\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\"
})
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;

View File

@ -64,11 +64,11 @@ const bar = 1
`) `)
// should generate working code // should generate working code
assertCode(content) assertCode(content)
// should anayze bindings // should analyze bindings
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS, foo: BindingTypes.PROPS,
bar: BindingTypes.SETUP_CONST, bar: BindingTypes.SETUP_CONST,
props: BindingTypes.SETUP_CONST props: BindingTypes.SETUP_REACTIVE_CONST
}) })
// should remove defineOptions import and call // should remove defineOptions import and call
@ -168,6 +168,16 @@ defineExpose({ foo: 123 })
expect(content).toMatch(/\bexpose\(\{ foo: 123 \}\)/) expect(content).toMatch(/\bexpose\(\{ foo: 123 \}\)/)
}) })
test('<script> after <script setup> the script content not end with `\\n`',() => {
const { content } = compile(`
<script setup>
import { x } from './x'
</script>
<script>const n = 1</script>
`)
assertCode(content)
})
describe('<script> and <script setup> co-usage', () => { describe('<script> and <script setup> co-usage', () => {
test('script first', () => { test('script first', () => {
const { content } = compile(` const { content } = compile(`
@ -403,7 +413,7 @@ defineExpose({ foo: 123 })
assertCode(content) assertCode(content)
}) })
// #4340 interpolations in tempalte strings // #4340 interpolations in template strings
test('js template string interpolations', () => { test('js template string interpolations', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
@ -432,6 +442,23 @@ defineExpose({ foo: 123 })
expect(content).toMatch(`return { FooBaz, Last }`) expect(content).toMatch(`return { FooBaz, Last }`)
assertCode(content) assertCode(content)
}) })
test('TS annotations', () => {
const { content } = compile(`
<script setup lang="ts">
import { Foo, Bar, Baz } from './x'
const a = 1
function b() {}
</script>
<template>
{{ a as Foo }}
{{ b<Bar>() }}
{{ Baz }}
</template>
`)
expect(content).toMatch(`return { a, b, Baz }`)
assertCode(content)
})
}) })
describe('inlineTemplate mode', () => { describe('inlineTemplate mode', () => {
@ -502,6 +529,7 @@ defineExpose({ foo: 123 })
import { ref } from 'vue' import { ref } from 'vue'
import Foo, { bar } from './Foo.vue' import Foo, { bar } from './Foo.vue'
import other from './util' import other from './util'
import * as tree from './tree'
const count = ref(0) const count = ref(0)
const constant = {} const constant = {}
const maybe = foo() const maybe = foo()
@ -511,6 +539,7 @@ defineExpose({ foo: 123 })
<template> <template>
<Foo>{{ bar }}</Foo> <Foo>{{ bar }}</Foo>
<div @click="fn">{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}</div> <div @click="fn">{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}</div>
{{ tree.foo() }}
</template> </template>
`, `,
{ inlineTemplate: true } { inlineTemplate: true }
@ -529,6 +558,8 @@ defineExpose({ foo: 123 })
expect(content).toMatch(`unref(maybe)`) expect(content).toMatch(`unref(maybe)`)
// should unref() on let bindings // should unref() on let bindings
expect(content).toMatch(`unref(lett)`) expect(content).toMatch(`unref(lett)`)
// no need to unref namespace import (this also preserves tree-shaking)
expect(content).toMatch(`tree.foo()`)
// no need to unref function declarations // no need to unref function declarations
expect(content).toMatch(`{ onClick: fn }`) expect(content).toMatch(`{ onClick: fn }`)
// no need to mark constant fns in patch flag // no need to mark constant fns in patch flag
@ -1164,6 +1195,59 @@ const emit = defineEmits(['a', 'b'])
assertAwaitDetection(`if (ok) { await foo } else { await bar }`) assertAwaitDetection(`if (ok) { await foo } else { await bar }`)
}) })
test('multiple `if` nested statements', () => {
assertAwaitDetection(`if (ok) {
let a = 'foo'
await 0 + await 1
await 2
} else if (a) {
await 10
if (b) {
await 0 + await 1
} else {
let a = 'foo'
await 2
}
if (b) {
await 3
await 4
}
} else {
await 5
}`)
})
test('multiple `if while` nested statements', () => {
assertAwaitDetection(`if (ok) {
while (d) {
await 5
}
while (d) {
await 5
await 6
if (c) {
let f = 10
10 + await 7
} else {
await 8
await 9
}
}
}`)
})
test('multiple `if for` nested statements', () => {
assertAwaitDetection(`if (ok) {
for (let a of [1,2,3]) {
await a
}
for (let a of [1,2,3]) {
await a
await a
}
}`)
})
test('should ignore await inside functions', () => { test('should ignore await inside functions', () => {
// function declaration // function declaration
assertAwaitDetection(`async function foo() { await bar }`, false) assertAwaitDetection(`async function foo() { await bar }`, false)
@ -1550,4 +1634,59 @@ describe('SFC analyze <script> bindings', () => {
foo: BindingTypes.PROPS foo: BindingTypes.PROPS
}) })
}) })
describe('auto name inference', () => {
test('basic', () => {
const { content } = compile(
`<script setup>const a = 1</script>
<template>{{ a }}</template>`,
undefined,
{
filename: 'FooBar.vue'
}
)
expect(content).toMatch(`export default {
name: 'FooBar'`)
assertCode(content)
})
test('do not overwrite manual name (object)', () => {
const { content } = compile(
`<script>
export default {
name: 'Baz'
}
</script>
<script setup>const a = 1</script>
<template>{{ a }}</template>`,
undefined,
{
filename: 'FooBar.vue'
}
)
expect(content).not.toMatch(`name: 'FooBar'`)
expect(content).toMatch(`name: 'Baz'`)
assertCode(content)
})
test('do not overwrite manual name (call)', () => {
const { content } = compile(
`<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Baz'
})
</script>
<script setup>const a = 1</script>
<template>{{ a }}</template>`,
undefined,
{
filename: 'FooBar.vue'
}
)
expect(content).not.toMatch(`name: 'FooBar'`)
expect(content).toMatch(`name: 'Baz'`)
assertCode(content)
})
})
}) })

View File

@ -59,7 +59,7 @@ describe('sfc props transform', () => {
// function // function
expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], { expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], {
foo: 1, foo: 1,
bar: () => {} bar: () => ({})
})`) })`)
assertCode(content) assertCode(content)
}) })
@ -74,7 +74,7 @@ describe('sfc props transform', () => {
// function // function
expect(content).toMatch(`props: { expect(content).toMatch(`props: {
foo: { type: Number, required: false, default: 1 }, foo: { type: Number, required: false, default: 1 },
bar: { type: Object, required: false, default: () => {} } bar: { type: Object, required: false, default: () => ({}) }
}`) }`)
assertCode(content) assertCode(content)
}) })
@ -92,11 +92,11 @@ describe('sfc props transform', () => {
// function // function
expect(content).toMatch(`props: { expect(content).toMatch(`props: {
foo: { default: 1 }, foo: { default: 1 },
bar: { default: () => {} }, bar: { default: () => ({}) },
baz: null, baz: null,
boola: { type: Boolean }, boola: { type: Boolean },
boolb: { type: [Boolean, Number] }, boolb: { type: [Boolean, Number] },
func: { type: Function, default: () => () => {} } func: { type: Function, default: () => (() => {}) }
}`) }`)
assertCode(content) assertCode(content)
}) })
@ -127,6 +127,28 @@ describe('sfc props transform', () => {
}) })
}) })
// #5425
test('non-identifier prop names', () => {
const { content, bindings } = compile(`
<script setup>
const { 'foo.bar': fooBar } = defineProps({ 'foo.bar': Function })
let x = fooBar
</script>
<template>{{ fooBar }}</template>
`)
expect(content).toMatch(`x = __props["foo.bar"]`)
expect(content).toMatch(`toDisplayString(__props["foo.bar"])`)
assertCode(content)
expect(bindings).toStrictEqual({
x: BindingTypes.SETUP_LET,
'foo.bar': BindingTypes.PROPS,
fooBar: BindingTypes.PROPS_ALIASED,
__propsAliases: {
fooBar: 'foo.bar'
}
})
})
test('rest spread', () => { test('rest spread', () => {
const { content, bindings } = compile(` const { content, bindings } = compile(`
<script setup> <script setup>
@ -141,7 +163,7 @@ describe('sfc props transform', () => {
foo: BindingTypes.PROPS, foo: BindingTypes.PROPS,
bar: BindingTypes.PROPS, bar: BindingTypes.PROPS,
baz: BindingTypes.PROPS, baz: BindingTypes.PROPS,
rest: BindingTypes.SETUP_CONST rest: BindingTypes.SETUP_REACTIVE_CONST
}) })
}) })

View File

@ -153,3 +153,24 @@ test('should generate the correct imports expression', () => {
expect(code).toMatch(`_ssrRenderAttr(\"src\", _imports_1)`) expect(code).toMatch(`_ssrRenderAttr(\"src\", _imports_1)`)
expect(code).toMatch(`_createVNode(\"img\", { src: _imports_1 })`) expect(code).toMatch(`_createVNode(\"img\", { src: _imports_1 })`)
}) })
// #3874
test('should not hoist srcset URLs in SSR mode', () => {
const { code } = compile({
filename: 'example.vue',
source: `
<picture>
<source srcset="./img/foo.svg"/>
<img src="./img/foo.svg"/>
</picture>
<router-link>
<picture>
<source srcset="./img/bar.svg"/>
<img src="./img/bar.svg"/>
</picture>
</router-link>
`,
ssr: true
})
expect(code).toMatchSnapshot()
})

View File

@ -54,9 +54,12 @@ describe('compiler sfc: transform asset url', () => {
test('support uri fragment', () => { test('support uri fragment', () => {
const result = compileWithAssetUrls( const result = compileWithAssetUrls(
'<use href="~@svg/file.svg#fragment"></use>' + '<use href="~@svg/file.svg#fragment"></use>' +
'<use href="~@svg/file.svg#fragment"></use>' '<use href="~@svg/file.svg#fragment"></use>',
{},
{
hoistStatic: true
}
) )
expect(result.code).toMatchSnapshot() expect(result.code).toMatchSnapshot()
}) })

View File

@ -26,6 +26,7 @@ function compileWithSrcset(
? createSrcsetTransformWithOptions(normalizeOptions(options)) ? createSrcsetTransformWithOptions(normalizeOptions(options))
: transformSrcset : transformSrcset
transform(ast, { transform(ast, {
hoistStatic: true,
nodeTransforms: [srcsetTransform, transformElement], nodeTransforms: [srcsetTransform, transformElement],
directiveTransforms: { directiveTransforms: {
bind: transformBind bind: transformBind
@ -85,4 +86,16 @@ describe('compiler sfc: transform srcset', () => {
expect(code).toMatch(`_createStaticVNode`) expect(code).toMatch(`_createStaticVNode`)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('srcset w/ explicit base option', () => {
const code = compileWithSrcset(
`
<img srcset="@/logo.png, @/logo.png 2x"/>
<img srcset="@/logo.png 1x, ./logo.png 2x"/>
`,
{ base: '/foo/' },
{ hoistStatic: true }
).code
expect(code).toMatchSnapshot()
})
}) })

View File

@ -1,13 +1,19 @@
import { parse, SFCScriptCompileOptions, compileScript } from '../src' import {
parse,
SFCScriptCompileOptions,
compileScript,
SFCParseOptions
} from '../src'
import { parse as babelParse } from '@babel/parser' import { parse as babelParse } from '@babel/parser'
export const mockId = 'xxxxxxxx' export const mockId = 'xxxxxxxx'
export function compileSFCScript( export function compileSFCScript(
src: string, src: string,
options?: Partial<SFCScriptCompileOptions> options?: Partial<SFCScriptCompileOptions>,
parseOptions?: SFCParseOptions
) { ) {
const { descriptor } = parse(src) const { descriptor } = parse(src, parseOptions)
return compileScript(descriptor, { return compileScript(descriptor, {
...options, ...options,
id: mockId id: mockId

View File

@ -11,15 +11,14 @@ import {
isFunctionType, isFunctionType,
walkIdentifiers walkIdentifiers
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { SFCDescriptor, SFCScriptBlock } from './parse' import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser'
import { import {
camelize, parse as _parse,
capitalize, parseExpression,
generateCodeFrame, ParserOptions,
isObject, ParserPlugin
makeMap } from '@babel/parser'
} from '@vue/shared' import { camelize, capitalize, generateCodeFrame, isObject,makeMap } from '@vue/shared'
import { import {
Node, Node,
Declaration, Declaration,
@ -188,6 +187,12 @@ export function compileScript(
const plugins: ParserPlugin[] = [] const plugins: ParserPlugin[] = []
if (!isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') { if (!isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
plugins.push('jsx') plugins.push('jsx')
} else {
// If don't match the case of adding jsx, should remove the jsx from the babelParserPlugins
if (options.babelParserPlugins)
options.babelParserPlugins = options.babelParserPlugins.filter(
n => n !== 'jsx'
)
} }
if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins) if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
if (isTS) plugins.push('typescript', 'decorators-legacy') if (isTS) plugins.push('typescript', 'decorators-legacy')
@ -279,6 +284,8 @@ export function compileScript(
let hasDefinePropsCall = false let hasDefinePropsCall = false
let hasDefineEmitCall = false let hasDefineEmitCall = false
let hasDefineExposeCall = false let hasDefineExposeCall = false
let hasDefaultExportName = false
let hasDefaultExportRender = false
let propsRuntimeDecl: Node | undefined let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: ObjectExpression | undefined let propsRuntimeDefaults: ObjectExpression | undefined
let propsDestructureDecl: Node | undefined let propsDestructureDecl: Node | undefined
@ -356,14 +363,23 @@ export function compileScript(
local: string, local: string,
imported: string | false, imported: string | false,
isType: boolean, isType: boolean,
isFromSetup: boolean isFromSetup: boolean,
needTemplateUsageCheck: boolean
) { ) {
if (source === 'vue' && imported) { if (source === 'vue' && imported) {
userImportAlias[imported] = local userImportAlias[imported] = local
} }
let isUsedInTemplate = true // template usage check is only needed in non-inline mode, so we can skip
if (isTS && sfc.template && !sfc.template.src && !sfc.template.lang) { // the work if inlineTemplate is true.
let isUsedInTemplate = needTemplateUsageCheck
if (
needTemplateUsageCheck &&
isTS &&
sfc.template &&
!sfc.template.src &&
!sfc.template.lang
) {
isUsedInTemplate = isImportUsed(local, sfc) isUsedInTemplate = isImportUsed(local, sfc)
} }
@ -425,7 +441,11 @@ export function compileScript(
prop.key prop.key
) )
} }
const propKey = (prop.key as Identifier).name
const propKey = prop.key.type === 'StringLiteral'
? prop.key.value
: (prop.key as Identifier).name
if (prop.value.type === 'AssignmentPattern') { if (prop.value.type === 'AssignmentPattern') {
// default value { foo = 123 } // default value { foo = 123 }
const { left, right } = prop.value const { left, right } = prop.value
@ -751,7 +771,7 @@ export function compileScript(
destructured.default.end! destructured.default.end!
) )
const isLiteral = destructured.default.type.endsWith('Literal') const isLiteral = destructured.default.type.endsWith('Literal')
return isLiteral ? value : `() => ${value}` return isLiteral ? value : `() => (${value})`
} }
} }
@ -821,12 +841,48 @@ export function compileScript(
node.importKind === 'type' || node.importKind === 'type' ||
(specifier.type === 'ImportSpecifier' && (specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'), specifier.importKind === 'type'),
false false,
!options.inlineTemplate
) )
} }
} else if (node.type === 'ExportDefaultDeclaration') { } else if (node.type === 'ExportDefaultDeclaration') {
// export default // export default
defaultExport = node defaultExport = node
// check if user has manually specified `name` or 'render` option in
// export default
// if has name, skip name inference
// if has render and no template, generate return object instead of
// empty render function (#4980)
let optionProperties
if (defaultExport.declaration.type === 'ObjectExpression') {
optionProperties = defaultExport.declaration.properties
} else if (
defaultExport.declaration.type === 'CallExpression' &&
defaultExport.declaration.arguments[0].type === 'ObjectExpression'
) {
optionProperties = defaultExport.declaration.arguments[0].properties
}
if (optionProperties) {
for (const s of optionProperties) {
if (
s.type === 'ObjectProperty' &&
s.key.type === 'Identifier' &&
s.key.name === 'name'
) {
hasDefaultExportName = true
}
if (
(s.type === 'ObjectMethod' || s.type === 'ObjectProperty') &&
s.key.type === 'Identifier' &&
s.key.name === 'render'
) {
// TODO warn when we provide a better way to do it?
hasDefaultExportRender = true
}
}
}
// export default { ... } --> const __default__ = { ... } // export default { ... } --> const __default__ = { ... }
const start = node.start! + scriptStartOffset! const start = node.start! + scriptStartOffset!
const end = node.declaration.start! + scriptStartOffset! const end = node.declaration.start! + scriptStartOffset!
@ -896,6 +952,10 @@ export function compileScript(
// we need to move the block up so that `const __default__` is // we need to move the block up so that `const __default__` is
// declared before being used in the actual component definition // declared before being used in the actual component definition
if (scriptStartOffset! > startOffset) { if (scriptStartOffset! > startOffset) {
// if content doesn't end with newline, add one
if (!/\n$/.test(script.content.trim())) {
s.appendLeft(scriptEndOffset!, `\n`)
}
s.move(scriptStartOffset!, scriptEndOffset!, 0) s.move(scriptStartOffset!, scriptEndOffset!, 0)
} }
} }
@ -969,10 +1029,13 @@ export function compileScript(
for (let i = 0; i < node.specifiers.length; i++) { for (let i = 0; i < node.specifiers.length; i++) {
const specifier = node.specifiers[i] const specifier = node.specifiers[i]
const local = specifier.local.name const local = specifier.local.name
const imported = let imported =
specifier.type === 'ImportSpecifier' && specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' && specifier.imported.type === 'Identifier' &&
specifier.imported.name specifier.imported.name
if (specifier.type === 'ImportNamespaceSpecifier') {
imported = '*'
}
const source = node.source.value const source = node.source.value
const existing = userImports[local] const existing = userImports[local]
if ( if (
@ -1000,7 +1063,8 @@ export function compileScript(
node.importKind === 'type' || node.importKind === 'type' ||
(specifier.type === 'ImportSpecifier' && (specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'), specifier.importKind === 'type'),
true true,
!options.inlineTemplate
) )
} }
} }
@ -1076,15 +1140,28 @@ export function compileScript(
(node.type === 'VariableDeclaration' && !node.declare) || (node.type === 'VariableDeclaration' && !node.declare) ||
node.type.endsWith('Statement') node.type.endsWith('Statement')
) { ) {
const scope: Statement[][] = [scriptSetupAst.body]
;(walk as any)(node, { ;(walk as any)(node, {
enter(child: Node, parent: Node) { enter(child: Node, parent: Node) {
if (isFunctionType(child)) { if (isFunctionType(child)) {
this.skip() this.skip()
} }
if (child.type === 'BlockStatement') {
scope.push(child.body)
}
if (child.type === 'AwaitExpression') { if (child.type === 'AwaitExpression') {
hasAwait = true hasAwait = true
const needsSemi = scriptSetupAst.body.some(n => { // if the await expression is an expression statement and
return n.type === 'ExpressionStatement' && n.start === child.start // - is in the root scope
// - or is not the first statement in a nested block scope
// then it needs a semicolon before the generated code.
const currentScope = scope[scope.length - 1]
const needsSemi = currentScope.some((n, i) => {
return (
(scope.length === 1 || i > 0) &&
n.type === 'ExpressionStatement' &&
n.start === child.start
)
}) })
processAwait( processAwait(
child, child,
@ -1092,6 +1169,9 @@ export function compileScript(
parent.type === 'ExpressionStatement' parent.type === 'ExpressionStatement'
) )
} }
},
exit(node: Node) {
if (node.type === 'BlockStatement') scope.pop()
} }
}) })
} }
@ -1161,7 +1241,7 @@ export function compileScript(
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS) checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS) checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS) checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_PROPS) checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS)
// 6. remove non-script content // 6. remove non-script content
if (script) { if (script) {
@ -1197,7 +1277,8 @@ export function compileScript(
// props aliases // props aliases
if (propsDestructureDecl) { if (propsDestructureDecl) {
if (propsDestructureRestId) { if (propsDestructureRestId) {
bindingMetadata[propsDestructureRestId] = BindingTypes.SETUP_CONST bindingMetadata[propsDestructureRestId] =
BindingTypes.SETUP_REACTIVE_CONST
} }
for (const key in propsDestructuredBindings) { for (const key in propsDestructuredBindings) {
const { local } = propsDestructuredBindings[key] const { local } = propsDestructuredBindings[key]
@ -1213,7 +1294,9 @@ export function compileScript(
)) { )) {
if (isType) continue if (isType) continue
bindingMetadata[key] = bindingMetadata[key] =
(imported === 'default' && source.endsWith('.vue')) || source === 'vue' imported === '*' ||
(imported === 'default' && source.endsWith('.vue')) ||
source === 'vue'
? BindingTypes.SETUP_CONST ? BindingTypes.SETUP_CONST
: BindingTypes.SETUP_MAYBE_REF : BindingTypes.SETUP_MAYBE_REF
} }
@ -1292,7 +1375,21 @@ export function compileScript(
// 10. generate return statement // 10. generate return statement
let returned let returned
if (options.inlineTemplate) { if (!options.inlineTemplate || (!sfc.template && hasDefaultExportRender)) {
// non-inline mode, or has manual render in normal <script>
// return bindings from script and script setup
const allBindings: Record<string, any> = {
...scriptBindings,
...setupBindings
}
for (const key in userImports) {
if (!userImports[key].isType && userImports[key].isUsedInTemplate) {
allBindings[key] = true
}
}
returned = `{ ${Object.keys(allBindings).join(', ')} }`
} else {
// inline mode
if (sfc.template && !sfc.template.src) { if (sfc.template && !sfc.template.src) {
if (options.templateOptions && options.templateOptions.ssr) { if (options.templateOptions && options.templateOptions.ssr) {
hasInlinedSsrRenderFn = true hasInlinedSsrRenderFn = true
@ -1350,18 +1447,6 @@ export function compileScript(
} else { } else {
returned = `() => {}` returned = `() => {}`
} }
} else {
// return bindings from script and script setup
const allBindings: Record<string, any> = {
...scriptBindings,
...setupBindings
}
for (const key in userImports) {
if (!userImports[key].isType && userImports[key].isUsedInTemplate) {
allBindings[key] = true
}
}
returned = `{ ${Object.keys(allBindings).join(', ')} }`
} }
if (!options.inlineTemplate && !__TEST__) { if (!options.inlineTemplate && !__TEST__) {
@ -1380,6 +1465,12 @@ export function compileScript(
// 11. finalize default export // 11. finalize default export
let runtimeOptions = `` let runtimeOptions = ``
if (!hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
const match = filename.match(/([^/\\]+)\.\w+$/)
if (match) {
runtimeOptions += `\n name: '${match[1]}',`
}
}
if (hasInlinedSsrRenderFn) { if (hasInlinedSsrRenderFn) {
runtimeOptions += `\n __ssrInlineRender: true,` runtimeOptions += `\n __ssrInlineRender: true,`
} }
@ -1509,14 +1600,18 @@ function walkDeclaration(
const userReactiveBinding = userImportAlias['reactive'] || 'reactive' const userReactiveBinding = userImportAlias['reactive'] || 'reactive'
if (isCallOf(init, userReactiveBinding)) { if (isCallOf(init, userReactiveBinding)) {
// treat reactive() calls as let since it's meant to be mutable // treat reactive() calls as let since it's meant to be mutable
bindingType = BindingTypes.SETUP_LET bindingType = isConst
? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_LET
} else if ( } else if (
// if a declaration is a const literal, we can mark it so that // if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it // the generated render fn code doesn't need to unref() it
isDefineCall || isDefineCall ||
(isConst && canNeverBeRef(init!, userReactiveBinding)) (isConst && canNeverBeRef(init!, userReactiveBinding))
) { ) {
bindingType = BindingTypes.SETUP_CONST bindingType = isCallOf(init, DEFINE_PROPS)
? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_CONST
} else if (isConst) { } else if (isConst) {
if (isCallOf(init, userImportAlias['ref'] || 'ref')) { if (isCallOf(init, userImportAlias['ref'] || 'ref')) {
bindingType = BindingTypes.SETUP_REF bindingType = BindingTypes.SETUP_REF
@ -2011,14 +2106,14 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
code += `,v${capitalize(camelize(prop.name))}` code += `,v${capitalize(camelize(prop.name))}`
} }
if (prop.exp) { if (prop.exp) {
code += `,${stripStrings( code += `,${processExp(
(prop.exp as SimpleExpressionNode).content (prop.exp as SimpleExpressionNode).content
)}` )}`
} }
} }
} }
} else if (node.type === NodeTypes.INTERPOLATION) { } else if (node.type === NodeTypes.INTERPOLATION) {
code += `,${stripStrings( code += `,${processExp(
(node.content as SimpleExpressionNode).content (node.content as SimpleExpressionNode).content
)}` )}`
} }
@ -2031,6 +2126,19 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
return code return code
} }
function processExp(exp: string) {
if (/ as \w|<.*>/.test(exp)) {
let ret = ''
// has potential type cast or generic arguments that uses types
const ast = parseExpression(exp, { plugins: ['typescript'] })
walkIdentifiers(ast, node => {
ret += `,` + node.name
})
return ret
}
return stripStrings(exp)
}
function stripStrings(exp: string) { function stripStrings(exp: string) {
return exp return exp
.replace(/'[^']*'|"[^"]*"/g, '') .replace(/'[^']*'|"[^"]*"/g, '')

View File

@ -33,7 +33,7 @@ function genVarName(id: string, raw: string, isProd: boolean): string {
} }
} }
function noramlizeExpression(exp: string) { function normalizeExpression(exp: string) {
exp = exp.trim() exp = exp.trim()
if ( if (
(exp[0] === `'` && exp[exp.length - 1] === `'`) || (exp[0] === `'` && exp[exp.length - 1] === `'`) ||
@ -51,7 +51,7 @@ export function parseCssVars(sfc: SFCDescriptor): string[] {
// ignore v-bind() in comments /* ... */ // ignore v-bind() in comments /* ... */
const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '') const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '')
while ((match = cssVarRE.exec(content))) { while ((match = cssVarRE.exec(content))) {
const variable = noramlizeExpression(match[1]) const variable = normalizeExpression(match[1])
if (!vars.includes(variable)) { if (!vars.includes(variable)) {
vars.push(variable) vars.push(variable)
} }
@ -74,7 +74,7 @@ export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
// rewrite CSS variables // rewrite CSS variables
if (cssVarRE.test(decl.value)) { if (cssVarRE.test(decl.value)) {
decl.value = decl.value.replace(cssVarRE, (_, $1) => { decl.value = decl.value.replace(cssVarRE, (_, $1) => {
return `var(--${genVarName(id, noramlizeExpression($1), isProd)})` return `var(--${genVarName(id, normalizeExpression($1), isProd)})`
}) })
} }
} }

View File

@ -30,12 +30,12 @@ export {
// Types // Types
export { export {
SFCParseOptions, SFCParseOptions,
SFCParseResult,
SFCDescriptor, SFCDescriptor,
SFCBlock, SFCBlock,
SFCTemplateBlock, SFCTemplateBlock,
SFCScriptBlock, SFCScriptBlock,
SFCStyleBlock, SFCStyleBlock
SFCParseResult
} from './parse' } from './parse'
export { export {
TemplateCompiler, TemplateCompiler,

View File

@ -13,6 +13,8 @@ import { parseCssVars } from './cssVars'
import { createCache } from './cache' import { createCache } from './cache'
import { hmrShouldReload, ImportBinding } from './compileScript' import { hmrShouldReload, ImportBinding } from './compileScript'
export const DEFAULT_FILENAME = 'anonymous.vue'
export interface SFCParseOptions { export interface SFCParseOptions {
filename?: string filename?: string
sourceMap?: boolean sourceMap?: boolean
@ -95,7 +97,7 @@ export function parse(
source: string, source: string,
{ {
sourceMap = true, sourceMap = true,
filename = 'anonymous.vue', filename = DEFAULT_FILENAME,
sourceRoot = '', sourceRoot = '',
pad = false, pad = false,
ignoreEmpty = true, ignoreEmpty = true,

View File

@ -176,6 +176,17 @@ function getImportsExpressionExp(
} }
const hashExp = `${name} + '${hash}'` const hashExp = `${name} + '${hash}'`
const finalExp = createSimpleExpression(
hashExp,
false,
loc,
ConstantTypes.CAN_STRINGIFY
)
if (!context.hoistStatic) {
return finalExp
}
const existingHoistIndex = context.hoists.findIndex(h => { const existingHoistIndex = context.hoists.findIndex(h => {
return ( return (
h && h &&
@ -192,9 +203,7 @@ function getImportsExpressionExp(
ConstantTypes.CAN_STRINGIFY ConstantTypes.CAN_STRINGIFY
) )
} }
return context.hoist( return context.hoist(finalExp)
createSimpleExpression(hashExp, false, loc, ConstantTypes.CAN_STRINGIFY)
)
} else { } else {
return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY) return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY)
} }

View File

@ -3,6 +3,7 @@ import {
ConstantTypes, ConstantTypes,
createCompoundExpression, createCompoundExpression,
createSimpleExpression, createSimpleExpression,
ExpressionNode,
NodeTransform, NodeTransform,
NodeTypes, NodeTypes,
SimpleExpressionNode SimpleExpressionNode
@ -68,40 +69,45 @@ export const transformSrcset: NodeTransform = (
} }
} }
const hasQualifiedUrl = imageCandidates.some(({ url }) => { const shouldProcessUrl = (url: string) => {
return ( return (
!isExternalUrl(url) && !isExternalUrl(url) &&
!isDataUrl(url) && !isDataUrl(url) &&
(options.includeAbsolute || isRelativeUrl(url)) (options.includeAbsolute || isRelativeUrl(url))
) )
}) }
// When srcset does not contain any qualified URLs, skip transforming // When srcset does not contain any qualified URLs, skip transforming
if (!hasQualifiedUrl) { if (!imageCandidates.some(({ url }) => shouldProcessUrl(url))) {
return return
} }
if (options.base) { if (options.base) {
const base = options.base const base = options.base
const set: string[] = [] const set: string[] = []
imageCandidates.forEach(({ url, descriptor }) => { let needImportTransform = false
imageCandidates.forEach(candidate => {
let { url, descriptor } = candidate
descriptor = descriptor ? ` ${descriptor}` : `` descriptor = descriptor ? ` ${descriptor}` : ``
if (isRelativeUrl(url)) { if (url[0] === '.') {
set.push((path.posix || path).join(base, url) + descriptor) candidate.url = (path.posix || path).join(base, url)
set.push(candidate.url + descriptor)
} else if (shouldProcessUrl(url)) {
needImportTransform = true
} else { } else {
set.push(url + descriptor) set.push(url + descriptor)
} }
}) })
attr.value.content = set.join(', ')
return if (!needImportTransform) {
attr.value.content = set.join(', ')
return
}
} }
const compoundExpression = createCompoundExpression([], attr.loc) const compoundExpression = createCompoundExpression([], attr.loc)
imageCandidates.forEach(({ url, descriptor }, index) => { imageCandidates.forEach(({ url, descriptor }, index) => {
if ( if (shouldProcessUrl(url)) {
!isExternalUrl(url) &&
!isDataUrl(url) &&
(options.includeAbsolute || isRelativeUrl(url))
) {
const { path } = parseUrl(url) const { path } = parseUrl(url)
let exp: SimpleExpressionNode let exp: SimpleExpressionNode
if (path) { if (path) {
@ -145,14 +151,17 @@ export const transformSrcset: NodeTransform = (
} }
}) })
const hoisted = context.hoist(compoundExpression) let exp: ExpressionNode = compoundExpression
hoisted.constType = ConstantTypes.CAN_STRINGIFY if (context.hoistStatic) {
exp = context.hoist(compoundExpression)
exp.constType = ConstantTypes.CAN_STRINGIFY
}
node.props[index] = { node.props[index] = {
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,
name: 'bind', name: 'bind',
arg: createSimpleExpression('srcset', true, attr.loc), arg: createSimpleExpression('srcset', true, attr.loc),
exp: hoisted, exp,
modifiers: [], modifiers: [],
loc: attr.loc loc: attr.loc
} }

View File

@ -1,4 +1,5 @@
import { compile } from '../src' import { compile } from '../src'
import { ssrHelpers, SSR_RENDER_SLOT_INNER } from '../src/runtimeHelpers'
describe('ssr: <slot>', () => { describe('ssr: <slot>', () => {
test('basic', () => { test('basic', () => {
@ -114,4 +115,16 @@ describe('ssr: <slot>', () => {
}" }"
`) `)
}) })
test('inside transition', () => {
const { code } = compile(`<transition><slot/></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) {
_ssrRenderSlotInner(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
}"
`)
})
}) })

View File

@ -4,6 +4,7 @@ export const SSR_INTERPOLATE = Symbol(`ssrInterpolate`)
export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`) export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`)
export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`) export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`)
export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`) export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`)
export const SSR_RENDER_SLOT_INNER = Symbol(`ssrRenderSlotInner`)
export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`) export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`)
export const SSR_RENDER_STYLE = Symbol(`ssrRenderStyle`) export const SSR_RENDER_STYLE = Symbol(`ssrRenderStyle`)
export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`) export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`)
@ -24,6 +25,7 @@ export const ssrHelpers = {
[SSR_RENDER_VNODE]: `ssrRenderVNode`, [SSR_RENDER_VNODE]: `ssrRenderVNode`,
[SSR_RENDER_COMPONENT]: `ssrRenderComponent`, [SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
[SSR_RENDER_SLOT]: `ssrRenderSlot`, [SSR_RENDER_SLOT]: `ssrRenderSlot`,
[SSR_RENDER_SLOT_INNER]: `ssrRenderSlotInner`,
[SSR_RENDER_CLASS]: `ssrRenderClass`, [SSR_RENDER_CLASS]: `ssrRenderClass`,
[SSR_RENDER_STYLE]: `ssrRenderStyle`, [SSR_RENDER_STYLE]: `ssrRenderStyle`,
[SSR_RENDER_ATTRS]: `ssrRenderAttrs`, [SSR_RENDER_ATTRS]: `ssrRenderAttrs`,

View File

@ -4,9 +4,13 @@ import {
processSlotOutlet, processSlotOutlet,
createCallExpression, createCallExpression,
SlotOutletNode, SlotOutletNode,
createFunctionExpression createFunctionExpression,
NodeTypes,
ElementTypes,
resolveComponentType,
TRANSITION
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { SSR_RENDER_SLOT } from '../runtimeHelpers' import { SSR_RENDER_SLOT, SSR_RENDER_SLOT_INNER } from '../runtimeHelpers'
import { import {
SSRTransformContext, SSRTransformContext,
processChildrenAsStatement processChildrenAsStatement
@ -31,10 +35,24 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
args.push(`"${context.scopeId}-s"`) args.push(`"${context.scopeId}-s"`)
} }
node.ssrCodegenNode = createCallExpression( let method = SSR_RENDER_SLOT
context.helper(SSR_RENDER_SLOT),
args // #3989
) // check if this is a single slot inside a transition wrapper - since
// transition will unwrap the slot fragment into a single vnode at runtime,
// we need to avoid rendering the slot as a fragment.
const parent = context.parent
if (
parent &&
parent.type === NodeTypes.ELEMENT &&
parent.tagType === ElementTypes.COMPONENT &&
resolveComponentType(parent, context, true) === TRANSITION &&
parent.children.filter(c => c.type === NodeTypes.ELEMENT).length === 1
) {
method = SSR_RENDER_SLOT_INNER
}
node.ssrCodegenNode = createCallExpression(context.helper(method), args)
} }
} }

View File

@ -184,7 +184,7 @@ exports[`object destructure 1`] = `
d = _toRef(__$temp_1, 'd', 1), d = _toRef(__$temp_1, 'd', 1),
f = _toRef(__$temp_1, 'e', 2), f = _toRef(__$temp_1, 'e', 2),
h = _toRef(__$temp_1, g) h = _toRef(__$temp_1, g)
let __$temp_2 = (useSomthing(() => 1)), let __$temp_2 = (useSomething(() => 1)),
foo = _toRef(__$temp_2, 'foo'); foo = _toRef(__$temp_2, 'foo');
console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value) console.log(n.value, a.value, c.value, d.value, f.value, h.value, foo.value)
" "

View File

@ -240,7 +240,7 @@ test('should not rewrite scope variable', () => {
test('object destructure', () => { test('object destructure', () => {
const { code, rootRefs } = transform(` const { code, rootRefs } = transform(`
let n = $ref(1), { a, b: c, d = 1, e: f = 2, [g]: h } = $(useFoo()) let n = $ref(1), { a, b: c, d = 1, e: f = 2, [g]: h } = $(useFoo())
let { foo } = $(useSomthing(() => 1)); let { foo } = $(useSomething(() => 1));
console.log(n, a, c, d, f, h, foo) console.log(n, a, c, d, f, h, foo)
`) `)
expect(code).toMatch(`a = _toRef(__$temp_1, 'a')`) expect(code).toMatch(`a = _toRef(__$temp_1, 'a')`)

View File

@ -21,7 +21,7 @@ import {
walkFunctionParams walkFunctionParams
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { parse, ParserPlugin } from '@babel/parser' import { parse, ParserPlugin } from '@babel/parser'
import { hasOwn, isArray, isString } from '@vue/shared' import { hasOwn, isArray, isString, genPropsAccessExp } from '@vue/shared'
const CONVERT_SYMBOL = '$' const CONVERT_SYMBOL = '$'
const ESCAPE_SYMBOL = '$$' const ESCAPE_SYMBOL = '$$'
@ -525,10 +525,10 @@ export function createReactivityTransformer(
`: __props_${propsLocalToPublicMap[id.name]}` `: __props_${propsLocalToPublicMap[id.name]}`
) )
} else { } else {
// { prop } -> { prop: __prop.prop } // { prop } -> { prop: __props.prop }
s.appendLeft( s.appendLeft(
id.end! + offset, id.end! + offset,
`: __props.${propsLocalToPublicMap[id.name]}` `: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`
) )
} }
} else { } else {
@ -539,7 +539,8 @@ export function createReactivityTransformer(
} else { } else {
if (isProp) { if (isProp) {
if (escapeScope) { if (escapeScope) {
// x --> __props_x // prop binding in $$()
// { prop } -> { prop: __props_prop }
registerEscapedPropBinding(id) registerEscapedPropBinding(id)
s.overwrite( s.overwrite(
id.start! + offset, id.start! + offset,
@ -551,7 +552,7 @@ export function createReactivityTransformer(
s.overwrite( s.overwrite(
id.start! + offset, id.start! + offset,
id.end! + offset, id.end! + offset,
`__props.${propsLocalToPublicMap[id.name]}` genPropsAccessExp(propsLocalToPublicMap[id.name])
) )
} }
} else { } else {

View File

@ -66,22 +66,22 @@ describe('reactivity/effect', () => {
it('should observe delete operations', () => { it('should observe delete operations', () => {
let dummy let dummy
const obj = reactive({ prop: 'value' }) const obj = reactive<{
prop?: string
}>({ prop: 'value' })
effect(() => (dummy = obj.prop)) effect(() => (dummy = obj.prop))
expect(dummy).toBe('value') expect(dummy).toBe('value')
// @ts-ignore
delete obj.prop delete obj.prop
expect(dummy).toBe(undefined) expect(dummy).toBe(undefined)
}) })
it('should observe has operations', () => { it('should observe has operations', () => {
let dummy let dummy
const obj = reactive<{ prop: string | number }>({ prop: 'value' }) const obj = reactive<{ prop?: string | number }>({ prop: 'value' })
effect(() => (dummy = 'prop' in obj)) effect(() => (dummy = 'prop' in obj))
expect(dummy).toBe(true) expect(dummy).toBe(true)
// @ts-ignore
delete obj.prop delete obj.prop
expect(dummy).toBe(false) expect(dummy).toBe(false)
obj.prop = 12 obj.prop = 12
@ -90,13 +90,12 @@ describe('reactivity/effect', () => {
it('should observe properties on the prototype chain', () => { it('should observe properties on the prototype chain', () => {
let dummy let dummy
const counter = reactive({ num: 0 }) const counter = reactive<{ num?: number }>({ num: 0 })
const parentCounter = reactive({ num: 2 }) const parentCounter = reactive({ num: 2 })
Object.setPrototypeOf(counter, parentCounter) Object.setPrototypeOf(counter, parentCounter)
effect(() => (dummy = counter.num)) effect(() => (dummy = counter.num))
expect(dummy).toBe(0) expect(dummy).toBe(0)
// @ts-ignore
delete counter.num delete counter.num
expect(dummy).toBe(2) expect(dummy).toBe(2)
parentCounter.num = 4 parentCounter.num = 4
@ -107,16 +106,14 @@ describe('reactivity/effect', () => {
it('should observe has operations on the prototype chain', () => { it('should observe has operations on the prototype chain', () => {
let dummy let dummy
const counter = reactive({ num: 0 }) const counter = reactive<{ num?: number }>({ num: 0 })
const parentCounter = reactive({ num: 2 }) const parentCounter = reactive<{ num?: number }>({ num: 2 })
Object.setPrototypeOf(counter, parentCounter) Object.setPrototypeOf(counter, parentCounter)
effect(() => (dummy = 'num' in counter)) effect(() => (dummy = 'num' in counter))
expect(dummy).toBe(true) expect(dummy).toBe(true)
// @ts-ignore
delete counter.num delete counter.num
expect(dummy).toBe(true) expect(dummy).toBe(true)
// @ts-ignore
delete parentCounter.num delete parentCounter.num
expect(dummy).toBe(false) expect(dummy).toBe(false)
counter.num = 3 counter.num = 3
@ -220,7 +217,7 @@ describe('reactivity/effect', () => {
it('should observe symbol keyed properties', () => { it('should observe symbol keyed properties', () => {
const key = Symbol('symbol keyed prop') const key = Symbol('symbol keyed prop')
let dummy, hasDummy let dummy, hasDummy
const obj = reactive({ [key]: 'value' }) const obj = reactive<{ [key]?: string }>({ [key]: 'value' })
effect(() => (dummy = obj[key])) effect(() => (dummy = obj[key]))
effect(() => (hasDummy = key in obj)) effect(() => (hasDummy = key in obj))
@ -228,7 +225,6 @@ describe('reactivity/effect', () => {
expect(hasDummy).toBe(true) expect(hasDummy).toBe(true)
obj[key] = 'newValue' obj[key] = 'newValue'
expect(dummy).toBe('newValue') expect(dummy).toBe('newValue')
// @ts-ignore
delete obj[key] delete obj[key]
expect(dummy).toBe(undefined) expect(dummy).toBe(undefined)
expect(hasDummy).toBe(false) expect(hasDummy).toBe(false)
@ -752,7 +748,7 @@ describe('reactivity/effect', () => {
const onTrigger = jest.fn((e: DebuggerEvent) => { const onTrigger = jest.fn((e: DebuggerEvent) => {
events.push(e) events.push(e)
}) })
const obj = reactive({ foo: 1 }) const obj = reactive<{ foo?: number }>({ foo: 1 })
const runner = effect( const runner = effect(
() => { () => {
dummy = obj.foo dummy = obj.foo
@ -760,7 +756,7 @@ describe('reactivity/effect', () => {
{ onTrigger } { onTrigger }
) )
obj.foo++ obj.foo!++
expect(dummy).toBe(2) expect(dummy).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1) expect(onTrigger).toHaveBeenCalledTimes(1)
expect(events[0]).toEqual({ expect(events[0]).toEqual({
@ -772,7 +768,6 @@ describe('reactivity/effect', () => {
newValue: 2 newValue: 2
}) })
// @ts-ignore
delete obj.foo delete obj.foo
expect(dummy).toBeUndefined() expect(dummy).toBeUndefined()
expect(onTrigger).toHaveBeenCalledTimes(2) expect(onTrigger).toHaveBeenCalledTimes(2)

View File

@ -120,7 +120,7 @@ describe('reactivity/effect/scope', () => {
counter.num = 6 counter.num = 6
expect(dummy).toBe(7) expect(dummy).toBe(7)
// nested scope should not be stoped // nested scope should not be stopped
expect(doubled).toBe(12) expect(doubled).toBe(12)
}) })
@ -212,7 +212,7 @@ describe('reactivity/effect/scope', () => {
expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledTimes(1)
}) })
it('should derefence child scope from parent scope after stopping child scope (no memleaks)', () => { it('should dereference child scope from parent scope after stopping child scope (no memleaks)', () => {
const parent = new EffectScope() const parent = new EffectScope()
const child = parent.run(() => new EffectScope())! const child = parent.run(() => new EffectScope())!
expect(parent.scopes!.includes(child)).toBe(true) expect(parent.scopes!.includes(child)).toBe(true)

View File

@ -112,14 +112,13 @@ describe('reactivity/reactive/Array', () => {
}) })
test('add non-integer prop on Array should not trigger length dependency', () => { test('add non-integer prop on Array should not trigger length dependency', () => {
const array = new Array(3) const array: any[] & { x?: string } = new Array(3)
const observed = reactive(array) const observed = reactive(array)
const fn = jest.fn() const fn = jest.fn()
effect(() => { effect(() => {
fn(observed.length) fn(observed.length)
}) })
expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledTimes(1)
// @ts-ignore
observed.x = 'x' observed.x = 'x'
expect(fn).toHaveBeenCalledTimes(1) expect(fn).toHaveBeenCalledTimes(1)
observed[-1] = 'x' observed[-1] = 'x'

View File

@ -68,21 +68,21 @@ describe('reactivity/readonly', () => {
`Set operation on key "Symbol(qux)" failed: target is readonly.` `Set operation on key "Symbol(qux)" failed: target is readonly.`
).toHaveBeenWarnedLast() ).toHaveBeenWarnedLast()
// @ts-ignore // @ts-expect-error
delete wrapped.foo delete wrapped.foo
expect(wrapped.foo).toBe(1) expect(wrapped.foo).toBe(1)
expect( expect(
`Delete operation on key "foo" failed: target is readonly.` `Delete operation on key "foo" failed: target is readonly.`
).toHaveBeenWarnedLast() ).toHaveBeenWarnedLast()
// @ts-ignore // @ts-expect-error
delete wrapped.bar.baz delete wrapped.bar.baz
expect(wrapped.bar.baz).toBe(2) expect(wrapped.bar.baz).toBe(2)
expect( expect(
`Delete operation on key "baz" failed: target is readonly.` `Delete operation on key "baz" failed: target is readonly.`
).toHaveBeenWarnedLast() ).toHaveBeenWarnedLast()
// @ts-ignore // @ts-expect-error
delete wrapped[qux] delete wrapped[qux]
expect(wrapped[qux]).toBe(3) expect(wrapped[qux]).toBe(3)
expect( expect(
@ -459,7 +459,7 @@ describe('reactivity/readonly', () => {
expect( expect(
'Set operation on key "_dirty" failed: target is readonly.' 'Set operation on key "_dirty" failed: target is readonly.'
).not.toHaveBeenWarned() ).not.toHaveBeenWarned()
// @ts-expect-error - non-existant property // @ts-expect-error - non-existent property
rC.randomProperty = true rC.randomProperty = true
expect( expect(
@ -476,7 +476,7 @@ describe('reactivity/readonly', () => {
expect(isReadonly(rr.foo)).toBe(true) expect(isReadonly(rr.foo)).toBe(true)
}) })
test('attemptingt to write plain value to a readonly ref nested in a reactive object should fail', () => { test('attempting to write plain value to a readonly ref nested in a reactive object should fail', () => {
const r = ref(false) const r = ref(false)
const ror = readonly(r) const ror = readonly(r)
const obj = reactive({ ror }) const obj = reactive({ ror })

View File

@ -8,7 +8,7 @@ describe('reactivity/shallowReadonly', () => {
test('should make root level properties readonly', () => { test('should make root level properties readonly', () => {
const props = shallowReadonly({ n: 1 }) const props = shallowReadonly({ n: 1 })
// @ts-ignore // @ts-expect-error
props.n = 2 props.n = 2
expect(props.n).toBe(1) expect(props.n).toBe(1)
expect( expect(
@ -19,7 +19,7 @@ describe('reactivity/shallowReadonly', () => {
// to retain 2.x behavior. // to retain 2.x behavior.
test('should NOT make nested properties readonly', () => { test('should NOT make nested properties readonly', () => {
const props = shallowReadonly({ n: { foo: 1 } }) const props = shallowReadonly({ n: { foo: 1 } })
// @ts-ignore
props.n.foo = 2 props.n.foo = 2
expect(props.n.foo).toBe(2) expect(props.n.foo).toBe(2)
expect( expect(

View File

@ -37,6 +37,10 @@ const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
const builtInSymbols = new Set( const builtInSymbols = new Set(
/*#__PURE__*/ /*#__PURE__*/
Object.getOwnPropertyNames(Symbol) Object.getOwnPropertyNames(Symbol)
// ios10.x Object.getOwnPropertyNames(Symbol) can enumerate 'arguments' and 'caller'
// but accessing them on Symbol leads to TypeError because Symbol is a strict mode
// function
.filter(key => key !== 'arguments' && key !== 'caller')
.map(key => (Symbol as any)[key]) .map(key => (Symbol as any)[key])
.filter(isSymbol) .filter(isSymbol)
) )
@ -125,9 +129,8 @@ function createGetter(isReadonly = false, shallow = false) {
} }
if (isRef(res)) { if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key. // ref unwrapping - skip unwrap for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return targetIsArray && isIntegerKey(key) ? res : res.value
return shouldUnwrap ? res.value : res
} }
if (isObject(res)) { if (isObject(res)) {

View File

@ -26,10 +26,12 @@ function get(
target = (target as any)[ReactiveFlags.RAW] target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target) const rawTarget = toRaw(target)
const rawKey = toRaw(key) const rawKey = toRaw(key)
if (key !== rawKey) { if (!isReadonly) {
!isReadonly && track(rawTarget, TrackOpTypes.GET, key) if (key !== rawKey) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
} }
!isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
const { has } = getProto(rawTarget) const { has } = getProto(rawTarget)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) { if (has.call(rawTarget, key)) {
@ -47,10 +49,12 @@ function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = (this as any)[ReactiveFlags.RAW] const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target) const rawTarget = toRaw(target)
const rawKey = toRaw(key) const rawKey = toRaw(key)
if (key !== rawKey) { if (!isReadonly) {
!isReadonly && track(rawTarget, TrackOpTypes.HAS, key) if (key !== rawKey) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
} }
!isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
return key === rawKey return key === rawKey
? target.has(key) ? target.has(key)
: target.has(key) || target.has(rawKey) : target.has(key) || target.has(rawKey)

View File

@ -18,7 +18,7 @@ export class EffectScope {
cleanups: (() => void)[] = [] cleanups: (() => void)[] = []
/** /**
* only assinged by undetached scope * only assigned by undetached scope
* @internal * @internal
*/ */
parent: EffectScope | undefined parent: EffectScope | undefined

View File

@ -11,7 +11,7 @@ import {
shallowCollectionHandlers, shallowCollectionHandlers,
shallowReadonlyCollectionHandlers shallowReadonlyCollectionHandlers
} from './collectionHandlers' } from './collectionHandlers'
import { UnwrapRefSimple, Ref } from './ref' import type { UnwrapRefSimple, Ref, RawSymbol } from './ref'
export const enum ReactiveFlags { export const enum ReactiveFlags {
SKIP = '__v_skip', SKIP = '__v_skip',
@ -204,7 +204,7 @@ function createReactiveObject(
if (existingProxy) { if (existingProxy) {
return existingProxy return existingProxy
} }
// only a whitelist of value types can be observed. // only specific value types can be observed.
const targetType = getTargetType(target) const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) { if (targetType === TargetType.INVALID) {
return target return target
@ -241,7 +241,9 @@ export function toRaw<T>(observed: T): T {
return raw ? toRaw(raw) : observed return raw ? toRaw(raw) : observed
} }
export function markRaw<T extends object>(value: T): T { export function markRaw<T extends object>(
value: T
): T & { [RawSymbol]?: true } {
def(value, ReactiveFlags.SKIP, true) def(value, ReactiveFlags.SKIP, true)
return value return value
} }

View File

@ -12,6 +12,7 @@ import { CollectionTypes } from './collectionHandlers'
import { createDep, Dep } from './dep' import { createDep, Dep } from './dep'
declare const RefSymbol: unique symbol declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol
export interface Ref<T = any> { export interface Ref<T = any> {
value: T value: T
@ -291,6 +292,7 @@ export type UnwrapRefSimple<T> = T extends
| BaseTypes | BaseTypes
| Ref | Ref
| RefUnwrapBailTypes[keyof RefUnwrapBailTypes] | RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
| { [RawSymbol]?: true }
? T ? T
: T extends Array<any> : T extends Array<any>
? { [K in keyof T]: UnwrapRefSimple<T[K]> } ? { [K in keyof T]: UnwrapRefSimple<T[K]> }

View File

@ -4,9 +4,11 @@ import {
Component, Component,
ref, ref,
nextTick, nextTick,
Suspense Suspense,
KeepAlive
} from '../src' } from '../src'
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test' import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
import { onActivated } from '../src/components/KeepAlive'
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
@ -799,4 +801,44 @@ describe('api: defineAsyncComponent', () => {
expect(vnodeHooks.onVnodeBeforeUnmount).toHaveBeenCalledTimes(1) expect(vnodeHooks.onVnodeBeforeUnmount).toHaveBeenCalledTimes(1)
expect(vnodeHooks.onVnodeUnmounted).toHaveBeenCalledTimes(1) expect(vnodeHooks.onVnodeUnmounted).toHaveBeenCalledTimes(1)
}) })
test('with KeepAlive', async () => {
const spy = jest.fn()
let resolve: (comp: Component) => void
const Foo = defineAsyncComponent(
() =>
new Promise(r => {
resolve = r as any
})
)
const Bar = defineAsyncComponent(() => Promise.resolve(() => 'Bar'))
const toggle = ref(true)
const root = nodeOps.createElement('div')
const app = createApp({
render: () => h(KeepAlive, [toggle.value ? h(Foo) : h(Bar)])
})
app.mount(root)
await nextTick()
resolve!({
setup() {
onActivated(() => {
spy()
})
return () => 'Foo'
}
})
await timeout()
expect(serializeInner(root)).toBe('Foo')
expect(spy).toBeCalledTimes(1)
toggle.value = false
await timeout()
expect(serializeInner(root)).toBe('Bar')
})
}) })

View File

@ -30,6 +30,11 @@ describe('api: createApp', () => {
const root1 = nodeOps.createElement('div') const root1 = nodeOps.createElement('div')
createApp(Comp).mount(root1) createApp(Comp).mount(root1)
expect(serializeInner(root1)).toBe(`0`) expect(serializeInner(root1)).toBe(`0`)
//#5571 mount multiple apps to the same host element
createApp(Comp).mount(root1)
expect(
`There is already an app instance mounted on the host container`
).toHaveBeenWarned()
// mount with props // mount with props
const root2 = nodeOps.createElement('div') const root2 = nodeOps.createElement('div')

View File

@ -334,7 +334,10 @@ describe('api: lifecycle hooks', () => {
const onTrigger = jest.fn((e: DebuggerEvent) => { const onTrigger = jest.fn((e: DebuggerEvent) => {
events.push(e) events.push(e)
}) })
const obj = reactive({ foo: 1, bar: 2 }) const obj = reactive<{
foo: number
bar?: number
}>({ foo: 1, bar: 2 })
const Comp = { const Comp = {
setup() { setup() {
@ -356,7 +359,6 @@ describe('api: lifecycle hooks', () => {
newValue: 2 newValue: 2
}) })
// @ts-ignore
delete obj.bar delete obj.bar
await nextTick() await nextTick()
expect(onTrigger).toHaveBeenCalledTimes(2) expect(onTrigger).toHaveBeenCalledTimes(2)

View File

@ -1411,7 +1411,7 @@ describe('api: options', () => {
} }
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
// @ts-ignore // @ts-expect-error
render(h(Comp), root) render(h(Comp), root)
expect('Invalid watch option: "foo"').toHaveBeenWarned() expect('Invalid watch option: "foo"').toHaveBeenWarned()

View File

@ -214,7 +214,7 @@ describe('api: watch', () => {
}) })
it('warn invalid watch source', () => { it('warn invalid watch source', () => {
// @ts-ignore // @ts-expect-error
watch(1, () => {}) watch(1, () => {})
expect(`Invalid watch source`).toHaveBeenWarned() expect(`Invalid watch source`).toHaveBeenWarned()
}) })
@ -748,7 +748,7 @@ describe('api: watch', () => {
() => { () => {
dummy = count.value dummy = count.value
}, },
// @ts-ignore // @ts-expect-error
{ immediate: false } { immediate: false }
) )
expect(dummy).toBe(0) expect(dummy).toBe(0)
@ -767,7 +767,7 @@ describe('api: watch', () => {
spy() spy()
return arr return arr
}, },
// @ts-ignore // @ts-expect-error
{ deep: true } { deep: true }
) )
expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledTimes(1)
@ -818,7 +818,7 @@ describe('api: watch', () => {
const onTrigger = jest.fn((e: DebuggerEvent) => { const onTrigger = jest.fn((e: DebuggerEvent) => {
events.push(e) events.push(e)
}) })
const obj = reactive({ foo: 1 }) const obj = reactive<{ foo?: number }>({ foo: 1 })
watchEffect( watchEffect(
() => { () => {
dummy = obj.foo dummy = obj.foo
@ -828,7 +828,7 @@ describe('api: watch', () => {
await nextTick() await nextTick()
expect(dummy).toBe(1) expect(dummy).toBe(1)
obj.foo++ obj.foo!++
await nextTick() await nextTick()
expect(dummy).toBe(2) expect(dummy).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1) expect(onTrigger).toHaveBeenCalledTimes(1)
@ -839,7 +839,6 @@ describe('api: watch', () => {
newValue: 2 newValue: 2
}) })
// @ts-ignore
delete obj.foo delete obj.foo
await nextTick() await nextTick()
expect(dummy).toBeUndefined() expect(dummy).toBeUndefined()
@ -893,6 +892,21 @@ describe('api: watch', () => {
expect(sideEffect).toBe(2) expect(sideEffect).toBe(2)
}) })
test('should force trigger on triggerRef when watching multiple sources: shallow ref array', async () => {
const v = shallowRef([] as any)
const spy = jest.fn()
watch([v], () => {
spy()
})
v.value.push(1)
triggerRef(v)
await nextTick()
// should trigger now
expect(spy).toHaveBeenCalledTimes(1)
})
// #2125 // #2125
test('watchEffect should not recursively trigger itself', async () => { test('watchEffect should not recursively trigger itself', async () => {
const spy = jest.fn() const spy = jest.fn()

View File

@ -153,7 +153,7 @@ describe('component: emit', () => {
emits: ['foo'], emits: ['foo'],
render() {}, render() {},
created() { created() {
// @ts-ignore // @ts-expect-error
this.$emit('bar') this.$emit('bar')
} }
}) })
@ -170,7 +170,7 @@ describe('component: emit', () => {
}, },
render() {}, render() {},
created() { created() {
// @ts-ignore // @ts-expect-error
this.$emit('bar') this.$emit('bar')
} }
}) })
@ -186,7 +186,7 @@ describe('component: emit', () => {
emits: [], emits: [],
render() {}, render() {},
created() { created() {
// @ts-ignore // @ts-expect-error
this.$emit('foo') this.$emit('foo')
} }
}) })
@ -355,6 +355,37 @@ describe('component: emit', () => {
expect(fn2).toHaveBeenCalledWith('two') expect(fn2).toHaveBeenCalledWith('two')
}) })
test('.trim and .number modifiers should work with v-model on component', () => {
const Foo = defineComponent({
render() {},
created() {
this.$emit('update:modelValue', ' +01.2 ')
this.$emit('update:foo', ' 1 ')
}
})
const fn1 = jest.fn()
const fn2 = jest.fn()
const Comp = () =>
h(Foo, {
modelValue: null,
modelModifiers: { trim: true, number: true },
'onUpdate:modelValue': fn1,
foo: null,
fooModifiers: { trim: true, number: true },
'onUpdate:foo': fn2
})
render(h(Comp), nodeOps.createElement('div'))
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn1).toHaveBeenCalledWith(1.2)
expect(fn2).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledWith(1)
})
test('isEmitListener', () => { test('isEmitListener', () => {
const options = { const options = {
click: null, click: null,

View File

@ -257,7 +257,6 @@ describe('component: proxy', () => {
expect(instanceProxy.isDisplayed).toBe(true) expect(instanceProxy.isDisplayed).toBe(true)
}) })
test('allow jest spying on proxy methods with Object.defineProperty', () => { test('allow jest spying on proxy methods with Object.defineProperty', () => {
// #5417 // #5417
let instanceProxy: any let instanceProxy: any
@ -426,7 +425,6 @@ describe('component: proxy', () => {
expect(instanceProxy.fromProp).toBe(false) expect(instanceProxy.fromProp).toBe(false)
}) })
// #864 // #864
test('should not warn declared but absent props', () => { test('should not warn declared but absent props', () => {
const Comp = { const Comp = {

View File

@ -18,7 +18,12 @@ import {
defineAsyncComponent, defineAsyncComponent,
Component, Component,
createApp, createApp,
onActivated onActivated,
onUnmounted,
onMounted,
reactive,
shallowRef,
onDeactivated
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { KeepAliveProps } from '../../src/components/KeepAlive' import { KeepAliveProps } from '../../src/components/KeepAlive'
@ -903,4 +908,73 @@ describe('KeepAlive', () => {
await nextTick() await nextTick()
expect(handler).toHaveBeenCalledWith(err, {}, 'activated hook') expect(handler).toHaveBeenCalledWith(err, {}, 'activated hook')
}) })
// #3648
test('should avoid unmount later included components', async () => {
const unmountedA = jest.fn()
const mountedA = jest.fn()
const activatedA = jest.fn()
const deactivatedA = jest.fn()
const unmountedB = jest.fn()
const mountedB = jest.fn()
const A = {
name: 'A',
setup() {
onMounted(mountedA)
onUnmounted(unmountedA)
onActivated(activatedA)
onDeactivated(deactivatedA)
return () => 'A'
}
}
const B = {
name: 'B',
setup() {
onMounted(mountedB)
onUnmounted(unmountedB)
return () => 'B'
}
}
const include = reactive<string[]>([])
const current = shallowRef(A)
const app = createApp({
setup() {
return () => {
return [
h(
KeepAlive,
{
include
},
h(current.value)
)
]
}
}
})
app.mount(root)
expect(serializeInner(root)).toBe(`A`)
expect(mountedA).toHaveBeenCalledTimes(1)
expect(unmountedA).toHaveBeenCalledTimes(0)
expect(activatedA).toHaveBeenCalledTimes(0)
expect(deactivatedA).toHaveBeenCalledTimes(0)
expect(mountedB).toHaveBeenCalledTimes(0)
expect(unmountedB).toHaveBeenCalledTimes(0)
include.push('A') // cache A
await nextTick()
current.value = B // toggle to B
await nextTick()
expect(serializeInner(root)).toBe(`B`)
expect(mountedA).toHaveBeenCalledTimes(1)
expect(unmountedA).toHaveBeenCalledTimes(0)
expect(activatedA).toHaveBeenCalledTimes(0)
expect(deactivatedA).toHaveBeenCalledTimes(1)
expect(mountedB).toHaveBeenCalledTimes(1)
expect(unmountedB).toHaveBeenCalledTimes(0)
})
}) })

View File

@ -1032,7 +1032,7 @@ describe('Suspense', () => {
await nextTick() await nextTick()
expect(deps.length).toBe(2) expect(deps.length).toBe(2)
// switch before two resovles // switch before two resolves
view.value = Three view.value = Three
await nextTick() await nextTick()
expect(deps.length).toBe(3) expect(deps.length).toBe(3)
@ -1098,7 +1098,7 @@ describe('Suspense', () => {
await nextTick() await nextTick()
expect(deps.length).toBe(2) expect(deps.length).toBe(2)
// switch back before two resovles // switch back before two resolves
view.value = One view.value = One
await nextTick() await nextTick()
expect(deps.length).toBe(2) expect(deps.length).toBe(2)

View File

@ -183,7 +183,7 @@ describe('error handling', () => {
}) })
// unlike other lifecycle hooks, created/beforeCreate are called as part of // unlike other lifecycle hooks, created/beforeCreate are called as part of
// the options API initiualization process instead of by the renderer. // the options API initialization process instead of by the renderer.
test('in created/beforeCreate hook', () => { test('in created/beforeCreate hook', () => {
const err = new Error('foo') const err = new Error('foo')
const fn = jest.fn() const fn = jest.fn()

View File

@ -22,7 +22,9 @@ describe('renderList', () => {
}) })
it('should warn when given a non-integer N', () => { it('should warn when given a non-integer N', () => {
renderList(3.1, () => {}) try {
renderList(3.1, () => {})
} catch (e) {}
expect( expect(
`The v-for range expect an integer value but got 3.1.` `The v-for range expect an integer value but got 3.1.`
).toHaveBeenWarned() ).toHaveBeenWarned()

View File

@ -1,5 +1,5 @@
// since v-memo really is a compiler + runtime combo feature, we are performing // since v-memo really is a compiler + runtime combo feature, we are performing
// more of an itegration test here. // more of an integration test here.
import { ComponentOptions, createApp, nextTick } from 'vue' import { ComponentOptions, createApp, nextTick } from 'vue'
describe('v-memo', () => { describe('v-memo', () => {
@ -210,4 +210,17 @@ describe('v-memo', () => {
// should update // should update
expect(el.innerHTML).toBe(`<div>2</div><div>2</div><div>2</div>`) expect(el.innerHTML).toBe(`<div>2</div><div>2</div><div>2</div>`)
}) })
test('v-memo dependency is NaN should be equal', async () => {
const [el, vm] = mount({
template: `<div v-memo="[x]">{{ y }}</div>`,
data: () => ({ x: NaN, y: 0 })
})
expect(el.innerHTML).toBe(`<div>0</div>`)
vm.y++
// should not update
await nextTick()
expect(el.innerHTML).toBe(`<div>0</div>`)
})
}) })

View File

@ -879,7 +879,7 @@ describe('renderer: optimized mode', () => {
// #3881 // #3881
// root cause: fragment inside a compiled slot passed to component which // root cause: fragment inside a compiled slot passed to component which
// programmatically invokes the slot. The entire slot should de-opt but // programmatically invokes the slot. The entire slot should de-opt but
// the fragment was incorretly put in optimized mode which causes it to skip // the fragment was incorrectly put in optimized mode which causes it to skip
// updates for its inner components. // updates for its inner components.
test('fragments inside programmatically invoked compiled slot should de-opt properly', async () => { test('fragments inside programmatically invoked compiled slot should de-opt properly', async () => {
const Parent: FunctionalComponent = (_, { slots }) => slots.default!() const Parent: FunctionalComponent = (_, { slots }) => slots.default!()

View File

@ -443,10 +443,8 @@ describe('api: template refs', () => {
expect(mapRefs()).toMatchObject(['2', '3', '4']) expect(mapRefs()).toMatchObject(['2', '3', '4'])
}) })
test('named ref in v-for', async () => { test('named ref in v-for', async () => {
const show = ref(true); const show = ref(true)
const list = reactive([1, 2, 3]) const list = reactive([1, 2, 3])
const listRefs = ref([]) const listRefs = ref([])
const mapRefs = () => listRefs.value.map(n => serializeInner(n)) const mapRefs = () => listRefs.value.map(n => serializeInner(n))
@ -495,6 +493,4 @@ describe('api: template refs', () => {
await nextTick() await nextTick()
expect(mapRefs()).toMatchObject(['2', '3', '4']) expect(mapRefs()).toMatchObject(['2', '3', '4'])
}) })
}) })

View File

@ -541,18 +541,6 @@ describe('scheduler', () => {
expect(count).toBe(5) expect(count).toBe(5)
}) })
test('should prevent duplicate queue', async () => {
let count = 0
const job = () => {
count++
}
job.cb = true
queueJob(job)
queueJob(job)
await nextTick()
expect(count).toBe(1)
})
// #1947 flushPostFlushCbs should handle nested calls // #1947 flushPostFlushCbs should handle nested calls
// e.g. app.mount inside app.mount // e.g. app.mount inside app.mount
test('flushPostFlushCbs', async () => { test('flushPostFlushCbs', async () => {

View File

@ -111,7 +111,7 @@ export function defineAsyncComponent<
) )
} }
return defineComponent({ return defineComponent<{}>({
name: 'AsyncComponentWrapper', name: 'AsyncComponentWrapper',
__asyncLoader: load, __asyncLoader: load,
@ -211,7 +211,10 @@ export function defineAsyncComponent<
function createInnerComp( function createInnerComp(
comp: ConcreteComponent, comp: ConcreteComponent,
{ vnode: { ref, props, children } }: ComponentInternalInstance {
vnode: { ref, props, children, shapeFlag },
parent
}: ComponentInternalInstance
) { ) {
const vnode = createVNode(comp, props, children) const vnode = createVNode(comp, props, children)
// ensure inner component inherits the async wrapper's ref owner // ensure inner component inherits the async wrapper's ref owner

View File

@ -284,6 +284,14 @@ export function createAppAPI<HostElement>(
isSVG?: boolean isSVG?: boolean
): any { ): any {
if (!isMounted) { if (!isMounted) {
// #5571
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`
)
}
const vnode = createVNode( const vnode = createVNode(
rootComponent as ConcreteComponent, rootComponent as ConcreteComponent,
rootProps rootProps
@ -345,9 +353,8 @@ export function createAppAPI<HostElement>(
`It will be overwritten with the new value.` `It will be overwritten with the new value.`
) )
} }
// TypeScript doesn't allow symbols as index type
// https://github.com/Microsoft/TypeScript/issues/24587 context.provides[key as string | symbol] = value
context.provides[key as string] = value
return app return app
} }

View File

@ -6,7 +6,8 @@ import {
ComponentOptionsWithObjectProps, ComponentOptionsWithObjectProps,
ComponentOptionsMixin, ComponentOptionsMixin,
RenderFunction, RenderFunction,
ComponentOptionsBase ComponentOptionsBase,
ComponentProvideOptions
} from './componentOptions' } from './componentOptions'
import { import {
SetupContext, SetupContext,
@ -40,6 +41,8 @@ export type DefineComponent<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {}, E extends EmitsOptions = {},
EE extends string = string, EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
RawOptions extends {} = {},
PP = PublicProps, PP = PublicProps,
Props = Readonly< Props = Readonly<
PropsOrPropOptions extends ComponentPropsOptions PropsOrPropOptions extends ComponentPropsOptions
@ -48,22 +51,23 @@ export type DefineComponent<
> & > &
({} extends E ? {} : EmitsToProps<E>), ({} extends E ? {} : EmitsToProps<E>),
Defaults = ExtractDefaultPropTypes<PropsOrPropOptions> Defaults = ExtractDefaultPropTypes<PropsOrPropOptions>
> = ComponentPublicInstanceConstructor< > = RawOptions &
CreateComponentPublicInstance< ComponentPublicInstanceConstructor<
Props, CreateComponentPublicInstance<
RawBindings, Props,
D, RawBindings,
C, D,
M, C,
Mixin, M,
Extends, Mixin,
E, Extends,
PP & Props, E,
Defaults, PP & Props,
true Defaults,
true
> &
Props
> & > &
Props
> &
ComponentOptionsBase< ComponentOptionsBase<
Props, Props,
RawBindings, RawBindings,
@ -74,7 +78,8 @@ export type DefineComponent<
Extends, Extends,
E, E,
EE, EE,
Defaults Defaults,
Provide
> & > &
PP PP
@ -104,20 +109,36 @@ export function defineComponent<
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Options extends {} = {}
>( >(
options: ComponentOptionsWithoutProps< options: Options &
Props, ComponentOptionsWithoutProps<
RawBindings, Props,
D, RawBindings,
C, D,
M, C,
Mixin, M,
Extends, Mixin,
E, Extends,
EE E,
> EE,
): DefineComponent<Props, RawBindings, D, C, M, Mixin, Extends, E, EE> Provide
>
): DefineComponent<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE,
Provide,
Options
>
// overload 3: object format with array props declaration // overload 3: object format with array props declaration
// props inferred as { [key in PropNames]?: any } // props inferred as { [key in PropNames]?: any }
@ -131,19 +152,23 @@ export function defineComponent<
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = Record<string, any>, E extends EmitsOptions = Record<string, any>,
EE extends string = string EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Options extends {} = {}
>( >(
options: ComponentOptionsWithArrayProps< options: Options &
PropNames, ComponentOptionsWithArrayProps<
RawBindings, PropNames,
D, RawBindings,
C, D,
M, C,
Mixin, M,
Extends, Mixin,
E, Extends,
EE E,
> EE,
Provide
>
): DefineComponent< ): DefineComponent<
Readonly<{ [key in PropNames]?: any }>, Readonly<{ [key in PropNames]?: any }>,
RawBindings, RawBindings,
@ -153,7 +178,9 @@ export function defineComponent<
Mixin, Mixin,
Extends, Extends,
E, E,
EE EE,
Provide,
Options
> >
// overload 4: object format with object props declaration // overload 4: object format with object props declaration
@ -169,20 +196,36 @@ export function defineComponent<
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = Record<string, any>, E extends EmitsOptions = Record<string, any>,
EE extends string = string EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Options extends {} = {}
>( >(
options: ComponentOptionsWithObjectProps< options: Options &
PropsOptions, ComponentOptionsWithObjectProps<
RawBindings, PropsOptions,
D, RawBindings,
C, D,
M, C,
Mixin, M,
Extends, Mixin,
E, Extends,
EE E,
> EE,
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE> Provide
>
): DefineComponent<
PropsOptions,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE,
Provide,
Options
>
// implementation, close to no-op // implementation, close to no-op
export function defineComponent(options: unknown) { export function defineComponent(options: unknown) {

View File

@ -212,7 +212,7 @@ function doWatch(
deep = true deep = true
} else if (isArray(source)) { } else if (isArray(source)) {
isMultiSource = true isMultiSource = true
forceTrigger = source.some(isReactive) forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () => getter = () =>
source.map(s => { source.map(s => {
if (isRef(s)) { if (isRef(s)) {

View File

@ -440,6 +440,15 @@ export interface ComponentInternalInstance {
* @internal * @internal
*/ */
[LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>> [LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
/**
* For caching bound $forceUpdate on public proxy access
*/
f?: () => void
/**
* For caching bound $nextTick on public proxy access
*/
n?: () => Promise<void>
} }
const emptyAppContext = createAppContext() const emptyAppContext = createAppContext()
@ -477,7 +486,7 @@ export function createComponentInstance(
accessCache: null!, accessCache: null!,
renderCache: [], renderCache: [],
// local resovled assets // local resolved assets
components: null, components: null,
directives: null, directives: null,

View File

@ -122,7 +122,8 @@ export function emit(
const { number, trim } = props[modifiersKey] || EMPTY_OBJ const { number, trim } = props[modifiersKey] || EMPTY_OBJ
if (trim) { if (trim) {
args = rawArgs.map(a => a.trim()) args = rawArgs.map(a => a.trim())
} else if (number) { }
if (number) {
args = rawArgs.map(toNumber) args = rawArgs.map(toNumber)
} }
} }

View File

@ -117,8 +117,9 @@ export interface ComponentOptionsBase<
Extends extends ComponentOptionsMixin, Extends extends ComponentOptionsMixin,
E extends EmitsOptions, E extends EmitsOptions,
EE extends string = string, EE extends string = string,
Defaults = {} Defaults = {},
> extends LegacyOptions<Props, D, C, M, Mixin, Extends>, Provide extends ComponentProvideOptions = ComponentProvideOptions
> extends LegacyOptions<Props, D, C, M, Mixin, Extends, Provide>,
ComponentInternalOptions, ComponentInternalOptions,
ComponentCustomOptions { ComponentCustomOptions {
setup?: ( setup?: (
@ -224,6 +225,7 @@ export type ComponentOptionsWithoutProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
PE = Props & EmitsToProps<E> PE = Props & EmitsToProps<E>
> = ComponentOptionsBase< > = ComponentOptionsBase<
PE, PE,
@ -235,7 +237,8 @@ export type ComponentOptionsWithoutProps<
Extends, Extends,
E, E,
EE, EE,
{} {},
Provide
> & { > & {
props?: undefined props?: undefined
} & ThisType< } & ThisType<
@ -252,6 +255,7 @@ export type ComponentOptionsWithArrayProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps<E> Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps<E>
> = ComponentOptionsBase< > = ComponentOptionsBase<
Props, Props,
@ -263,7 +267,8 @@ export type ComponentOptionsWithArrayProps<
Extends, Extends,
E, E,
EE, EE,
{} {},
Provide
> & { > & {
props: PropNames[] props: PropNames[]
} & ThisType< } & ThisType<
@ -289,6 +294,7 @@ export type ComponentOptionsWithObjectProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Props = Readonly<ExtractPropTypes<PropsOptions>> & EmitsToProps<E>, Props = Readonly<ExtractPropTypes<PropsOptions>> & EmitsToProps<E>,
Defaults = ExtractDefaultPropTypes<PropsOptions> Defaults = ExtractDefaultPropTypes<PropsOptions>
> = ComponentOptionsBase< > = ComponentOptionsBase<
@ -301,7 +307,8 @@ export type ComponentOptionsWithObjectProps<
Extends, Extends,
E, E,
EE, EE,
Defaults Defaults,
Provide
> & { > & {
props: PropsOptions & ThisType<void> props: PropsOptions & ThisType<void>
} & ThisType< } & ThisType<
@ -384,6 +391,10 @@ type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[]
type ComponentWatchOptions = Record<string, ComponentWatchOptionItem> type ComponentWatchOptions = Record<string, ComponentWatchOptionItem>
export type ComponentProvideOptions = ObjectProvideOptions | Function
type ObjectProvideOptions = Record<string | symbol, unknown>
type ComponentInjectOptions = string[] | ObjectInjectOptions type ComponentInjectOptions = string[] | ObjectInjectOptions
type ObjectInjectOptions = Record< type ObjectInjectOptions = Record<
@ -397,7 +408,8 @@ interface LegacyOptions<
C extends ComputedOptions, C extends ComputedOptions,
M extends MethodOptions, M extends MethodOptions,
Mixin extends ComponentOptionsMixin, Mixin extends ComponentOptionsMixin,
Extends extends ComponentOptionsMixin Extends extends ComponentOptionsMixin,
Provide extends ComponentProvideOptions = ComponentProvideOptions
> { > {
compatConfig?: CompatConfig compatConfig?: CompatConfig
@ -431,7 +443,7 @@ interface LegacyOptions<
computed?: C computed?: C
methods?: M methods?: M
watch?: ComponentWatchOptions watch?: ComponentWatchOptions
provide?: Data | Function provide?: Provide
inject?: ComponentInjectOptions inject?: ComponentInjectOptions
// assets // assets
@ -471,8 +483,8 @@ interface LegacyOptions<
* *
* type-only, used to assist Mixin's type inference, * type-only, used to assist Mixin's type inference,
* typescript will try to simplify the inferred `Mixin` type, * typescript will try to simplify the inferred `Mixin` type,
* with the `__differenciator`, typescript won't be able to combine different mixins, * with the `__differentiator`, typescript won't be able to combine different mixins,
* because the `__differenciator` will be different * because the `__differentiator` will be different
*/ */
__differentiator?: keyof D | keyof C | keyof M __differentiator?: keyof D | keyof C | keyof M
} }

View File

@ -135,7 +135,8 @@ const enum BooleanFlags {
// extract props which defined with default from prop options // extract props which defined with default from prop options
export type ExtractDefaultPropTypes<O> = O extends object export type ExtractDefaultPropTypes<O> = O extends object
? { [K in DefaultKeys<O>]: InferPropType<O[K]> } ? // use `keyof Pick<O, DefaultKeys<O>>` instead of `DefaultKeys<O>` to support IDE features
{ [K in keyof Pick<O, DefaultKeys<O>>]: InferPropType<O[K]> }
: {} : {}
type NormalizedProp = type NormalizedProp =
@ -225,7 +226,7 @@ export function updateProps(
for (let i = 0; i < propsToUpdate.length; i++) { for (let i = 0; i < propsToUpdate.length; i++) {
let key = propsToUpdate[i] let key = propsToUpdate[i]
// skip if the prop key is a declared emit event listener // skip if the prop key is a declared emit event listener
if (isEmitListener(instance.emitsOptions, key)){ if (isEmitListener(instance.emitsOptions, key)) {
continue continue
} }
// PROPS flag guarantees rawProps to be non-null // PROPS flag guarantees rawProps to be non-null

View File

@ -34,7 +34,8 @@ import {
OptionTypesKeys, OptionTypesKeys,
resolveMergedOptions, resolveMergedOptions,
shouldCacheAccess, shouldCacheAccess,
MergedComponentOptionsOverride MergedComponentOptionsOverride,
ComponentProvideOptions
} from './componentOptions' } from './componentOptions'
import { EmitsOptions, EmitFn } from './componentEmits' import { EmitsOptions, EmitFn } from './componentEmits'
import { Slots } from './componentSlots' import { Slots } from './componentSlots'
@ -150,7 +151,8 @@ export type CreateComponentPublicInstance<
PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> & PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
EnsureNonVoid<M>, EnsureNonVoid<M>,
PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> & PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
EnsureNonVoid<Defaults> EnsureNonVoid<Defaults>,
Provide extends ComponentProvideOptions = ComponentProvideOptions
> = ComponentPublicInstance< > = ComponentPublicInstance<
PublicP, PublicP,
PublicB, PublicB,
@ -161,7 +163,19 @@ export type CreateComponentPublicInstance<
PublicProps, PublicProps,
PublicDefaults, PublicDefaults,
MakeDefaultsOptional, MakeDefaultsOptional,
ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E, string, Defaults> ComponentOptionsBase<
P,
B,
D,
C,
M,
Mixin,
Extends,
E,
string,
Defaults,
Provide
>
> >
// public properties exposed on the proxy, which is used as the render context // public properties exposed on the proxy, which is used as the render context
@ -238,8 +252,8 @@ export const publicPropertiesMap: PublicPropertiesMap =
$root: i => getPublicInstance(i.root), $root: i => getPublicInstance(i.root),
$emit: i => i.emit, $emit: i => i.emit,
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type), $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
$forceUpdate: i => () => queueJob(i.update), $forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
$nextTick: i => nextTick.bind(i.proxy!), $nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
} as PublicPropertiesMap) } as PublicPropertiesMap)

View File

@ -215,6 +215,8 @@ export function renderComponentRoot(
`The directives will not function as intended.` `The directives will not function as intended.`
) )
} }
// clone before mutating since the root may be a hoisted vnode
root = cloneVNode(root)
root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
} }
// inherit transition data // inherit transition data

View File

@ -63,6 +63,10 @@ const normalizeSlot = (
rawSlot: Function, rawSlot: Function,
ctx: ComponentInternalInstance | null | undefined ctx: ComponentInternalInstance | null | undefined
): Slot => { ): Slot => {
if ((rawSlot as any)._n) {
// already normalized - #5353
return rawSlot as Slot
}
const normalized = withCtx((...args: any[]) => { const normalized = withCtx((...args: any[]) => {
if (__DEV__ && currentInstance) { if (__DEV__ && currentInstance) {
warn( warn(

View File

@ -16,10 +16,12 @@ import { warn } from '../warning'
import { isKeepAlive } from './KeepAlive' import { isKeepAlive } from './KeepAlive'
import { toRaw } from '@vue/reactivity' import { toRaw } from '@vue/reactivity'
import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling' import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
import { ShapeFlags, PatchFlags } from '@vue/shared' import { ShapeFlags, PatchFlags, isArray } from '@vue/shared'
import { onBeforeUnmount, onMounted } from '../apiLifecycle' import { onBeforeUnmount, onMounted } from '../apiLifecycle'
import { RendererElement } from '../renderer' import { RendererElement } from '../renderer'
type Hook<T = () => void> = T | T[]
export interface BaseTransitionProps<HostElement = RendererElement> { export interface BaseTransitionProps<HostElement = RendererElement> {
mode?: 'in-out' | 'out-in' | 'default' mode?: 'in-out' | 'out-in' | 'default'
appear?: boolean appear?: boolean
@ -34,20 +36,20 @@ export interface BaseTransitionProps<HostElement = RendererElement> {
// Hooks. Using camel case for easier usage in render functions & JSX. // Hooks. Using camel case for easier usage in render functions & JSX.
// In templates these can be written as @before-enter="xxx" as prop names // In templates these can be written as @before-enter="xxx" as prop names
// are camelized. // are camelized.
onBeforeEnter?: (el: HostElement) => void onBeforeEnter?: Hook<(el: HostElement) => void>
onEnter?: (el: HostElement, done: () => void) => void onEnter?: Hook<(el: HostElement, done: () => void) => void>
onAfterEnter?: (el: HostElement) => void onAfterEnter?: Hook<(el: HostElement) => void>
onEnterCancelled?: (el: HostElement) => void onEnterCancelled?: Hook<(el: HostElement) => void>
// leave // leave
onBeforeLeave?: (el: HostElement) => void onBeforeLeave?: Hook<(el: HostElement) => void>
onLeave?: (el: HostElement, done: () => void) => void onLeave?: Hook<(el: HostElement, done: () => void) => void>
onAfterLeave?: (el: HostElement) => void onAfterLeave?: Hook<(el: HostElement) => void>
onLeaveCancelled?: (el: HostElement) => void // only fired in persisted mode onLeaveCancelled?: Hook<(el: HostElement) => void> // only fired in persisted mode
// appear // appear
onBeforeAppear?: (el: HostElement) => void onBeforeAppear?: Hook<(el: HostElement) => void>
onAppear?: (el: HostElement, done: () => void) => void onAppear?: Hook<(el: HostElement, done: () => void) => void>
onAfterAppear?: (el: HostElement) => void onAfterAppear?: Hook<(el: HostElement) => void>
onAppearCancelled?: (el: HostElement) => void onAppearCancelled?: Hook<(el: HostElement) => void>
} }
export interface TransitionHooks< export interface TransitionHooks<
@ -69,9 +71,9 @@ export interface TransitionHooks<
delayedLeave?(): void delayedLeave?(): void
} }
export type TransitionHookCaller = ( export type TransitionHookCaller = <T extends any[] = [el: any]>(
hook: ((el: any) => void) | Array<(el: any) => void> | undefined, hook: Hook<(...args: T) => void> | undefined,
args?: any[] args?: T
) => void ) => void
export type PendingCallback = (cancelled?: boolean) => void export type PendingCallback = (cancelled?: boolean) => void
@ -331,6 +333,19 @@ export function resolveTransitionHooks(
) )
} }
const callAsyncHook = (
hook: Hook<(el: any, done: () => void) => void>,
args: [TransitionElement, () => void]
) => {
const done = args[1]
callHook(hook, args)
if (isArray(hook)) {
if (hook.every(hook => hook.length <= 1)) done()
} else if (hook.length <= 1) {
done()
}
}
const hooks: TransitionHooks<TransitionElement> = { const hooks: TransitionHooks<TransitionElement> = {
mode, mode,
persisted, persisted,
@ -388,10 +403,7 @@ export function resolveTransitionHooks(
el._enterCb = undefined el._enterCb = undefined
}) })
if (hook) { if (hook) {
hook(el, done) callAsyncHook(hook, [el, done])
if (hook.length <= 1) {
done()
}
} else { } else {
done() done()
} }
@ -423,10 +435,7 @@ export function resolveTransitionHooks(
}) })
leavingVNodesCache[key] = vnode leavingVNodesCache[key] = vnode
if (onLeave) { if (onLeave) {
onLeave(el, done) callAsyncHook(onLeave, [el, done])
if (onLeave.length <= 1) {
done()
}
} else { } else {
done() done()
} }

View File

@ -42,6 +42,7 @@ import { setTransitionHooks } from './BaseTransition'
import { ComponentRenderContext } from '../componentPublicInstance' import { ComponentRenderContext } from '../componentPublicInstance'
import { devtoolsComponentAdded } from '../devtools' import { devtoolsComponentAdded } from '../devtools'
import { isAsyncWrapper } from '../apiAsyncComponent' import { isAsyncWrapper } from '../apiAsyncComponent'
import { isSuspense } from './Suspense'
type MatchPattern = string | RegExp | (string | RegExp)[] type MatchPattern = string | RegExp | (string | RegExp)[]
@ -323,7 +324,7 @@ const KeepAliveImpl: ComponentOptions = {
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode current = vnode
return rawVNode return isSuspense(rawVNode.type) ? rawVNode : vnode
} }
} }
} }

View File

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-globals */
import { App } from './apiCreateApp' import { App } from './apiCreateApp'
import { Fragment, Text, Comment, Static } from './vnode' import { Fragment, Text, Comment, Static } from './vnode'
import { ComponentInternalInstance } from './component' import { ComponentInternalInstance } from './component'
@ -53,7 +54,6 @@ export function setDevtoolsHook(hook: DevtoolsHook, target: any) {
// handle late devtools injection - only do this if we are in an actual // handle late devtools injection - only do this if we are in an actual
// browser environment to avoid the timer handle stalling test runner exit // browser environment to avoid the timer handle stalling test runner exit
// (#4815) // (#4815)
// eslint-disable-next-line no-restricted-globals
typeof window !== 'undefined' && typeof window !== 'undefined' &&
// some envs mock window but not fully // some envs mock window but not fully
window.HTMLElement && window.HTMLElement &&

View File

@ -67,7 +67,6 @@ export function renderList(
} else if (typeof source === 'number') { } else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) { if (__DEV__ && !Number.isInteger(source)) {
warn(`The v-for range expect an integer value but got ${source}.`) warn(`The v-for range expect an integer value but got ${source}.`)
return []
} }
ret = new Array(source) ret = new Array(source)
for (let i = 0; i < source; i++) { for (let i = 0; i < source; i++) {

View File

@ -1,3 +1,4 @@
import { hasChanged } from '@vue/shared'
import { currentBlock, isBlockTreeEnabled, VNode } from '../vnode' import { currentBlock, isBlockTreeEnabled, VNode } from '../vnode'
export function withMemo( export function withMemo(
@ -22,8 +23,9 @@ export function isMemoSame(cached: VNode, memo: any[]) {
if (prev.length != memo.length) { if (prev.length != memo.length) {
return false return false
} }
for (let i = 0; i < prev.length; i++) { for (let i = 0; i < prev.length; i++) {
if (prev[i] !== memo[i]) { if (hasChanged(prev[i], memo[i])) {
return false return false
} }
} }

View File

@ -97,10 +97,15 @@ export function createHydrationFunctions(
isFragmentStart isFragmentStart
) )
const { type, ref, shapeFlag } = vnode const { type, ref, shapeFlag, patchFlag } = vnode
const domType = node.nodeType const domType = node.nodeType
vnode.el = node vnode.el = node
if (patchFlag === PatchFlags.BAIL) {
optimized = false
vnode.dynamicChildren = null
}
let nextNode: Node | null = null let nextNode: Node | null = null
switch (type) { switch (type) {
case Text: case Text:

View File

@ -1098,6 +1098,8 @@ function baseCreateRenderer(
if ( if (
patchFlag > 0 && patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT && patchFlag & PatchFlags.STABLE_FRAGMENT &&
// #5523 dev root fragment may inherit directives so always force update
!(__DEV__ && patchFlag & PatchFlags.DEV_ROOT_FRAGMENT) &&
dynamicChildren && dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result // #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children // of renderSlot() with no valid children
@ -1288,7 +1290,6 @@ function baseCreateRenderer(
} }
} else { } else {
// no update needed. just copy over properties // no update needed. just copy over properties
n2.component = n1.component
n2.el = n1.el n2.el = n1.el
instance.vnode = n2 instance.vnode = n2
} }
@ -1419,7 +1420,12 @@ function baseCreateRenderer(
// activated hook for keep-alive roots. // activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render // #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive // since the hook may be injected by a child keep-alive
if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { if (
initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
(parent &&
isAsyncWrapper(parent.vnode) &&
parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
) {
instance.a && queuePostRenderEffect(instance.a, parentSuspense) instance.a && queuePostRenderEffect(instance.a, parentSuspense)
if ( if (
__COMPAT__ && __COMPAT__ &&
@ -1544,11 +1550,11 @@ function baseCreateRenderer(
// create reactive effect for rendering // create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect( const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn, componentUpdateFn,
() => queueJob(instance.update), () => queueJob(update),
instance.scope // track it in component's effect scope instance.scope // track it in component's effect scope
)) ))
const update = (instance.update = effect.run.bind(effect) as SchedulerJob) const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid update.id = instance.uid
// allowRecurse // allowRecurse
// #1801, #2043 component render effects should allow recursive updates // #1801, #2043 component render effects should allow recursive updates
@ -1561,7 +1567,6 @@ function baseCreateRenderer(
effect.onTrigger = instance.rtg effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e) ? e => invokeArrayFns(instance.rtg!, e)
: void 0 : void 0
// @ts-ignore (for scheduler)
update.ownerInstance = instance update.ownerInstance = instance
} }

View File

@ -510,6 +510,14 @@ function _createVNode(
if (children) { if (children) {
normalizeChildren(cloned, children) normalizeChildren(cloned, children)
} }
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag |= PatchFlags.BAIL
return cloned return cloned
} }

View File

@ -15,8 +15,7 @@ describe('createApp for dom', () => {
// #4398 // #4398
test('should not mutate original root component options object', () => { test('should not mutate original root component options object', () => {
const originalObj = {
const originalObj = {
data() { data() {
return { return {
counter: 0 counter: 0
@ -28,17 +27,16 @@ describe('createApp for dom', () => {
expect(msg).toMatch(`Component is missing template or render function`) expect(msg).toMatch(`Component is missing template or render function`)
}) })
const Root = { ...originalObj} const Root = { ...originalObj }
const app = createApp(Root) const app = createApp(Root)
app.config.warnHandler = handler app.config.warnHandler = handler
app.mount(document.createElement('div')) app.mount(document.createElement('div'))
// ensure mount is based on a copy of Root object rather than Root object itself // ensure mount is based on a copy of Root object rather than Root object itself
expect(app._component).not.toBe(Root) expect(app._component).not.toBe(Root)
// ensure no mutation happened to Root object // ensure no mutation happened to Root object
expect(originalObj).toMatchObject(Root) expect(originalObj).toMatchObject(Root)
}) })
}) })

View File

@ -201,7 +201,7 @@ describe('vModel', () => {
it('should support modifiers', async () => { it('should support modifiers', async () => {
const component = defineComponent({ const component = defineComponent({
data() { data() {
return { number: null, trim: null, lazy: null } return { number: null, trim: null, lazy: null, trimNumber: null }
}, },
render() { render() {
return [ return [
@ -229,6 +229,19 @@ describe('vModel', () => {
trim: true trim: true
} }
), ),
withVModel(
h('input', {
class: 'trim-number',
'onUpdate:modelValue': (val: any) => {
this.trimNumber = val
}
}),
this.trimNumber,
{
trim: true,
number: true
}
),
withVModel( withVModel(
h('input', { h('input', {
class: 'lazy', class: 'lazy',
@ -248,6 +261,7 @@ describe('vModel', () => {
const number = root.querySelector('.number') const number = root.querySelector('.number')
const trim = root.querySelector('.trim') const trim = root.querySelector('.trim')
const trimNumber = root.querySelector('.trim-number')
const lazy = root.querySelector('.lazy') const lazy = root.querySelector('.lazy')
const data = root._vnode.component.data const data = root._vnode.component.data
@ -261,6 +275,16 @@ describe('vModel', () => {
await nextTick() await nextTick()
expect(data.trim).toEqual('hello, world') expect(data.trim).toEqual('hello, world')
trimNumber.value = ' 1 '
triggerEvent('input', trimNumber)
await nextTick()
expect(data.trimNumber).toEqual(1)
trimNumber.value = ' +01.2 '
triggerEvent('input', trimNumber)
await nextTick()
expect(data.trimNumber).toEqual(1.2)
lazy.value = 'foo' lazy.value = 'foo'
triggerEvent('change', lazy) triggerEvent('change', lazy)
await nextTick() await nextTick()
@ -1015,7 +1039,7 @@ describe('vModel', () => {
bar.selected = false bar.selected = false
data.value = new Set([{ foo: 1 }, { bar: 1 }]) data.value = new Set([{ foo: 1 }, { bar: 1 }])
await nextTick() await nextTick()
// whithout looseEqual, here is different from Array // without looseEqual, here is different from Array
expect(foo.selected).toEqual(false) expect(foo.selected).toEqual(false)
expect(bar.selected).toEqual(false) expect(bar.selected).toEqual(false)
}) })

View File

@ -205,7 +205,7 @@ describe('runtime-dom: props patching', () => {
test('form attribute', () => { test('form attribute', () => {
const el = document.createElement('input') const el = document.createElement('input')
patchProp(el, 'form', null, 'foo') patchProp(el, 'form', null, 'foo')
// non existant element // non existent element
expect(el.form).toBe(null) expect(el.form).toBe(null)
expect(el.getAttribute('form')).toBe('foo') expect(el.getAttribute('form')).toBe('foo')
// remove attribute // remove attribute

View File

@ -122,13 +122,13 @@ export function defineCustomElement(options: {
export function defineCustomElement( export function defineCustomElement(
options: any, options: any,
hydate?: RootHydrateFunction hydrate?: RootHydrateFunction
): VueElementConstructor { ): VueElementConstructor {
const Comp = defineComponent(options as any) const Comp = defineComponent(options as any)
class VueCustomElement extends VueElement { class VueCustomElement extends VueElement {
static def = Comp static def = Comp
constructor(initialProps?: Record<string, any>) { constructor(initialProps?: Record<string, any>) {
super(Comp, initialProps, hydate) super(Comp, initialProps, hydrate)
} }
} }

View File

@ -174,7 +174,10 @@ export function resolveTransitionProps(
done && done() done && done()
} }
let isLeaving = false
const finishLeave = (el: Element, done?: () => void) => { const finishLeave = (el: Element, done?: () => void) => {
isLeaving = false
removeTransitionClass(el, leaveFromClass)
removeTransitionClass(el, leaveToClass) removeTransitionClass(el, leaveToClass)
removeTransitionClass(el, leaveActiveClass) removeTransitionClass(el, leaveActiveClass)
done && done() done && done()
@ -221,6 +224,7 @@ export function resolveTransitionProps(
onEnter: makeEnterHook(false), onEnter: makeEnterHook(false),
onAppear: makeEnterHook(true), onAppear: makeEnterHook(true),
onLeave(el, done) { onLeave(el, done) {
isLeaving = true
const resolve = () => finishLeave(el, done) const resolve = () => finishLeave(el, done)
addTransitionClass(el, leaveFromClass) addTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) { if (__COMPAT__ && legacyClassEnabled) {
@ -230,6 +234,10 @@ export function resolveTransitionProps(
forceReflow() forceReflow()
addTransitionClass(el, leaveActiveClass) addTransitionClass(el, leaveActiveClass)
nextFrame(() => { nextFrame(() => {
if (!isLeaving) {
// cancelled
return
}
removeTransitionClass(el, leaveFromClass) removeTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) { if (__COMPAT__ && legacyClassEnabled) {
removeTransitionClass(el, legacyLeaveFromClass) removeTransitionClass(el, legacyLeaveFromClass)

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