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'
},
rules: {
'no-debugger': 'error',
'no-unused-vars': [
'error',
// 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
'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals],
// 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': [
'error',
'ObjectExpression > SpreadElement',
'ObjectPattern > RestElement',
'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
contact_links:
- name: Create new issue
url: https://new-issue.vuejs.org/?repo=vuejs/core
about: Please use the following link to create a new issue.
- name: Discord Chat
url: https://chat.vuejs.org
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
url: https://www.patreon.com/evanyou
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
- 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:
@ -40,7 +40,7 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
## Development Setup
You will need [Node.js](https://nodejs.org) **version 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.

View File

@ -7,7 +7,7 @@ on:
branches:
- main
jobs:
test:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -15,7 +15,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v2.0.1
with:
version: 6.15.1
version: 7.0.1
- name: Set node version to 16
uses: actions/setup-node@v2
@ -26,9 +26,9 @@ jobs:
- run: pnpm install
- name: Run unit tests
run: pnpm run test
run: pnpm run test-unit
test-dts:
e2e-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -36,7 +36,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v2.0.1
with:
version: 6.15.1
version: 7.0.1
- name: Set node version to 16
uses: actions/setup-node@v2
@ -46,6 +46,30 @@ jobs:
- 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
run: pnpm run test-dts
@ -59,7 +83,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v2.0.1
with:
version: 6.15.1
version: 7.0.1
- name: Set node version to 16
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",
"lint": "eslint --ext .ts packages/*/src/**.ts",
"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-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",
@ -18,7 +18,7 @@
"release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"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": "serve",
"open": "open http://localhost:5000/packages/template-explorer/local.html",
@ -58,7 +58,7 @@
"@types/jest": "^27.0.1",
"@types/node": "^16.4.7",
"@types/puppeteer": "^5.0.0",
"@typescript-eslint/parser": "^4.1.1",
"@typescript-eslint/parser": "^5.23.0",
"@vue/reactivity": "workspace:*",
"@vue/runtime-core": "workspace:*",
"@vue/runtime-dom": "workspace:*",
@ -89,9 +89,9 @@
"serve": "^12.0.0",
"todomvc-app-css": "^2.3.0",
"ts-jest": "^27.0.5",
"tslib": "^2.3.1",
"typescript": "^4.2.2",
"vite": "^2.9.0",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vite": "^2.9.8",
"vue": "workspace:*",
"yorkie": "^2.0.0"
}

View File

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

View File

@ -1037,7 +1037,7 @@ describe('compiler: parse', () => {
offset: 0
}
},
ns: 0,
ns: Namespaces.HTML,
props: [
{
loc: {
@ -1054,7 +1054,7 @@ describe('compiler: parse', () => {
}
},
name: 'class',
type: 6,
type: NodeTypes.ATTRIBUTE,
value: {
content: 'c',
loc: {
@ -1070,13 +1070,13 @@ describe('compiler: parse', () => {
offset: 11
}
},
type: 2
type: NodeTypes.TEXT
}
}
],
tag: 'div',
tagType: 0,
type: 1
tagType: ElementTypes.ELEMENT,
type: NodeTypes.ELEMENT
})
})
@ -2023,7 +2023,7 @@ foo
isPreTag: tag => tag === 'pre'
})
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`)
})

View File

@ -111,7 +111,7 @@ return function render(_ctx, _cache) {
? (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }))
: orNot
? (_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`)
})
test('resolve implcitly self-referencing component', () => {
test('resolve implicitly self-referencing component', () => {
const { root } = parseWithElementTransform(`<Example/>`, {
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`, () => {
const { node, root } = parseWithElementTransform(
`<div class="foo" :class="{ bar: isBar }" />`,

View File

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

View File

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

View File

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

View File

@ -97,6 +97,10 @@ export const enum BindingTypes {
* template expressions.
*/
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.
*/

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

View File

@ -24,7 +24,13 @@ import {
walkIdentifiers
} from '../babelUtils'
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 {
Node,
@ -122,7 +128,11 @@ export function processExpression(
const isDestructureAssignment =
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
} else if (type === BindingTypes.SETUP_REF) {
return `${raw}.value`
@ -181,17 +191,17 @@ export function processExpression(
} else if (type === BindingTypes.PROPS) {
// use __props which is generated by compileScript so in ts mode
// it gets correct type
return `__props.${raw}`
return genPropsAccessExp(raw)
} else if (type === BindingTypes.PROPS_ALIASED) {
// prop with a different local alias (from defineProps() destructure)
return `__props.${bindingMetadata.__propsAliases![raw]}`
return genPropsAccessExp(bindingMetadata.__propsAliases![raw])
}
} else {
if (type && type.startsWith('setup')) {
// setup bindings in non-inline mode
return `$setup.${raw}`
} else if (type === BindingTypes.PROPS_ALIASED) {
return `$props.${bindingMetadata.__propsAliases![raw]}`
return `$props['${bindingMetadata.__propsAliases![raw]}']`
} else if (type) {
return `$${type}.${raw}`
}

View File

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

View File

@ -32,3 +32,23 @@ return function render(_ctx, _cache) {
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 { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow'
import { warnTransitionChildren } from './transforms/warnTransitionChildren'
import { transformTransition } from './transforms/Transition'
import { stringifyStatic } from './transforms/stringifyStatic'
import { ignoreSideEffectTags } from './transforms/ignoreSideEffectTags'
import { extend } from '@vue/shared'
@ -25,7 +25,7 @@ export { parserOptions }
export const DOMNodeTransforms: NodeTransform[] = [
transformStyle,
...(__DEV__ ? [warnTransitionChildren] : [])
...(__DEV__ ? [transformTransition] : [])
]
export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {

View File

@ -8,7 +8,7 @@ import {
import { TRANSITION } from '../runtimeHelpers'
import { createDOMCompilerError, DOMErrorCodes } from '../errors'
export const warnTransitionChildren: NodeTransform = (node, context) => {
export const transformTransition: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT
@ -16,7 +16,12 @@ export const warnTransitionChildren: NodeTransform = (node, context) => {
const component = context.isBuiltInComponent(node.tag)
if (component === TRANSITION) {
return () => {
if (node.children.length && hasMultipleChildren(node)) {
if (!node.children.length) {
return
}
// warn multiple transition children
if (hasMultipleChildren(node)) {
context.onError(
createDOMCompilerError(
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
): string {
let res = `<${node.tag}`
let innerHTML = ''
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
@ -286,28 +287,38 @@ function stringifyElement(
if (p.value) {
res += `="${escapeHtml(p.value.content)}"`
}
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
const exp = p.exp as SimpleExpressionNode
if (exp.content[0] === '_') {
// internally generated string constant references
// e.g. imported URL strings via compiler-sfc transformAssetUrl plugin
res += ` ${(p.arg as SimpleExpressionNode).content}="__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))
} else if (p.type === NodeTypes.DIRECTIVE) {
if (p.name === 'bind') {
const exp = p.exp as SimpleExpressionNode
if (exp.content[0] === '_') {
// internally generated string constant references
// e.g. imported URL strings via compiler-sfc transformAssetUrl plugin
res += ` ${
(p.arg as SimpleExpressionNode).content
}="__VUE_EXP_START__${exp.content}__VUE_EXP_END__"`
continue
}
res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml(
evaluated
)}"`
// 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(
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 += `>`
for (let i = 0; i < node.children.length; i++) {
res += stringifyNode(node.children[i], context)
if (innerHTML) {
res += innerHTML
} else {
for (let i = 0; i < node.children.length; i++) {
res += stringifyNode(node.children[i], context)
}
}
if (!isVoidTag(node.tag)) {
res += `</${node.tag}>`
@ -330,7 +345,7 @@ function stringifyElement(
// here, e.g. `{{ 1 }}` or `{{ 'foo' }}`
// 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.
// (see compiler-core/src/transformExpressions)
// (see compiler-core/src/transforms/transformExpression)
function evaluateConstant(exp: ExpressionNode): string {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
return new Function(`return ${exp.content}`)()

View File

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

View File

@ -1,5 +1,63 @@
// 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`] = `
"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`] = `
"export const n = 1
const __default__ = {}
import { x } from './x'
import { x } from './x'
export default /*#__PURE__*/Object.assign(__default__, {
setup(__props, { expose }) {
@ -42,7 +101,8 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage script
const __default__ = {
name: \\"test\\"
}
import { x } from './x'
import { x } from './x'
export default /*#__PURE__*/_defineComponent({
...__default__,
@ -63,6 +123,7 @@ exports[`SFC compile <script setup> <script> and <script setup> co-usage script
const __default__ = def
import { x } from './x'
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`] = `
"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`] = `
"import { defineComponent as _defineComponent } from 'vue'
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 Foo, { bar } from './Foo.vue'
import other from './util'
import * as tree from './tree'
export default {
setup(__props) {
@ -735,7 +981,8 @@ return (_ctx, _cache) => {
]),
_: 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 */))
}
}
@ -1006,7 +1253,8 @@ exports[`SFC compile <script setup> should expose top level declarations 1`] = `
const bb = 2
function cc() {}
class dd {}
import { x } from './x'
import { x } from './x'
export default {
setup(__props, { expose }) {

View File

@ -65,7 +65,7 @@ exports[`sfc props transform default values w/ runtime declaration 1`] = `
export default {
props: _mergeDefaults(['foo', 'bar'], {
foo: 1,
bar: () => {}
bar: () => ({})
}),
setup(__props) {
@ -83,7 +83,7 @@ exports[`sfc props transform default values w/ type declaration 1`] = `
export default /*#__PURE__*/_defineComponent({
props: {
foo: { type: Number, required: false, default: 1 },
bar: { type: Object, required: false, default: () => {} }
bar: { type: Object, required: false, default: () => ({}) }
},
setup(__props: any) {
@ -101,11 +101,11 @@ exports[`sfc props transform default values w/ type declaration, prod mode 1`] =
export default /*#__PURE__*/_defineComponent({
props: {
foo: { default: 1 },
bar: { default: () => {} },
bar: { default: () => ({}) },
baz: null,
boola: { type: Boolean },
boolb: { type: [Boolean, Number] },
func: { type: Function, default: () => () => {} }
func: { type: Function, default: () => (() => {}) }
},
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`] = `
"import { createPropsRestProxy as _createPropsRestProxy } from 'vue'

View File

@ -1,5 +1,51 @@
// 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`] = `
Object {
"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_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) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"use\\", { href: _hoisted_1 }),
_createElementVNode(\\"use\\", { href: _hoisted_1 })
_hoisted_2,
_hoisted_3
], 64 /* STABLE_FRAGMENT */))
}"
`;

View File

@ -1,5 +1,23 @@
// 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`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
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_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
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) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"\\"
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_1
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_2
}),
_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\\"
})
_hoisted_9,
_hoisted_10,
_hoisted_11,
_hoisted_12,
_hoisted_13,
_hoisted_14,
_hoisted_15,
_hoisted_16,
_hoisted_17,
_hoisted_18,
_hoisted_19,
_hoisted_20
], 64 /* STABLE_FRAGMENT */))
}"
`;
@ -71,56 +101,69 @@ export function render(_ctx, _cache) {
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\\"
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) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"\\"
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png\\"
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"/foo/logo.png 2x\\"
}),
_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\\"
})
_hoisted_1,
_hoisted_2,
_hoisted_3,
_hoisted_4,
_hoisted_5,
_hoisted_6,
_hoisted_7,
_hoisted_8,
_hoisted_9,
_hoisted_10,
_hoisted_11,
_hoisted_12
], 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_8 = _imports_1 + ', ' + _imports_1 + ' 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) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: \\"\\"
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_1
}),
_createElementVNode(\\"img\\", {
src: \\"./logo.png\\",
srcset: _hoisted_2
}),
_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\\"
})
_hoisted_10,
_hoisted_11,
_hoisted_12,
_hoisted_13,
_hoisted_14,
_hoisted_15,
_hoisted_16,
_hoisted_17,
_hoisted_18,
_hoisted_19,
_hoisted_20,
_hoisted_21
], 64 /* STABLE_FRAGMENT */))
}"
`;

View File

@ -64,11 +64,11 @@ const bar = 1
`)
// should generate working code
assertCode(content)
// should anayze bindings
// should analyze bindings
expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS,
bar: BindingTypes.SETUP_CONST,
props: BindingTypes.SETUP_CONST
props: BindingTypes.SETUP_REACTIVE_CONST
})
// should remove defineOptions import and call
@ -168,6 +168,16 @@ defineExpose({ 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', () => {
test('script first', () => {
const { content } = compile(`
@ -403,7 +413,7 @@ defineExpose({ foo: 123 })
assertCode(content)
})
// #4340 interpolations in tempalte strings
// #4340 interpolations in template strings
test('js template string interpolations', () => {
const { content } = compile(`
<script setup lang="ts">
@ -432,6 +442,23 @@ defineExpose({ foo: 123 })
expect(content).toMatch(`return { FooBaz, Last }`)
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', () => {
@ -502,6 +529,7 @@ defineExpose({ foo: 123 })
import { ref } from 'vue'
import Foo, { bar } from './Foo.vue'
import other from './util'
import * as tree from './tree'
const count = ref(0)
const constant = {}
const maybe = foo()
@ -511,6 +539,7 @@ defineExpose({ foo: 123 })
<template>
<Foo>{{ bar }}</Foo>
<div @click="fn">{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}</div>
{{ tree.foo() }}
</template>
`,
{ inlineTemplate: true }
@ -529,6 +558,8 @@ defineExpose({ foo: 123 })
expect(content).toMatch(`unref(maybe)`)
// should unref() on let bindings
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
expect(content).toMatch(`{ onClick: fn }`)
// 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 }`)
})
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', () => {
// function declaration
assertAwaitDetection(`async function foo() { await bar }`, false)
@ -1550,4 +1634,59 @@ describe('SFC analyze <script> bindings', () => {
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
expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], {
foo: 1,
bar: () => {}
bar: () => ({})
})`)
assertCode(content)
})
@ -74,7 +74,7 @@ describe('sfc props transform', () => {
// function
expect(content).toMatch(`props: {
foo: { type: Number, required: false, default: 1 },
bar: { type: Object, required: false, default: () => {} }
bar: { type: Object, required: false, default: () => ({}) }
}`)
assertCode(content)
})
@ -92,11 +92,11 @@ describe('sfc props transform', () => {
// function
expect(content).toMatch(`props: {
foo: { default: 1 },
bar: { default: () => {} },
bar: { default: () => ({}) },
baz: null,
boola: { type: Boolean },
boolb: { type: [Boolean, Number] },
func: { type: Function, default: () => () => {} }
func: { type: Function, default: () => (() => {}) }
}`)
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', () => {
const { content, bindings } = compile(`
<script setup>
@ -141,7 +163,7 @@ describe('sfc props transform', () => {
foo: BindingTypes.PROPS,
bar: 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(`_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', () => {
const result = compileWithAssetUrls(
'<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()
})

View File

@ -26,6 +26,7 @@ function compileWithSrcset(
? createSrcsetTransformWithOptions(normalizeOptions(options))
: transformSrcset
transform(ast, {
hoistStatic: true,
nodeTransforms: [srcsetTransform, transformElement],
directiveTransforms: {
bind: transformBind
@ -85,4 +86,16 @@ describe('compiler sfc: transform srcset', () => {
expect(code).toMatch(`_createStaticVNode`)
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'
export const mockId = 'xxxxxxxx'
export function compileSFCScript(
src: string,
options?: Partial<SFCScriptCompileOptions>
options?: Partial<SFCScriptCompileOptions>,
parseOptions?: SFCParseOptions
) {
const { descriptor } = parse(src)
const { descriptor } = parse(src, parseOptions)
return compileScript(descriptor, {
...options,
id: mockId

View File

@ -11,15 +11,14 @@ import {
isFunctionType,
walkIdentifiers
} from '@vue/compiler-dom'
import { SFCDescriptor, SFCScriptBlock } from './parse'
import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser'
import { DEFAULT_FILENAME, SFCDescriptor, SFCScriptBlock } from './parse'
import {
camelize,
capitalize,
generateCodeFrame,
isObject,
makeMap
} from '@vue/shared'
parse as _parse,
parseExpression,
ParserOptions,
ParserPlugin
} from '@babel/parser'
import { camelize, capitalize, generateCodeFrame, isObject,makeMap } from '@vue/shared'
import {
Node,
Declaration,
@ -188,6 +187,12 @@ export function compileScript(
const plugins: ParserPlugin[] = []
if (!isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
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 (isTS) plugins.push('typescript', 'decorators-legacy')
@ -279,6 +284,8 @@ export function compileScript(
let hasDefinePropsCall = false
let hasDefineEmitCall = false
let hasDefineExposeCall = false
let hasDefaultExportName = false
let hasDefaultExportRender = false
let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: ObjectExpression | undefined
let propsDestructureDecl: Node | undefined
@ -356,14 +363,23 @@ export function compileScript(
local: string,
imported: string | false,
isType: boolean,
isFromSetup: boolean
isFromSetup: boolean,
needTemplateUsageCheck: boolean
) {
if (source === 'vue' && imported) {
userImportAlias[imported] = local
}
let isUsedInTemplate = true
if (isTS && sfc.template && !sfc.template.src && !sfc.template.lang) {
// template usage check is only needed in non-inline mode, so we can skip
// the work if inlineTemplate is true.
let isUsedInTemplate = needTemplateUsageCheck
if (
needTemplateUsageCheck &&
isTS &&
sfc.template &&
!sfc.template.src &&
!sfc.template.lang
) {
isUsedInTemplate = isImportUsed(local, sfc)
}
@ -425,7 +441,11 @@ export function compileScript(
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') {
// default value { foo = 123 }
const { left, right } = prop.value
@ -751,7 +771,7 @@ export function compileScript(
destructured.default.end!
)
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' ||
(specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'),
false
false,
!options.inlineTemplate
)
}
} else if (node.type === 'ExportDefaultDeclaration') {
// export default
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__ = { ... }
const start = node.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
// declared before being used in the actual component definition
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)
}
}
@ -969,10 +1029,13 @@ export function compileScript(
for (let i = 0; i < node.specifiers.length; i++) {
const specifier = node.specifiers[i]
const local = specifier.local.name
const imported =
let imported =
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name
if (specifier.type === 'ImportNamespaceSpecifier') {
imported = '*'
}
const source = node.source.value
const existing = userImports[local]
if (
@ -1000,7 +1063,8 @@ export function compileScript(
node.importKind === 'type' ||
(specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'),
true
true,
!options.inlineTemplate
)
}
}
@ -1076,15 +1140,28 @@ export function compileScript(
(node.type === 'VariableDeclaration' && !node.declare) ||
node.type.endsWith('Statement')
) {
const scope: Statement[][] = [scriptSetupAst.body]
;(walk as any)(node, {
enter(child: Node, parent: Node) {
if (isFunctionType(child)) {
this.skip()
}
if (child.type === 'BlockStatement') {
scope.push(child.body)
}
if (child.type === 'AwaitExpression') {
hasAwait = true
const needsSemi = scriptSetupAst.body.some(n => {
return n.type === 'ExpressionStatement' && n.start === child.start
// if the await expression is an expression statement and
// - 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(
child,
@ -1092,6 +1169,9 @@ export function compileScript(
parent.type === 'ExpressionStatement'
)
}
},
exit(node: Node) {
if (node.type === 'BlockStatement') scope.pop()
}
})
}
@ -1161,7 +1241,7 @@ export function compileScript(
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS)
// 6. remove non-script content
if (script) {
@ -1197,7 +1277,8 @@ export function compileScript(
// props aliases
if (propsDestructureDecl) {
if (propsDestructureRestId) {
bindingMetadata[propsDestructureRestId] = BindingTypes.SETUP_CONST
bindingMetadata[propsDestructureRestId] =
BindingTypes.SETUP_REACTIVE_CONST
}
for (const key in propsDestructuredBindings) {
const { local } = propsDestructuredBindings[key]
@ -1213,7 +1294,9 @@ export function compileScript(
)) {
if (isType) continue
bindingMetadata[key] =
(imported === 'default' && source.endsWith('.vue')) || source === 'vue'
imported === '*' ||
(imported === 'default' && source.endsWith('.vue')) ||
source === 'vue'
? BindingTypes.SETUP_CONST
: BindingTypes.SETUP_MAYBE_REF
}
@ -1292,7 +1375,21 @@ export function compileScript(
// 10. generate return statement
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 (options.templateOptions && options.templateOptions.ssr) {
hasInlinedSsrRenderFn = true
@ -1350,18 +1447,6 @@ export function compileScript(
} else {
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__) {
@ -1380,6 +1465,12 @@ export function compileScript(
// 11. finalize default export
let runtimeOptions = ``
if (!hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
const match = filename.match(/([^/\\]+)\.\w+$/)
if (match) {
runtimeOptions += `\n name: '${match[1]}',`
}
}
if (hasInlinedSsrRenderFn) {
runtimeOptions += `\n __ssrInlineRender: true,`
}
@ -1509,14 +1600,18 @@ function walkDeclaration(
const userReactiveBinding = userImportAlias['reactive'] || 'reactive'
if (isCallOf(init, userReactiveBinding)) {
// 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 (
// if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it
isDefineCall ||
(isConst && canNeverBeRef(init!, userReactiveBinding))
) {
bindingType = BindingTypes.SETUP_CONST
bindingType = isCallOf(init, DEFINE_PROPS)
? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_CONST
} else if (isConst) {
if (isCallOf(init, userImportAlias['ref'] || 'ref')) {
bindingType = BindingTypes.SETUP_REF
@ -2011,14 +2106,14 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
code += `,v${capitalize(camelize(prop.name))}`
}
if (prop.exp) {
code += `,${stripStrings(
code += `,${processExp(
(prop.exp as SimpleExpressionNode).content
)}`
}
}
}
} else if (node.type === NodeTypes.INTERPOLATION) {
code += `,${stripStrings(
code += `,${processExp(
(node.content as SimpleExpressionNode).content
)}`
}
@ -2031,6 +2126,19 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
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) {
return exp
.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()
if (
(exp[0] === `'` && exp[exp.length - 1] === `'`) ||
@ -51,7 +51,7 @@ export function parseCssVars(sfc: SFCDescriptor): string[] {
// ignore v-bind() in comments /* ... */
const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '')
while ((match = cssVarRE.exec(content))) {
const variable = noramlizeExpression(match[1])
const variable = normalizeExpression(match[1])
if (!vars.includes(variable)) {
vars.push(variable)
}
@ -74,7 +74,7 @@ export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
// rewrite CSS variables
if (cssVarRE.test(decl.value)) {
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
export {
SFCParseOptions,
SFCParseResult,
SFCDescriptor,
SFCBlock,
SFCTemplateBlock,
SFCScriptBlock,
SFCStyleBlock,
SFCParseResult
SFCStyleBlock
} from './parse'
export {
TemplateCompiler,

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { compile } from '../src'
import { ssrHelpers, SSR_RENDER_SLOT_INNER } from '../src/runtimeHelpers'
describe('ssr: <slot>', () => {
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_COMPONENT = Symbol(`ssrRenderComponent`)
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_STYLE = Symbol(`ssrRenderStyle`)
export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`)
@ -24,6 +25,7 @@ export const ssrHelpers = {
[SSR_RENDER_VNODE]: `ssrRenderVNode`,
[SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
[SSR_RENDER_SLOT]: `ssrRenderSlot`,
[SSR_RENDER_SLOT_INNER]: `ssrRenderSlotInner`,
[SSR_RENDER_CLASS]: `ssrRenderClass`,
[SSR_RENDER_STYLE]: `ssrRenderStyle`,
[SSR_RENDER_ATTRS]: `ssrRenderAttrs`,

View File

@ -4,9 +4,13 @@ import {
processSlotOutlet,
createCallExpression,
SlotOutletNode,
createFunctionExpression
createFunctionExpression,
NodeTypes,
ElementTypes,
resolveComponentType,
TRANSITION
} from '@vue/compiler-dom'
import { SSR_RENDER_SLOT } from '../runtimeHelpers'
import { SSR_RENDER_SLOT, SSR_RENDER_SLOT_INNER } from '../runtimeHelpers'
import {
SSRTransformContext,
processChildrenAsStatement
@ -31,10 +35,24 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
args.push(`"${context.scopeId}-s"`)
}
node.ssrCodegenNode = createCallExpression(
context.helper(SSR_RENDER_SLOT),
args
)
let method = SSR_RENDER_SLOT
// #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),
f = _toRef(__$temp_1, 'e', 2),
h = _toRef(__$temp_1, g)
let __$temp_2 = (useSomthing(() => 1)),
let __$temp_2 = (useSomething(() => 1)),
foo = _toRef(__$temp_2, 'foo');
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', () => {
const { code, rootRefs } = transform(`
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)
`)
expect(code).toMatch(`a = _toRef(__$temp_1, 'a')`)

View File

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

View File

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

View File

@ -120,7 +120,7 @@ describe('reactivity/effect/scope', () => {
counter.num = 6
expect(dummy).toBe(7)
// nested scope should not be stoped
// nested scope should not be stopped
expect(doubled).toBe(12)
})
@ -212,7 +212,7 @@ describe('reactivity/effect/scope', () => {
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 child = parent.run(() => new EffectScope())!
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', () => {
const array = new Array(3)
const array: any[] & { x?: string } = new Array(3)
const observed = reactive(array)
const fn = jest.fn()
effect(() => {
fn(observed.length)
})
expect(fn).toHaveBeenCalledTimes(1)
// @ts-ignore
observed.x = 'x'
expect(fn).toHaveBeenCalledTimes(1)
observed[-1] = 'x'

View File

@ -68,21 +68,21 @@ describe('reactivity/readonly', () => {
`Set operation on key "Symbol(qux)" failed: target is readonly.`
).toHaveBeenWarnedLast()
// @ts-ignore
// @ts-expect-error
delete wrapped.foo
expect(wrapped.foo).toBe(1)
expect(
`Delete operation on key "foo" failed: target is readonly.`
).toHaveBeenWarnedLast()
// @ts-ignore
// @ts-expect-error
delete wrapped.bar.baz
expect(wrapped.bar.baz).toBe(2)
expect(
`Delete operation on key "baz" failed: target is readonly.`
).toHaveBeenWarnedLast()
// @ts-ignore
// @ts-expect-error
delete wrapped[qux]
expect(wrapped[qux]).toBe(3)
expect(
@ -459,7 +459,7 @@ describe('reactivity/readonly', () => {
expect(
'Set operation on key "_dirty" failed: target is readonly.'
).not.toHaveBeenWarned()
// @ts-expect-error - non-existant property
// @ts-expect-error - non-existent property
rC.randomProperty = true
expect(
@ -476,7 +476,7 @@ describe('reactivity/readonly', () => {
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 ror = readonly(r)
const obj = reactive({ ror })

View File

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

View File

@ -37,6 +37,10 @@ const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
const builtInSymbols = new Set(
/*#__PURE__*/
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])
.filter(isSymbol)
)
@ -125,9 +129,8 @@ function createGetter(isReadonly = false, shallow = false) {
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {

View File

@ -26,10 +26,12 @@ function get(
target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (key !== rawKey) {
!isReadonly && track(rawTarget, TrackOpTypes.GET, key)
if (!isReadonly) {
if (key !== rawKey) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
}
!isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
const { has } = getProto(rawTarget)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
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 rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (key !== rawKey) {
!isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
if (!isReadonly) {
if (key !== rawKey) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
}
!isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)

View File

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

View File

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

View File

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

View File

@ -4,9 +4,11 @@ import {
Component,
ref,
nextTick,
Suspense
Suspense,
KeepAlive
} from '../src'
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))
@ -799,4 +801,44 @@ describe('api: defineAsyncComponent', () => {
expect(vnodeHooks.onVnodeBeforeUnmount).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')
createApp(Comp).mount(root1)
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
const root2 = nodeOps.createElement('div')

View File

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

View File

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

View File

@ -214,7 +214,7 @@ describe('api: watch', () => {
})
it('warn invalid watch source', () => {
// @ts-ignore
// @ts-expect-error
watch(1, () => {})
expect(`Invalid watch source`).toHaveBeenWarned()
})
@ -748,7 +748,7 @@ describe('api: watch', () => {
() => {
dummy = count.value
},
// @ts-ignore
// @ts-expect-error
{ immediate: false }
)
expect(dummy).toBe(0)
@ -767,7 +767,7 @@ describe('api: watch', () => {
spy()
return arr
},
// @ts-ignore
// @ts-expect-error
{ deep: true }
)
expect(spy).toHaveBeenCalledTimes(1)
@ -818,7 +818,7 @@ describe('api: watch', () => {
const onTrigger = jest.fn((e: DebuggerEvent) => {
events.push(e)
})
const obj = reactive({ foo: 1 })
const obj = reactive<{ foo?: number }>({ foo: 1 })
watchEffect(
() => {
dummy = obj.foo
@ -828,7 +828,7 @@ describe('api: watch', () => {
await nextTick()
expect(dummy).toBe(1)
obj.foo++
obj.foo!++
await nextTick()
expect(dummy).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1)
@ -839,7 +839,6 @@ describe('api: watch', () => {
newValue: 2
})
// @ts-ignore
delete obj.foo
await nextTick()
expect(dummy).toBeUndefined()
@ -893,6 +892,21 @@ describe('api: watch', () => {
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
test('watchEffect should not recursively trigger itself', async () => {
const spy = jest.fn()

View File

@ -153,7 +153,7 @@ describe('component: emit', () => {
emits: ['foo'],
render() {},
created() {
// @ts-ignore
// @ts-expect-error
this.$emit('bar')
}
})
@ -170,7 +170,7 @@ describe('component: emit', () => {
},
render() {},
created() {
// @ts-ignore
// @ts-expect-error
this.$emit('bar')
}
})
@ -186,7 +186,7 @@ describe('component: emit', () => {
emits: [],
render() {},
created() {
// @ts-ignore
// @ts-expect-error
this.$emit('foo')
}
})
@ -355,6 +355,37 @@ describe('component: emit', () => {
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', () => {
const options = {
click: null,

View File

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

View File

@ -18,7 +18,12 @@ import {
defineAsyncComponent,
Component,
createApp,
onActivated
onActivated,
onUnmounted,
onMounted,
reactive,
shallowRef,
onDeactivated
} from '@vue/runtime-test'
import { KeepAliveProps } from '../../src/components/KeepAlive'
@ -903,4 +908,73 @@ describe('KeepAlive', () => {
await nextTick()
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()
expect(deps.length).toBe(2)
// switch before two resovles
// switch before two resolves
view.value = Three
await nextTick()
expect(deps.length).toBe(3)
@ -1098,7 +1098,7 @@ describe('Suspense', () => {
await nextTick()
expect(deps.length).toBe(2)
// switch back before two resovles
// switch back before two resolves
view.value = One
await nextTick()
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
// 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', () => {
const err = new Error('foo')
const fn = jest.fn()

View File

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

View File

@ -1,5 +1,5 @@
// 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'
describe('v-memo', () => {
@ -210,4 +210,17 @@ describe('v-memo', () => {
// should update
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
// root cause: fragment inside a compiled slot passed to component which
// 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.
test('fragments inside programmatically invoked compiled slot should de-opt properly', async () => {
const Parent: FunctionalComponent = (_, { slots }) => slots.default!()

View File

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

View File

@ -541,18 +541,6 @@ describe('scheduler', () => {
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
// e.g. app.mount inside app.mount
test('flushPostFlushCbs', async () => {

View File

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

View File

@ -284,6 +284,14 @@ export function createAppAPI<HostElement>(
isSVG?: boolean
): any {
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(
rootComponent as ConcreteComponent,
rootProps
@ -345,9 +353,8 @@ export function createAppAPI<HostElement>(
`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] = value
context.provides[key as string | symbol] = value
return app
}

View File

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

View File

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

View File

@ -440,6 +440,15 @@ export interface ComponentInternalInstance {
* @internal
*/
[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()
@ -477,7 +486,7 @@ export function createComponentInstance(
accessCache: null!,
renderCache: [],
// local resovled assets
// local resolved assets
components: null,
directives: null,

View File

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

View File

@ -117,8 +117,9 @@ export interface ComponentOptionsBase<
Extends extends ComponentOptionsMixin,
E extends EmitsOptions,
EE extends string = string,
Defaults = {}
> extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
Defaults = {},
Provide extends ComponentProvideOptions = ComponentProvideOptions
> extends LegacyOptions<Props, D, C, M, Mixin, Extends, Provide>,
ComponentInternalOptions,
ComponentCustomOptions {
setup?: (
@ -224,6 +225,7 @@ export type ComponentOptionsWithoutProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
PE = Props & EmitsToProps<E>
> = ComponentOptionsBase<
PE,
@ -235,7 +237,8 @@ export type ComponentOptionsWithoutProps<
Extends,
E,
EE,
{}
{},
Provide
> & {
props?: undefined
} & ThisType<
@ -252,6 +255,7 @@ export type ComponentOptionsWithArrayProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps<E>
> = ComponentOptionsBase<
Props,
@ -263,7 +267,8 @@ export type ComponentOptionsWithArrayProps<
Extends,
E,
EE,
{}
{},
Provide
> & {
props: PropNames[]
} & ThisType<
@ -289,6 +294,7 @@ export type ComponentOptionsWithObjectProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
Props = Readonly<ExtractPropTypes<PropsOptions>> & EmitsToProps<E>,
Defaults = ExtractDefaultPropTypes<PropsOptions>
> = ComponentOptionsBase<
@ -301,7 +307,8 @@ export type ComponentOptionsWithObjectProps<
Extends,
E,
EE,
Defaults
Defaults,
Provide
> & {
props: PropsOptions & ThisType<void>
} & ThisType<
@ -384,6 +391,10 @@ type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[]
type ComponentWatchOptions = Record<string, ComponentWatchOptionItem>
export type ComponentProvideOptions = ObjectProvideOptions | Function
type ObjectProvideOptions = Record<string | symbol, unknown>
type ComponentInjectOptions = string[] | ObjectInjectOptions
type ObjectInjectOptions = Record<
@ -397,7 +408,8 @@ interface LegacyOptions<
C extends ComputedOptions,
M extends MethodOptions,
Mixin extends ComponentOptionsMixin,
Extends extends ComponentOptionsMixin
Extends extends ComponentOptionsMixin,
Provide extends ComponentProvideOptions = ComponentProvideOptions
> {
compatConfig?: CompatConfig
@ -431,7 +443,7 @@ interface LegacyOptions<
computed?: C
methods?: M
watch?: ComponentWatchOptions
provide?: Data | Function
provide?: Provide
inject?: ComponentInjectOptions
// assets
@ -471,8 +483,8 @@ interface LegacyOptions<
*
* type-only, used to assist Mixin's type inference,
* typescript will try to simplify the inferred `Mixin` type,
* with the `__differenciator`, typescript won't be able to combine different mixins,
* because the `__differenciator` will be different
* with the `__differentiator`, typescript won't be able to combine different mixins,
* because the `__differentiator` will be different
*/
__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
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 =
@ -225,7 +226,7 @@ export function updateProps(
for (let i = 0; i < propsToUpdate.length; i++) {
let key = propsToUpdate[i]
// skip if the prop key is a declared emit event listener
if (isEmitListener(instance.emitsOptions, key)){
if (isEmitListener(instance.emitsOptions, key)) {
continue
}
// PROPS flag guarantees rawProps to be non-null

View File

@ -34,7 +34,8 @@ import {
OptionTypesKeys,
resolveMergedOptions,
shouldCacheAccess,
MergedComponentOptionsOverride
MergedComponentOptionsOverride,
ComponentProvideOptions
} from './componentOptions'
import { EmitsOptions, EmitFn } from './componentEmits'
import { Slots } from './componentSlots'
@ -150,7 +151,8 @@ export type CreateComponentPublicInstance<
PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
EnsureNonVoid<M>,
PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
EnsureNonVoid<Defaults>
EnsureNonVoid<Defaults>,
Provide extends ComponentProvideOptions = ComponentProvideOptions
> = ComponentPublicInstance<
PublicP,
PublicB,
@ -161,7 +163,19 @@ export type CreateComponentPublicInstance<
PublicProps,
PublicDefaults,
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
@ -238,8 +252,8 @@ export const publicPropertiesMap: PublicPropertiesMap =
$root: i => getPublicInstance(i.root),
$emit: i => i.emit,
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
$forceUpdate: i => () => queueJob(i.update),
$nextTick: i => nextTick.bind(i.proxy!),
$forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
} as PublicPropertiesMap)

View File

@ -215,6 +215,8 @@ export function renderComponentRoot(
`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
}
// inherit transition data

View File

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

View File

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

View File

@ -42,6 +42,7 @@ import { setTransitionHooks } from './BaseTransition'
import { ComponentRenderContext } from '../componentPublicInstance'
import { devtoolsComponentAdded } from '../devtools'
import { isAsyncWrapper } from '../apiAsyncComponent'
import { isSuspense } from './Suspense'
type MatchPattern = string | RegExp | (string | RegExp)[]
@ -323,7 +324,7 @@ const KeepAliveImpl: ComponentOptions = {
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
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 { Fragment, Text, Comment, Static } from './vnode'
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
// browser environment to avoid the timer handle stalling test runner exit
// (#4815)
// eslint-disable-next-line no-restricted-globals
typeof window !== 'undefined' &&
// some envs mock window but not fully
window.HTMLElement &&

View File

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

View File

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

View File

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

View File

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

View File

@ -510,6 +510,14 @@ function _createVNode(
if (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
}

View File

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

View File

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

View File

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

View File

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

View File

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

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