mirror of https://github.com/vuejs/core.git
chore: Merge branch 'main' into minor
This commit is contained in:
commit
1d8727ec97
|
@ -6,7 +6,7 @@
|
|||
|
||||
Messages must be matched by the following regex:
|
||||
|
||||
``` js
|
||||
```regexp
|
||||
/^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip)(\(.+\))?: .{1,50}/
|
||||
```
|
||||
|
||||
|
|
|
@ -41,13 +41,3 @@ jobs:
|
|||
with:
|
||||
name: size-data
|
||||
path: temp/size
|
||||
|
||||
- name: Save PR number
|
||||
if: ${{github.event_name == 'pull_request'}}
|
||||
run: echo ${{ github.event.number }} > ./pr.txt
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{github.event_name == 'pull_request'}}
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr.txt
|
||||
|
|
|
@ -35,18 +35,6 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download PR number
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
name: pr-number
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Read PR Number
|
||||
id: pr-number
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ./pr.txt
|
||||
|
||||
- name: Download Size Data
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
|
@ -77,7 +65,6 @@ jobs:
|
|||
uses: actions-cool/maintain-one-comment@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
number: ${{ steps.pr-number.outputs.content }}
|
||||
body: |
|
||||
${{ steps.size-report.outputs.content }}
|
||||
<!-- VUE_CORE_SIZE -->
|
||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,3 +1,16 @@
|
|||
## [3.4.27](https://github.com/vuejs/core/compare/v3.4.26...v3.4.27) (2024-05-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compat:** include legacy scoped slots ([#10868](https://github.com/vuejs/core/issues/10868)) ([8366126](https://github.com/vuejs/core/commit/83661264a4ced3cb2ff6800904a86dd9e82bbfe2)), closes [#8869](https://github.com/vuejs/core/issues/8869)
|
||||
* **compiler-core:** add support for arrow aysnc function with unbracketed ([#5789](https://github.com/vuejs/core/issues/5789)) ([ca7d421](https://github.com/vuejs/core/commit/ca7d421e8775f6813f8943d32ab485e0c542f98b)), closes [#5788](https://github.com/vuejs/core/issues/5788)
|
||||
* **compiler-dom:** restrict createStaticVNode usage with option elements ([#10846](https://github.com/vuejs/core/issues/10846)) ([0e3d617](https://github.com/vuejs/core/commit/0e3d6178b02d0386d779720ae2cc4eac1d1ec990)), closes [#6568](https://github.com/vuejs/core/issues/6568) [#7434](https://github.com/vuejs/core/issues/7434)
|
||||
* **compiler-sfc:** handle keyof operator ([#10874](https://github.com/vuejs/core/issues/10874)) ([10d34a5](https://github.com/vuejs/core/commit/10d34a5624775f20437ccad074a97270ef74c3fb)), closes [#10871](https://github.com/vuejs/core/issues/10871)
|
||||
* **hydration:** handle edge case of style mismatch without style attribute ([f2c1412](https://github.com/vuejs/core/commit/f2c1412e46a8fad3e13403bfa78335c4f704f21c)), closes [#10786](https://github.com/vuejs/core/issues/10786)
|
||||
|
||||
|
||||
|
||||
# [3.5.0-alpha.2](https://github.com/vuejs/core/compare/v3.4.26...v3.5.0-alpha.2) (2024-05-04)
|
||||
|
||||
|
||||
|
|
|
@ -45,6 +45,12 @@ export default tseslint.config(
|
|||
message:
|
||||
'Our output target is ES2016, so async/await syntax should be avoided.',
|
||||
},
|
||||
{
|
||||
selector: 'ChainExpression',
|
||||
message:
|
||||
'Our output target is ES2016, and optional chaining results in ' +
|
||||
'verbose helpers and should be avoided.',
|
||||
},
|
||||
],
|
||||
'sort-imports': ['error', { ignoreDeclarationSort: true }],
|
||||
|
||||
|
@ -134,7 +140,7 @@ export default tseslint.config(
|
|||
{
|
||||
files: [
|
||||
'eslint.config.js',
|
||||
'rollup.config.js',
|
||||
'rollup*.config.js',
|
||||
'scripts/**',
|
||||
'./*.{js,ts}',
|
||||
'packages/*/*.js',
|
||||
|
|
40
package.json
40
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"private": true,
|
||||
"version": "3.5.0-alpha.2",
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"packageManager": "pnpm@9.1.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
|
@ -59,58 +59,58 @@
|
|||
"node": ">=18.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/types": "^7.24.0",
|
||||
"@babel/parser": "^7.24.6",
|
||||
"@babel/types": "^7.24.6",
|
||||
"@codspeed/vitest-plugin": "^3.1.0",
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-commonjs": "^25.0.8",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.4",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/hash-sum": "^1.0.2",
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vitest/coverage-istanbul": "^1.5.2",
|
||||
"@vue/consolidate": "1.0.0",
|
||||
"conventional-changelog-cli": "^4.1.0",
|
||||
"enquirer": "^2.4.1",
|
||||
"esbuild": "^0.20.2",
|
||||
"esbuild": "^0.21.4",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"eslint": "^9.1.1",
|
||||
"eslint-plugin-import-x": "^0.5.0",
|
||||
"eslint": "^9.3.0",
|
||||
"eslint-plugin-import-x": "^0.5.1",
|
||||
"eslint-plugin-vitest": "^0.5.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"execa": "^8.0.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"lint-staged": "^15.2.5",
|
||||
"lodash": "^4.17.21",
|
||||
"magic-string": "^0.30.10",
|
||||
"markdown-table": "^3.0.3",
|
||||
"marked": "^12.0.2",
|
||||
"minimist": "^1.2.8",
|
||||
"npm-run-all2": "^6.1.2",
|
||||
"picocolors": "^1.0.0",
|
||||
"npm-run-all2": "^6.2.0",
|
||||
"picocolors": "^1.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"pug": "^3.0.2",
|
||||
"pug": "^3.0.3",
|
||||
"puppeteer": "~22.7.1",
|
||||
"rimraf": "^5.0.5",
|
||||
"rollup": "^4.17.1",
|
||||
"rollup-plugin-dts": "^6.1.0",
|
||||
"rimraf": "^5.0.7",
|
||||
"rollup": "^4.18.0",
|
||||
"rollup-plugin-dts": "^6.1.1",
|
||||
"rollup-plugin-esbuild": "^6.1.1",
|
||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||
"semver": "^7.6.0",
|
||||
"semver": "^7.6.2",
|
||||
"serve": "^14.2.3",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"terser": "^5.30.4",
|
||||
"terser": "^5.31.0",
|
||||
"todomvc-app-css": "^2.4.3",
|
||||
"tslib": "^2.6.2",
|
||||
"tsx": "^4.7.3",
|
||||
"tsx": "^4.11.0",
|
||||
"typescript": "~5.4.5",
|
||||
"typescript-eslint": "^7.7.1",
|
||||
"vite": "^5.2.10",
|
||||
"typescript-eslint": "^7.10.0",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^1.5.2"
|
||||
},
|
||||
"pnpm": {
|
||||
|
|
|
@ -1284,6 +1284,18 @@ describe('compiler: element transform', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('<math> should be forced into blocks', () => {
|
||||
const ast = parse(`<div><math/></div>`)
|
||||
transform(ast, {
|
||||
nodeTransforms: [transformElement],
|
||||
})
|
||||
expect((ast as any).children[0].children[0].codegenNode).toMatchObject({
|
||||
type: NodeTypes.VNODE_CALL,
|
||||
tag: `"math"`,
|
||||
isBlock: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('force block for runtime custom directive w/ children', () => {
|
||||
const { node } = parseWithElementTransform(`<div v-foo>hello</div>`)
|
||||
expect(node.isBlock).toBe(true)
|
||||
|
|
|
@ -286,6 +286,23 @@ describe('compiler: transform v-on', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('should NOT wrap as function if expression is already function expression (async)', () => {
|
||||
const { node } = parseWithVOn(
|
||||
`<div @click="async $event => await foo($event)"/>`,
|
||||
)
|
||||
expect((node.codegenNode as VNodeCall).props).toMatchObject({
|
||||
properties: [
|
||||
{
|
||||
key: { content: `onClick` },
|
||||
value: {
|
||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
||||
content: `async $event => await foo($event)`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test('should NOT wrap as function if expression is already function expression (with newlines)', () => {
|
||||
const { node } = parseWithVOn(
|
||||
`<div @click="
|
||||
|
@ -630,6 +647,39 @@ describe('compiler: transform v-on', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('inline async arrow function with no bracket expression handler', () => {
|
||||
const { root, node } = parseWithVOn(
|
||||
`<div v-on:click="async e => await foo(e)" />`,
|
||||
{
|
||||
prefixIdentifiers: true,
|
||||
cacheHandlers: true,
|
||||
},
|
||||
)
|
||||
|
||||
expect(root.cached).toBe(1)
|
||||
const vnodeCall = node.codegenNode as VNodeCall
|
||||
// should not treat cached handler as dynamicProp, so no flags
|
||||
expect(vnodeCall.patchFlag).toBeUndefined()
|
||||
expect(
|
||||
(vnodeCall.props as ObjectExpression).properties[0].value,
|
||||
).toMatchObject({
|
||||
type: NodeTypes.JS_CACHE_EXPRESSION,
|
||||
index: 0,
|
||||
value: {
|
||||
type: NodeTypes.COMPOUND_EXPRESSION,
|
||||
children: [
|
||||
`async `,
|
||||
{ content: `e` },
|
||||
` => await `,
|
||||
{ content: `_ctx.foo` },
|
||||
`(`,
|
||||
{ content: `e` },
|
||||
`)`,
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('inline async function expression handler', () => {
|
||||
const { root, node } = parseWithVOn(
|
||||
`<div v-on:click="async function () { await foo() } " />`,
|
||||
|
|
|
@ -46,13 +46,13 @@
|
|||
},
|
||||
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/parser": "^7.24.6",
|
||||
"@vue/shared": "workspace:*",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.24.0"
|
||||
"@babel/types": "^7.24.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -571,7 +571,7 @@ export interface ForRenderListExpression extends CallExpression {
|
|||
}
|
||||
|
||||
export interface ForIteratorExpression extends FunctionExpression {
|
||||
returns: BlockCodegenNode
|
||||
returns?: BlockCodegenNode
|
||||
}
|
||||
|
||||
// AST Utilities ---------------------------------------------------------------
|
||||
|
|
|
@ -53,6 +53,7 @@ export function walkIdentifiers(
|
|||
}
|
||||
} else if (
|
||||
node.type === 'ObjectProperty' &&
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
parent?.type === 'ObjectPattern'
|
||||
) {
|
||||
// mark property in destructure pattern
|
||||
|
@ -407,6 +408,7 @@ function isReferenced(node: Node, parent: Node, grandparent?: Node): boolean {
|
|||
// no: export { NODE as foo } from "foo";
|
||||
case 'ExportSpecifier':
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (grandparent?.source) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -168,6 +168,8 @@ function parseFilter(node: SimpleExpressionNode, context: TransformContext) {
|
|||
expression = wrapFilter(expression, filters[i], context)
|
||||
}
|
||||
node.content = expression
|
||||
// reset ast since the content is replaced
|
||||
node.ast = undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -174,7 +174,8 @@ export function getConstantType(
|
|||
if (
|
||||
codegenNode.isBlock &&
|
||||
node.tag !== 'svg' &&
|
||||
node.tag !== 'foreignObject'
|
||||
node.tag !== 'foreignObject' &&
|
||||
node.tag !== 'math'
|
||||
) {
|
||||
return ConstantTypes.NOT_CONSTANT
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ export const transformElement: NodeTransform = (node, context) => {
|
|||
// updates inside get proper isSVG flag at runtime. (#639, #643)
|
||||
// This is technically web-specific, but splitting the logic out of core
|
||||
// leads to too much unnecessary complexity.
|
||||
(tag === 'svg' || tag === 'foreignObject'))
|
||||
(tag === 'svg' || tag === 'foreignObject' || tag === 'math'))
|
||||
|
||||
// props
|
||||
if (props.length > 0) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import { hasScopeRef, isMemberExpression } from '../utils'
|
|||
import { TO_HANDLER_KEY } from '../runtimeHelpers'
|
||||
|
||||
const fnExpRE =
|
||||
/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
|
||||
/^\s*(async\s*)?(\([^)]*?\)|[\w$_]+)\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
|
||||
|
||||
export interface VOnDirectiveNode extends DirectiveNode {
|
||||
// v-on without arg is handled directly in ./transformElements.ts due to it affecting
|
||||
|
|
|
@ -1,5 +1,24 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`stringify static html > should bail for <option> elements with number values 1`] = `
|
||||
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||
|
||||
const _hoisted_1 = /*#__PURE__*/_createElementVNode("select", null, [
|
||||
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
|
||||
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
|
||||
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
|
||||
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
|
||||
/*#__PURE__*/_createElementVNode("option", { value: 1 })
|
||||
], -1 /* HOISTED */)
|
||||
const _hoisted_2 = [
|
||||
_hoisted_1
|
||||
]
|
||||
|
||||
return function render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`stringify static html > should bail on bindings that are hoisted but not stringifiable 1`] = `
|
||||
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||
|
||||
|
@ -20,6 +39,19 @@ return function render(_ctx, _cache) {
|
|||
}"
|
||||
`;
|
||||
|
||||
exports[`stringify static html > should work for <option> elements with string values 1`] = `
|
||||
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||
|
||||
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1)
|
||||
const _hoisted_2 = [
|
||||
_hoisted_1
|
||||
]
|
||||
|
||||
return function render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`stringify static html > should work with bindings that are non-static but stringifiable 1`] = `
|
||||
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||
|
||||
|
|
|
@ -485,4 +485,51 @@ describe('stringify static html', () => {
|
|||
expect(code).toMatch(`<code>text1</code>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should work for <option> elements with string values', () => {
|
||||
const { ast, code } = compileWithStringify(
|
||||
`<div><select>${repeat(
|
||||
`<option value="1" />`,
|
||||
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
|
||||
)}</select></div>`,
|
||||
)
|
||||
// should be optimized now
|
||||
expect(ast.hoists).toMatchObject([
|
||||
{
|
||||
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||
callee: CREATE_STATIC,
|
||||
arguments: [
|
||||
JSON.stringify(
|
||||
`<select>${repeat(
|
||||
`<option value="1"></option>`,
|
||||
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
|
||||
)}</select>`,
|
||||
),
|
||||
'1',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: NodeTypes.JS_ARRAY_EXPRESSION,
|
||||
},
|
||||
])
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should bail for <option> elements with number values', () => {
|
||||
const { ast, code } = compileWithStringify(
|
||||
`<div><select>${repeat(
|
||||
`<option :value="1" />`,
|
||||
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
|
||||
)}</select></div>`,
|
||||
)
|
||||
expect(ast.hoists).toMatchObject([
|
||||
{
|
||||
type: NodeTypes.VNODE_CALL,
|
||||
},
|
||||
{
|
||||
type: NodeTypes.JS_ARRAY_EXPRESSION,
|
||||
},
|
||||
])
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
type TextCallNode,
|
||||
type TransformContext,
|
||||
createCallExpression,
|
||||
isStaticArgOf,
|
||||
} from '@vue/compiler-core'
|
||||
import {
|
||||
escapeHtml,
|
||||
|
@ -200,6 +201,7 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
|
|||
// probably only need to check for most common case
|
||||
// i.e. non-phrasing-content tags inside `<p>`
|
||||
function walk(node: ElementNode): boolean {
|
||||
const isOptionTag = node.tag === 'option' && node.ns === Namespaces.HTML
|
||||
for (let i = 0; i < node.props.length; i++) {
|
||||
const p = node.props[i]
|
||||
// bail on non-attr bindings
|
||||
|
@ -225,6 +227,16 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
|
|||
) {
|
||||
return bail()
|
||||
}
|
||||
// <option :value="1"> cannot be safely stringified
|
||||
if (
|
||||
isOptionTag &&
|
||||
isStaticArgOf(p.arg, 'value') &&
|
||||
p.exp &&
|
||||
p.exp.ast &&
|
||||
p.exp.ast.type !== 'StringLiteral'
|
||||
) {
|
||||
return bail()
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
|
|
|
@ -304,3 +304,19 @@ return () => {}
|
|||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`sfc reactive props destructure > rest spread non-inline 1`] = `
|
||||
"import { createPropsRestProxy as _createPropsRestProxy } from 'vue'
|
||||
|
||||
export default {
|
||||
props: ['foo', 'bar'],
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
const rest = _createPropsRestProxy(__props, ["foo"])
|
||||
|
||||
return { rest }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
|
|
@ -264,6 +264,27 @@ describe('sfc reactive props destructure', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('rest spread non-inline', () => {
|
||||
const { content, bindings } = compile(
|
||||
`
|
||||
<script setup>
|
||||
const { foo, ...rest } = defineProps(['foo', 'bar'])
|
||||
</script>
|
||||
<template>{{ rest.bar }}</template>
|
||||
`,
|
||||
{ inlineTemplate: false },
|
||||
)
|
||||
expect(content).toMatch(
|
||||
`const rest = _createPropsRestProxy(__props, ["foo"])`,
|
||||
)
|
||||
assertCode(content)
|
||||
expect(bindings).toStrictEqual({
|
||||
foo: BindingTypes.PROPS,
|
||||
bar: BindingTypes.PROPS,
|
||||
rest: BindingTypes.SETUP_REACTIVE_CONST,
|
||||
})
|
||||
})
|
||||
|
||||
// #6960
|
||||
test('computed static key', () => {
|
||||
const { content, bindings } = compile(`
|
||||
|
|
|
@ -447,6 +447,42 @@ describe('resolveType', () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('keyof', () => {
|
||||
const files = {
|
||||
'/foo.ts': `export type IMP = { ${1}: 1 };`,
|
||||
}
|
||||
|
||||
const { props } = resolve(
|
||||
`
|
||||
import { IMP } from './foo'
|
||||
interface Foo { foo: 1, ${1}: 1 }
|
||||
type Bar = { bar: 1 }
|
||||
declare const obj: Bar
|
||||
declare const set: Set<any>
|
||||
declare const arr: Array<any>
|
||||
|
||||
defineProps<{
|
||||
imp: keyof IMP,
|
||||
foo: keyof Foo,
|
||||
bar: keyof Bar,
|
||||
obj: keyof typeof obj,
|
||||
set: keyof typeof set,
|
||||
arr: keyof typeof arr
|
||||
}>()
|
||||
`,
|
||||
files,
|
||||
)
|
||||
|
||||
expect(props).toStrictEqual({
|
||||
imp: ['Number'],
|
||||
foo: ['String', 'Number'],
|
||||
bar: ['String'],
|
||||
obj: ['String'],
|
||||
set: ['String'],
|
||||
arr: ['String', 'Number'],
|
||||
})
|
||||
})
|
||||
|
||||
test('ExtractPropTypes (element-plus)', () => {
|
||||
const { props, raw } = resolve(
|
||||
`
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
|
||||
import { parse as babelParse } from '@babel/parser'
|
||||
import {
|
||||
type SFCTemplateCompileOptions,
|
||||
compileTemplate,
|
||||
|
@ -452,6 +453,36 @@ test('prefixing edge case for reused AST ssr mode', () => {
|
|||
).not.toThrowError()
|
||||
})
|
||||
|
||||
// #10852
|
||||
test('non-identifier expression in legacy filter syntax', () => {
|
||||
const src = `
|
||||
<template>
|
||||
<div>
|
||||
Today is
|
||||
{{ new Date() | formatDate }}
|
||||
</div>
|
||||
</template>
|
||||
`
|
||||
|
||||
const { descriptor } = parse(src)
|
||||
const compilationResult = compileTemplate({
|
||||
id: 'xxx',
|
||||
filename: 'test.vue',
|
||||
ast: descriptor.template!.ast,
|
||||
source: descriptor.template!.content,
|
||||
ssr: false,
|
||||
compilerOptions: {
|
||||
compatConfig: {
|
||||
MODE: 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(() => {
|
||||
babelParse(compilationResult.code, { sourceType: 'module' })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
interface Pos {
|
||||
line: number
|
||||
column: number
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/parser": "^7.24.6",
|
||||
"@vue/compiler-core": "workspace:*",
|
||||
"@vue/compiler-dom": "workspace:*",
|
||||
"@vue/compiler-ssr": "workspace:*",
|
||||
|
@ -53,15 +53,15 @@
|
|||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.24.0",
|
||||
"@babel/types": "^7.24.6",
|
||||
"@vue/consolidate": "^1.0.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"lru-cache": "10.1.0",
|
||||
"merge-source-map": "^1.1.0",
|
||||
"minimatch": "^9.0.4",
|
||||
"postcss-modules": "^6.0.0",
|
||||
"postcss-selector-parser": "^6.0.16",
|
||||
"pug": "^3.0.2",
|
||||
"sass": "^1.75.0"
|
||||
"postcss-selector-parser": "^6.1.0",
|
||||
"pug": "^3.0.3",
|
||||
"sass": "^1.77.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -524,8 +524,14 @@ export function compileScript(
|
|||
)
|
||||
}
|
||||
|
||||
// defineProps / defineEmits
|
||||
// defineProps
|
||||
const isDefineProps = processDefineProps(ctx, init, decl.id)
|
||||
if (ctx.propsDestructureRestId) {
|
||||
setupBindings[ctx.propsDestructureRestId] =
|
||||
BindingTypes.SETUP_REACTIVE_CONST
|
||||
}
|
||||
|
||||
// defineEmits
|
||||
const isDefineEmits =
|
||||
!isDefineProps && processDefineEmits(ctx, init, decl.id)
|
||||
!isDefineEmits &&
|
||||
|
|
|
@ -37,10 +37,23 @@ export function processDefineOptions(
|
|||
(prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
|
||||
prop.key.type === 'Identifier'
|
||||
) {
|
||||
if (prop.key.name === 'props') propsOption = prop
|
||||
if (prop.key.name === 'emits') emitsOption = prop
|
||||
if (prop.key.name === 'expose') exposeOption = prop
|
||||
if (prop.key.name === 'slots') slotsOption = prop
|
||||
switch (prop.key.name) {
|
||||
case 'props':
|
||||
propsOption = prop
|
||||
break
|
||||
|
||||
case 'emits':
|
||||
emitsOption = prop
|
||||
break
|
||||
|
||||
case 'expose':
|
||||
exposeOption = prop
|
||||
break
|
||||
|
||||
case 'slots':
|
||||
slotsOption = prop
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1448,6 +1448,7 @@ export function inferRuntimeType(
|
|||
ctx: TypeResolveContext,
|
||||
node: Node & MaybeWithScope,
|
||||
scope = node._ownerScope || ctxToScope(ctx),
|
||||
isKeyOf = false,
|
||||
): string[] {
|
||||
try {
|
||||
switch (node.type) {
|
||||
|
@ -1467,8 +1468,18 @@ export function inferRuntimeType(
|
|||
const types = new Set<string>()
|
||||
const members =
|
||||
node.type === 'TSTypeLiteral' ? node.members : node.body.body
|
||||
|
||||
for (const m of members) {
|
||||
if (
|
||||
if (isKeyOf) {
|
||||
if (
|
||||
m.type === 'TSPropertySignature' &&
|
||||
m.key.type === 'NumericLiteral'
|
||||
) {
|
||||
types.add('Number')
|
||||
} else {
|
||||
types.add('String')
|
||||
}
|
||||
} else if (
|
||||
m.type === 'TSCallSignatureDeclaration' ||
|
||||
m.type === 'TSConstructSignatureDeclaration'
|
||||
) {
|
||||
|
@ -1477,6 +1488,7 @@ export function inferRuntimeType(
|
|||
types.add('Object')
|
||||
}
|
||||
}
|
||||
|
||||
return types.size ? Array.from(types) : ['Object']
|
||||
}
|
||||
case 'TSPropertySignature':
|
||||
|
@ -1512,9 +1524,22 @@ export function inferRuntimeType(
|
|||
case 'TSTypeReference': {
|
||||
const resolved = resolveTypeReference(ctx, node, scope)
|
||||
if (resolved) {
|
||||
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
|
||||
return inferRuntimeType(ctx, resolved, resolved._ownerScope, isKeyOf)
|
||||
}
|
||||
|
||||
if (node.typeName.type === 'Identifier') {
|
||||
if (isKeyOf) {
|
||||
switch (node.typeName.name) {
|
||||
case 'String':
|
||||
case 'Array':
|
||||
case 'ArrayLike':
|
||||
case 'ReadonlyArray':
|
||||
return ['String', 'Number']
|
||||
default:
|
||||
return ['String']
|
||||
}
|
||||
}
|
||||
|
||||
switch (node.typeName.name) {
|
||||
case 'Array':
|
||||
case 'Function':
|
||||
|
@ -1634,7 +1659,7 @@ export function inferRuntimeType(
|
|||
// typeof only support identifier in local scope
|
||||
const matched = scope.declares[id.name]
|
||||
if (matched) {
|
||||
return inferRuntimeType(ctx, matched, matched._ownerScope)
|
||||
return inferRuntimeType(ctx, matched, matched._ownerScope, isKeyOf)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
@ -1642,7 +1667,12 @@ export function inferRuntimeType(
|
|||
|
||||
// e.g. readonly
|
||||
case 'TSTypeOperator': {
|
||||
return inferRuntimeType(ctx, node.typeAnnotation, scope)
|
||||
return inferRuntimeType(
|
||||
ctx,
|
||||
node.typeAnnotation,
|
||||
scope,
|
||||
node.operator === 'keyof',
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -1501,8 +1501,7 @@ describe('should work when props type is incompatible with setup returned type '
|
|||
|
||||
describe('withKeys and withModifiers as pro', () => {
|
||||
const onKeydown = withKeys(e => {}, [''])
|
||||
// @ts-expect-error invalid modifiers
|
||||
const onClick = withModifiers(e => {}, [''])
|
||||
const onClick = withModifiers(e => {}, [])
|
||||
;<input onKeydown={onKeydown} onClick={onClick} />
|
||||
})
|
||||
|
||||
|
|
|
@ -12,10 +12,13 @@ const source = ref('foo')
|
|||
const source2 = computed(() => source.value)
|
||||
const source3 = () => 1
|
||||
|
||||
type OnCleanup = (fn: () => void) => void
|
||||
|
||||
// lazy watcher will have consistent types for oldValue.
|
||||
watch(source, (value, oldValue) => {
|
||||
watch(source, (value, oldValue, onCleanup) => {
|
||||
expectType<string>(value)
|
||||
expectType<string>(oldValue)
|
||||
expectType<OnCleanup>(onCleanup)
|
||||
})
|
||||
|
||||
watch([source, source2, source3], (values, oldValues) => {
|
||||
|
@ -92,9 +95,10 @@ defineComponent({
|
|||
created() {
|
||||
this.$watch(
|
||||
() => this.a,
|
||||
(v, ov) => {
|
||||
(v, ov, onCleanup) => {
|
||||
expectType<number>(v)
|
||||
expectType<number>(ov)
|
||||
expectType<OnCleanup>(onCleanup)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
ref,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
} from '../src/index'
|
||||
import { computed } from '@vue/runtime-dom'
|
||||
import { customRef, shallowRef, triggerRef, unref } from '../src/ref'
|
||||
|
@ -251,6 +252,18 @@ describe('reactivity/ref', () => {
|
|||
x: 1,
|
||||
})
|
||||
const x = toRef(a, 'x')
|
||||
|
||||
const b = ref({ y: 1 })
|
||||
|
||||
const c = toRef(b)
|
||||
|
||||
const d = toRef({ z: 1 })
|
||||
|
||||
expect(isRef(d)).toBe(true)
|
||||
expect(d.value.z).toBe(1)
|
||||
|
||||
expect(c).toBe(b)
|
||||
|
||||
expect(isRef(x)).toBe(true)
|
||||
expect(x.value).toBe(1)
|
||||
|
||||
|
@ -453,4 +466,16 @@ describe('reactivity/ref', () => {
|
|||
r.value = obj
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('toValue', () => {
|
||||
const a = ref(1)
|
||||
const b = computed(() => a.value + 1)
|
||||
const c = () => a.value + 2
|
||||
const d = 4
|
||||
|
||||
expect(toValue(a)).toBe(1)
|
||||
expect(toValue(b)).toBe(2)
|
||||
expect(toValue(c)).toBe(3)
|
||||
expect(toValue(d)).toBe(4)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -248,7 +248,11 @@ function createReactiveObject(
|
|||
) {
|
||||
if (!isObject(target)) {
|
||||
if (__DEV__) {
|
||||
warn(`value cannot be made reactive: ${String(target)}`)
|
||||
warn(
|
||||
`value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
|
||||
target,
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ describe('api: lifecycle hooks', () => {
|
|||
}
|
||||
render(h(Comp), root)
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
// #10863
|
||||
expect(fn).toHaveBeenCalledWith()
|
||||
})
|
||||
|
||||
it('onMounted', () => {
|
||||
|
|
|
@ -97,6 +97,30 @@ describe('api: watch', () => {
|
|||
expect(spy).toBeCalledWith([1], [1], expect.anything())
|
||||
})
|
||||
|
||||
it('should not call functions inside a reactive source array', () => {
|
||||
const spy1 = vi.fn()
|
||||
const array = reactive([spy1])
|
||||
const spy2 = vi.fn()
|
||||
watch(array, spy2, { immediate: true })
|
||||
expect(spy1).toBeCalledTimes(0)
|
||||
expect(spy2).toBeCalledWith([spy1], undefined, expect.anything())
|
||||
})
|
||||
|
||||
it('should not unwrap refs in a reactive source array', async () => {
|
||||
const val = ref({ foo: 1 })
|
||||
const array = reactive([val])
|
||||
const spy = vi.fn()
|
||||
watch(array, spy, { immediate: true })
|
||||
expect(spy).toBeCalledTimes(1)
|
||||
expect(spy).toBeCalledWith([val], undefined, expect.anything())
|
||||
|
||||
// deep by default
|
||||
val.value.foo++
|
||||
await nextTick()
|
||||
expect(spy).toBeCalledTimes(2)
|
||||
expect(spy).toBeCalledWith([val], [val], expect.anything())
|
||||
})
|
||||
|
||||
it('should not fire if watched getter result did not change', async () => {
|
||||
const spy = vi.fn()
|
||||
const n = ref(0)
|
||||
|
@ -187,6 +211,24 @@ describe('api: watch', () => {
|
|||
expect(dummy).toBe(1)
|
||||
})
|
||||
|
||||
it('directly watching reactive array with explicit deep: false', async () => {
|
||||
const val = ref(1)
|
||||
const array: any[] = reactive([val])
|
||||
const spy = vi.fn()
|
||||
watch(array, spy, { immediate: true, deep: false })
|
||||
expect(spy).toBeCalledTimes(1)
|
||||
expect(spy).toBeCalledWith([val], undefined, expect.anything())
|
||||
|
||||
val.value++
|
||||
await nextTick()
|
||||
expect(spy).toBeCalledTimes(1)
|
||||
|
||||
array[1] = 2
|
||||
await nextTick()
|
||||
expect(spy).toBeCalledTimes(2)
|
||||
expect(spy).toBeCalledWith([val, 2], [val, 2], expect.anything())
|
||||
})
|
||||
|
||||
// #9916
|
||||
it('watching shallow reactive array with deep: false', async () => {
|
||||
class foo {
|
||||
|
@ -891,6 +933,52 @@ describe('api: watch', () => {
|
|||
expect(dummy).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('deep with symbols', async () => {
|
||||
const symbol1 = Symbol()
|
||||
const symbol2 = Symbol()
|
||||
const symbol3 = Symbol()
|
||||
const symbol4 = Symbol()
|
||||
|
||||
const raw: any = {
|
||||
[symbol1]: {
|
||||
[symbol2]: 1,
|
||||
},
|
||||
}
|
||||
|
||||
Object.defineProperty(raw, symbol3, {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
value: 1,
|
||||
})
|
||||
|
||||
const state = reactive(raw)
|
||||
const spy = vi.fn()
|
||||
|
||||
watch(() => state, spy, { deep: true })
|
||||
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(0)
|
||||
|
||||
state[symbol1][symbol2] = 2
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Non-enumerable properties don't trigger deep watchers
|
||||
state[symbol3] = 3
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Adding a new symbol property
|
||||
state[symbol4] = 1
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Removing a symbol property
|
||||
delete state[symbol4]
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('immediate', async () => {
|
||||
const count = ref(0)
|
||||
const cb = vi.fn()
|
||||
|
@ -1517,4 +1605,20 @@ describe('api: watch', () => {
|
|||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('circular reference', async () => {
|
||||
const obj = { a: 1 }
|
||||
// @ts-expect-error
|
||||
obj.b = obj
|
||||
const foo = ref(obj)
|
||||
const spy = vi.fn()
|
||||
|
||||
watch(foo, spy, { deep: true })
|
||||
|
||||
// @ts-expect-error
|
||||
foo.value.b.a = 2
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(foo.value.a).toBe(2)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2021,7 +2021,7 @@ describe('Suspense', () => {
|
|||
viewRef.value = 0
|
||||
await nextTick()
|
||||
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
expect(serializeInner(root)).toBe('<div>sync</div>')
|
||||
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
|
@ -2035,6 +2035,56 @@ describe('Suspense', () => {
|
|||
expect(serializeInner(root)).toBe(`<div>sync</div>`)
|
||||
})
|
||||
|
||||
// #10899
|
||||
test('KeepAlive + Suspense switch before branch resolves', async () => {
|
||||
const Async1 = defineAsyncComponent({
|
||||
render() {
|
||||
return h('div', 'async1')
|
||||
},
|
||||
})
|
||||
const Async2 = defineAsyncComponent({
|
||||
render() {
|
||||
return h('div', 'async2')
|
||||
},
|
||||
})
|
||||
const components = [Async1, Async2]
|
||||
const viewRef = ref(0)
|
||||
const root = nodeOps.createElement('div')
|
||||
const App = {
|
||||
render() {
|
||||
return h(KeepAlive, null, {
|
||||
default: () => {
|
||||
return h(Suspense, null, {
|
||||
default: h(components[viewRef.value]),
|
||||
fallback: h('div', 'loading'),
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
render(h(App), root)
|
||||
expect(serializeInner(root)).toBe(`<div>loading</div>`)
|
||||
|
||||
// switch to Async2 before Async1 resolves
|
||||
viewRef.value = 1
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>loading</div>`)
|
||||
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<div>async2</div>')
|
||||
|
||||
viewRef.value = 0
|
||||
await nextTick()
|
||||
await Promise.all(deps)
|
||||
expect(serializeInner(root)).toBe(`<div>async1</div>`)
|
||||
|
||||
viewRef.value = 1
|
||||
await nextTick()
|
||||
await Promise.all(deps)
|
||||
expect(serializeInner(root)).toBe(`<div>async2</div>`)
|
||||
})
|
||||
|
||||
// #6416 follow up / #10017
|
||||
test('Suspense patched during HOC async component re-mount', async () => {
|
||||
const key = ref('k')
|
||||
|
|
|
@ -1160,6 +1160,21 @@ describe('SSR hydration', () => {
|
|||
expect((vnode as any).component?.subTree.children[0].el).toBe(text)
|
||||
})
|
||||
|
||||
// #7215
|
||||
test('empty text node', () => {
|
||||
const Comp = {
|
||||
render(this: any) {
|
||||
return h('p', [''])
|
||||
},
|
||||
}
|
||||
const { container } = mountWithHydration('<p></p>', () => h(Comp))
|
||||
expect(container.childNodes.length).toBe(1)
|
||||
const p = container.childNodes[0]
|
||||
expect(p.childNodes.length).toBe(1)
|
||||
const text = p.childNodes[0]
|
||||
expect(text.nodeType).toBe(3)
|
||||
})
|
||||
|
||||
test('app.unmount()', async () => {
|
||||
const container = document.createElement('DIV')
|
||||
container.innerHTML = '<button></button>'
|
||||
|
@ -1527,6 +1542,13 @@ describe('SSR hydration', () => {
|
|||
expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
|
||||
})
|
||||
|
||||
test('style mismatch when no style attribute is present', () => {
|
||||
mountWithHydration(`<div></div>`, () =>
|
||||
h('div', { style: { color: 'red' } }),
|
||||
)
|
||||
expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
|
||||
})
|
||||
|
||||
test('style mismatch w/ v-show', () => {
|
||||
mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
|
||||
withDirectives(createVNode('div', { style: 'color: red' }, ''), [
|
||||
|
|
|
@ -291,6 +291,16 @@ describe('vnode', () => {
|
|||
const cloned8 = cloneVNode(original4)
|
||||
expect(cloned8.ref).toMatchObject({ i: mockInstance2, r, k: 'foo' })
|
||||
|
||||
// @ts-expect-error #8230
|
||||
const original5 = createVNode('div', { ref: 111, ref_key: 'foo' })
|
||||
expect(original5.ref).toMatchObject({
|
||||
i: mockInstance2,
|
||||
r: '111',
|
||||
k: 'foo',
|
||||
})
|
||||
const cloned9 = cloneVNode(original5)
|
||||
expect(cloned9.ref).toMatchObject({ i: mockInstance2, r: '111', k: 'foo' })
|
||||
|
||||
setCurrentRenderingInstance(null)
|
||||
})
|
||||
|
||||
|
|
|
@ -68,10 +68,15 @@ export function injectHook(
|
|||
|
||||
export const createHook =
|
||||
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
|
||||
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
|
||||
(hook: T, target: ComponentInternalInstance | null = currentInstance) => {
|
||||
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
|
||||
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
|
||||
injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)
|
||||
if (
|
||||
!isInSSRComponentSetup ||
|
||||
lifecycle === LifecycleHooks.SERVER_PREFETCH
|
||||
) {
|
||||
injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)
|
||||
}
|
||||
}
|
||||
|
||||
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
|
||||
export const onMounted = createHook(LifecycleHooks.MOUNTED)
|
||||
|
|
|
@ -66,7 +66,7 @@ type MapSources<T, Immediate> = {
|
|||
: never
|
||||
}
|
||||
|
||||
type OnCleanup = (cleanupFn: () => void) => void
|
||||
export type OnCleanup = (cleanupFn: () => void) => void
|
||||
|
||||
export interface WatchOptionsBase extends DebuggerOptions {
|
||||
flush?: 'pre' | 'post' | 'sync'
|
||||
|
@ -499,6 +499,11 @@ export function traverse(
|
|||
for (const key in value) {
|
||||
traverse(value[key], depth, seen)
|
||||
}
|
||||
for (const key of Object.getOwnPropertySymbols(value)) {
|
||||
if (Object.prototype.propertyIsEnumerable.call(value, key)) {
|
||||
traverse(value[key as any], depth, seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -36,8 +36,7 @@ import {
|
|||
legacyresolveScopedSlots,
|
||||
} from './renderHelpers'
|
||||
import { resolveFilter } from '../helpers/resolveAssets'
|
||||
import type { InternalSlots, Slots } from '../componentSlots'
|
||||
import type { ContextualRenderFn } from '../componentRenderContext'
|
||||
import type { Slots } from '../componentSlots'
|
||||
import { resolveMergedOptions } from '../componentOptions'
|
||||
|
||||
export type LegacyPublicInstance = ComponentPublicInstance &
|
||||
|
@ -106,14 +105,7 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
|
|||
|
||||
$scopedSlots: i => {
|
||||
assertCompatEnabled(DeprecationTypes.INSTANCE_SCOPED_SLOTS, i)
|
||||
const res: InternalSlots = {}
|
||||
for (const key in i.slots) {
|
||||
const fn = i.slots[key]!
|
||||
if (!(fn as ContextualRenderFn)._ns /* non-scoped slot */) {
|
||||
res[key] = fn
|
||||
}
|
||||
}
|
||||
return res
|
||||
return __DEV__ ? shallowReadonly(i.slots) : i.slots
|
||||
},
|
||||
|
||||
$on: i => on.bind(null, i),
|
||||
|
|
|
@ -1057,7 +1057,7 @@ export function finishComponentSetup(
|
|||
: ``) /* should not happen */,
|
||||
)
|
||||
} else {
|
||||
warn(`Component is missing template or render function.`)
|
||||
warn(`Component is missing template or render function: `, Component)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from './component'
|
||||
import { nextTick, queueJob } from './scheduler'
|
||||
import {
|
||||
type OnCleanup,
|
||||
type WatchOptions,
|
||||
type WatchStopHandle,
|
||||
instanceWatch,
|
||||
|
@ -317,8 +318,8 @@ export type ComponentPublicInstance<
|
|||
$watch<T extends string | ((...args: any) => any)>(
|
||||
source: T,
|
||||
cb: T extends (...args: any) => infer R
|
||||
? (...args: [R, R]) => any
|
||||
: (...args: any) => any,
|
||||
? (...args: [R, R, OnCleanup]) => any
|
||||
: (...args: [any, any, OnCleanup]) => any,
|
||||
options?: WatchOptions,
|
||||
): WatchStopHandle
|
||||
} & ExposedKeys<
|
||||
|
@ -486,9 +487,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
|||
return desc.get.call(instance.proxy)
|
||||
} else {
|
||||
const val = globalProperties[key]
|
||||
return isFunction(val)
|
||||
? Object.assign(val.bind(instance.proxy), val)
|
||||
: val
|
||||
return isFunction(val) ? extend(val.bind(instance.proxy), val) : val
|
||||
}
|
||||
} else {
|
||||
return globalProperties[key]
|
||||
|
|
|
@ -228,7 +228,15 @@ const KeepAliveImpl: ComponentOptions = {
|
|||
const cacheSubtree = () => {
|
||||
// fix #1621, the pendingCacheKey could be 0
|
||||
if (pendingCacheKey != null) {
|
||||
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
|
||||
// if KeepAlive child is a Suspense, it needs to be cached after Suspense resolves
|
||||
// avoid caching vnode that not been mounted
|
||||
if (isSuspense(instance.subTree.type)) {
|
||||
queuePostRenderEffect(() => {
|
||||
cache.set(pendingCacheKey!, getInnerChild(instance.subTree))
|
||||
}, instance.subTree.suspense)
|
||||
} else {
|
||||
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(cacheSubtree)
|
||||
|
@ -305,11 +313,11 @@ const KeepAliveImpl: ComponentOptions = {
|
|||
rawVNode.ssContent = vnode
|
||||
}
|
||||
}
|
||||
// #1513 it's possible for the returned vnode to be cloned due to attr
|
||||
// #1511 it's possible for the returned vnode to be cloned due to attr
|
||||
// fallthrough or scopeId, so the vnode here may not be the final vnode
|
||||
// that is mounted. Instead of caching it directly, we store the pending
|
||||
// key and cache `instance.subTree` (the normalized vnode) in
|
||||
// beforeMount/beforeUpdate hooks.
|
||||
// mounted/updated hooks.
|
||||
pendingCacheKey = key
|
||||
|
||||
if (cachedVNode) {
|
||||
|
|
|
@ -47,14 +47,13 @@ const resolveTarget = <T = RendererElement>(
|
|||
return null
|
||||
} else {
|
||||
const target = select(targetSelector)
|
||||
if (!target) {
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Failed to locate Teleport target with selector "${targetSelector}". ` +
|
||||
`Note the target element must exist before the component is mounted - ` +
|
||||
`i.e. the target cannot be rendered by the component itself, and ` +
|
||||
`ideally should be outside of the entire Vue component tree.`,
|
||||
)
|
||||
if (__DEV__ && !target && !isTeleportDisabled(props)) {
|
||||
warn(
|
||||
`Failed to locate Teleport target with selector "${targetSelector}". ` +
|
||||
`Note the target element must exist before the component is mounted - ` +
|
||||
`i.e. the target cannot be rendered by the component itself, and ` +
|
||||
`ideally should be outside of the entire Vue component tree.`,
|
||||
)
|
||||
}
|
||||
return target as T
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ export function setDevtoolsHook(hook: DevtoolsHook, target: any) {
|
|||
// some envs mock window but not fully
|
||||
window.HTMLElement &&
|
||||
// also exclude jsdom
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
!window.navigator?.userAgent?.includes('jsdom')
|
||||
) {
|
||||
const replay = (target.__VUE_DEVTOOLS_HOOK_REPLAY__ =
|
||||
|
|
|
@ -43,7 +43,7 @@ export function renderList<T>(
|
|||
source: T,
|
||||
renderItem: <K extends keyof T>(
|
||||
value: T[K],
|
||||
key: K,
|
||||
key: string,
|
||||
index: number,
|
||||
) => VNodeChild,
|
||||
): VNodeChild[]
|
||||
|
|
|
@ -541,7 +541,9 @@ export function createHydrationFunctions(
|
|||
optimized,
|
||||
)
|
||||
} else if (vnode.type === Text && !vnode.children) {
|
||||
continue
|
||||
// #7215 create a TextNode for empty text node
|
||||
// because server rendered HTML won't contain a text node
|
||||
insert((vnode.el = createText('')), container)
|
||||
} else {
|
||||
hasMismatch = true
|
||||
if (
|
||||
|
@ -727,8 +729,8 @@ function propHasMismatch(
|
|||
): boolean {
|
||||
let mismatchType: string | undefined
|
||||
let mismatchKey: string | undefined
|
||||
let actual: any
|
||||
let expected: any
|
||||
let actual: string | boolean | null | undefined
|
||||
let expected: string | boolean | null | undefined
|
||||
if (key === 'class') {
|
||||
// classes might be in different order, but that doesn't affect cascade
|
||||
// so we just need to check if the class lists contain the same classes.
|
||||
|
@ -739,7 +741,7 @@ function propHasMismatch(
|
|||
}
|
||||
} else if (key === 'style') {
|
||||
// style might be in different order, but that doesn't affect cascade
|
||||
actual = el.getAttribute('style')
|
||||
actual = el.getAttribute('style') || ''
|
||||
expected = isString(clientValue)
|
||||
? clientValue
|
||||
: stringifyStyle(normalizeStyle(clientValue))
|
||||
|
@ -755,11 +757,14 @@ function propHasMismatch(
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const root = instance?.subTree
|
||||
if (
|
||||
vnode === root ||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
(root?.type === Fragment && (root.children as VNode[]).includes(vnode))
|
||||
) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const cssVars = instance?.getCssVars?.()
|
||||
for (const key in cssVars) {
|
||||
expectedMap.set(`--${key}`, String(cssVars[key]))
|
||||
|
@ -840,7 +845,9 @@ function toStyleMap(str: string): Map<string, string> {
|
|||
const styleMap: Map<string, string> = new Map()
|
||||
for (const item of str.split(';')) {
|
||||
let [key, value] = item.split(':')
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
key = key?.trim()
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
value = value?.trim()
|
||||
if (key && value) {
|
||||
styleMap.set(key, value)
|
||||
|
|
|
@ -45,6 +45,7 @@ export function warn(msg: string, ...args: any[]) {
|
|||
instance,
|
||||
ErrorCodes.APP_WARN_HANDLER,
|
||||
[
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
msg + args.map(a => a.toString?.() ?? JSON.stringify(a)).join(''),
|
||||
instance && instance.proxy,
|
||||
trace
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
type Ref,
|
||||
type VueElement,
|
||||
createApp,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
|
@ -60,6 +61,54 @@ describe('defineCustomElement', () => {
|
|||
expect(e.shadowRoot!.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
// #10610
|
||||
test('When elements move, avoid prematurely disconnecting MutationObserver', async () => {
|
||||
const CustomInput = defineCustomElement({
|
||||
props: ['value'],
|
||||
emits: ['update'],
|
||||
setup(props, { emit }) {
|
||||
return () =>
|
||||
h('input', {
|
||||
type: 'number',
|
||||
value: props.value,
|
||||
onInput: (e: InputEvent) => {
|
||||
const num = (e.target! as HTMLInputElement).valueAsNumber
|
||||
emit('update', Number.isNaN(num) ? null : num)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
customElements.define('my-el-input', CustomInput)
|
||||
const num = ref('12')
|
||||
const containerComp = defineComponent({
|
||||
setup() {
|
||||
return () => {
|
||||
return h('div', [
|
||||
h('my-el-input', {
|
||||
value: num.value,
|
||||
onUpdate: ($event: CustomEvent) => {
|
||||
num.value = $event.detail[0]
|
||||
},
|
||||
}),
|
||||
h('div', { id: 'move' }),
|
||||
])
|
||||
}
|
||||
},
|
||||
})
|
||||
const app = createApp(containerComp)
|
||||
app.mount(container)
|
||||
const myInputEl = container.querySelector('my-el-input')!
|
||||
const inputEl = myInputEl.shadowRoot!.querySelector('input')!
|
||||
await nextTick()
|
||||
expect(inputEl.value).toBe('12')
|
||||
const moveEl = container.querySelector('#move')!
|
||||
moveEl.append(myInputEl)
|
||||
await nextTick()
|
||||
myInputEl.removeAttribute('value')
|
||||
await nextTick()
|
||||
expect(inputEl.value).toBe('')
|
||||
})
|
||||
|
||||
test('should not unmount on move', async () => {
|
||||
container.innerHTML = `<div><my-element></my-element></div>`
|
||||
const e = container.childNodes[0].childNodes[0] as VueElement
|
||||
|
|
|
@ -256,7 +256,13 @@ describe('vModel', () => {
|
|||
it('should support modifiers', async () => {
|
||||
const component = defineComponent({
|
||||
data() {
|
||||
return { number: null, trim: null, lazy: null, trimNumber: null }
|
||||
return {
|
||||
number: null,
|
||||
trim: null,
|
||||
lazy: null,
|
||||
trimNumber: null,
|
||||
trimLazy: null,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return [
|
||||
|
@ -284,6 +290,19 @@ describe('vModel', () => {
|
|||
trim: true,
|
||||
},
|
||||
),
|
||||
withVModel(
|
||||
h('input', {
|
||||
class: 'trim-lazy',
|
||||
'onUpdate:modelValue': (val: any) => {
|
||||
this.trimLazy = val
|
||||
},
|
||||
}),
|
||||
this.trim,
|
||||
{
|
||||
trim: true,
|
||||
lazy: true,
|
||||
},
|
||||
),
|
||||
withVModel(
|
||||
h('input', {
|
||||
class: 'trim-number',
|
||||
|
@ -317,6 +336,7 @@ describe('vModel', () => {
|
|||
const number = root.querySelector('.number')
|
||||
const trim = root.querySelector('.trim')
|
||||
const trimNumber = root.querySelector('.trim-number')
|
||||
const trimLazy = root.querySelector('.trim-lazy')
|
||||
const lazy = root.querySelector('.lazy')
|
||||
const data = root._vnode.component.data
|
||||
|
||||
|
@ -340,6 +360,11 @@ describe('vModel', () => {
|
|||
await nextTick()
|
||||
expect(data.trimNumber).toEqual(1.2)
|
||||
|
||||
trimLazy.value = ' ddd '
|
||||
triggerEvent('change', trimLazy)
|
||||
await nextTick()
|
||||
expect(data.trimLazy).toEqual('ddd')
|
||||
|
||||
lazy.value = 'foo'
|
||||
triggerEvent('change', lazy)
|
||||
await nextTick()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { nodeOps, svgNS } from '../src/nodeOps'
|
||||
|
||||
import { defineComponent, h, nextTick, ref } from 'vue'
|
||||
import { mathmlNS, nodeOps, svgNS } from '../src/nodeOps'
|
||||
import { render } from '@vue/runtime-dom'
|
||||
describe('runtime-dom: node-ops', () => {
|
||||
test("the <select>'s multiple attr should be set in createElement", () => {
|
||||
const el = nodeOps.createElement('select', undefined, undefined, {
|
||||
|
@ -106,5 +107,38 @@ describe('runtime-dom: node-ops', () => {
|
|||
expect(nodes[0]).toBe(parent.firstChild)
|
||||
expect(nodes[1]).toBe(parent.childNodes[parent.childNodes.length - 2])
|
||||
})
|
||||
|
||||
test('The math elements should keep their MathML namespace', async () => {
|
||||
let root = document.createElement('div') as any
|
||||
|
||||
let countRef: any
|
||||
const component = defineComponent({
|
||||
data() {
|
||||
return { value: 0 }
|
||||
},
|
||||
setup() {
|
||||
const count = ref(0)
|
||||
countRef = count
|
||||
return {
|
||||
count,
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<math>
|
||||
<mrow class="bar" v-if="count % 2">Bar</mrow>
|
||||
<msup class="foo" v-else>Foo</msup>
|
||||
</math>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
render(h(component), root)
|
||||
const foo = root.querySelector('.foo')
|
||||
expect(foo.namespaceURI).toBe(mathmlNS)
|
||||
countRef.value++
|
||||
await nextTick()
|
||||
const bar = root.querySelector('.bar')
|
||||
expect(bar.namespaceURI).toBe(mathmlNS)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -199,12 +199,12 @@ export class VueElement extends BaseClass {
|
|||
|
||||
disconnectedCallback() {
|
||||
this._connected = false
|
||||
if (this._ob) {
|
||||
this._ob.disconnect()
|
||||
this._ob = null
|
||||
}
|
||||
nextTick(() => {
|
||||
if (!this._connected) {
|
||||
if (this._ob) {
|
||||
this._ob.disconnect()
|
||||
this._ob = null
|
||||
}
|
||||
render(null, this.shadowRoot!)
|
||||
this._instance = null
|
||||
}
|
||||
|
|
|
@ -406,7 +406,7 @@ export interface DataHTMLAttributes extends HTMLAttributes {
|
|||
|
||||
export interface DetailsHTMLAttributes extends HTMLAttributes {
|
||||
open?: Booleanish
|
||||
onToggle?: Event
|
||||
onToggle?: (payload: ToggleEvent) => void
|
||||
}
|
||||
|
||||
export interface DelHTMLAttributes extends HTMLAttributes {
|
||||
|
|
|
@ -266,7 +266,7 @@ export function renderVNodeChildren(
|
|||
push: PushFn,
|
||||
children: VNodeArrayChildren,
|
||||
parentComponent: ComponentInternalInstance,
|
||||
slotScopeId: string | undefined,
|
||||
slotScopeId?: string,
|
||||
) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
renderVNode(push, normalizeVNode(children[i]), parentComponent, slotScopeId)
|
||||
|
@ -277,7 +277,7 @@ function renderElementVNode(
|
|||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance,
|
||||
slotScopeId: string | undefined,
|
||||
slotScopeId?: string,
|
||||
) {
|
||||
const tag = vnode.type as string
|
||||
let { props, children, shapeFlag, scopeId, dirs } = vnode
|
||||
|
@ -362,7 +362,7 @@ function renderTeleportVNode(
|
|||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance,
|
||||
slotScopeId: string | undefined,
|
||||
slotScopeId?: string,
|
||||
) {
|
||||
const target = vnode.props && vnode.props.to
|
||||
const disabled = vnode.props && vnode.props.disabled
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"vite": "^5.2.10"
|
||||
"vite": "^5.2.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/repl": "^4.1.2",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Vite Vue Starter
|
||||
|
||||
This is a project template using [Vite](https://vitejs.dev/). It requires [Node.js](https://nodejs.org) version 18+, 20+.
|
||||
This is a project template using [Vite](https://vitejs.dev/). It requires [Node.js](https://nodejs.org) version 18+ or 20+.
|
||||
|
||||
To start:
|
||||
|
||||
|
@ -11,4 +11,8 @@ npm run dev
|
|||
# if using yarn:
|
||||
yarn
|
||||
yarn dev
|
||||
|
||||
# if using pnpm:
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"vite": "^5.2.10"
|
||||
"vite": "^5.2.11"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,9 +133,9 @@ export const toHandlerKey = cacheStringFunction(<T extends string>(str: T) => {
|
|||
export const hasChanged = (value: any, oldValue: any): boolean =>
|
||||
!Object.is(value, oldValue)
|
||||
|
||||
export const invokeArrayFns = (fns: Function[], arg?: any) => {
|
||||
export const invokeArrayFns = (fns: Function[], ...arg: any[]) => {
|
||||
for (let i = 0; i < fns.length; i++) {
|
||||
fns[i](arg)
|
||||
fns[i](...arg)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,8 +51,8 @@ export function stringifyStyle(
|
|||
}
|
||||
for (const key in styles) {
|
||||
const value = styles[key]
|
||||
const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key)
|
||||
if (isString(value) || typeof value === 'number') {
|
||||
const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key)
|
||||
// only render valid values
|
||||
ret += `${normalizedKey}:${value};`
|
||||
}
|
||||
|
|
|
@ -284,7 +284,7 @@ describe('INSTANCE_SCOPED_SLOTS', () => {
|
|||
).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('should not include legacy slot usage in $scopedSlots', () => {
|
||||
test('should include legacy slot usage in $scopedSlots', () => {
|
||||
let normalSlots: Slots
|
||||
let scopedSlots: Slots
|
||||
new Vue({
|
||||
|
@ -301,7 +301,7 @@ describe('INSTANCE_SCOPED_SLOTS', () => {
|
|||
}).$mount()
|
||||
|
||||
expect('default' in normalSlots!).toBe(true)
|
||||
expect('default' in scopedSlots!).toBe(false)
|
||||
expect('default' in scopedSlots!).toBe(true)
|
||||
|
||||
expect(
|
||||
deprecationData[DeprecationTypes.INSTANCE_SCOPED_SLOTS].message,
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/vuejs/core/tree/main/packages/vue-compat#readme",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/parser": "^7.24.6",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
|
|
1287
pnpm-lock.yaml
1287
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -41,6 +41,8 @@ export default defineConfig({
|
|||
'packages/runtime-dom/src/components/Transition*',
|
||||
// mostly entries
|
||||
'packages/vue-compat/**',
|
||||
'packages/sfc-playground/**',
|
||||
'scripts/**',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue