chore: Merge branch 'main' into minor

This commit is contained in:
Evan You 2024-05-30 11:21:12 +08:00
commit 1d8727ec97
No known key found for this signature in database
GPG Key ID: B9D421896CA450FB
61 changed files with 1450 additions and 749 deletions

View File

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

View File

@ -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

View File

@ -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 -->

View File

@ -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)

View File

@ -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',

View File

@ -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": {

View File

@ -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)

View File

@ -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() } " />`,

View File

@ -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"
}
}

View File

@ -571,7 +571,7 @@ export interface ForRenderListExpression extends CallExpression {
}
export interface ForIteratorExpression extends FunctionExpression {
returns: BlockCodegenNode
returns?: BlockCodegenNode
}
// AST Utilities ---------------------------------------------------------------

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(`

View File

@ -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(
`

View File

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

View File

@ -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"
}
}

View File

@ -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 &&

View File

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

View File

@ -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) {

View File

@ -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} />
})

View File

@ -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)
},
)
},

View File

@ -7,6 +7,7 @@ import {
ref,
toRef,
toRefs,
toValue,
} from '../src/index'
import { computed } from '@vue/runtime-dom'
import { customRef, shallowRef, triggerRef, unref } from '../src/ref'
@ -251,6 +252,18 @@ describe('reactivity/ref', () => {
x: 1,
})
const x = toRef(a, 'x')
const b = ref({ y: 1 })
const c = toRef(b)
const d = toRef({ z: 1 })
expect(isRef(d)).toBe(true)
expect(d.value.z).toBe(1)
expect(c).toBe(b)
expect(isRef(x)).toBe(true)
expect(x.value).toBe(1)
@ -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)
})
})

View File

@ -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
}

View File

@ -40,6 +40,8 @@ describe('api: lifecycle hooks', () => {
}
render(h(Comp), root)
expect(fn).toHaveBeenCalledTimes(1)
// #10863
expect(fn).toHaveBeenCalledWith()
})
it('onMounted', () => {

View File

@ -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)
})
})

View File

@ -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')

View File

@ -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' }, ''), [

View File

@ -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)
})

View File

@ -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)

View File

@ -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
}

View File

@ -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),

View File

@ -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)
}
}
}

View File

@ -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]

View File

@ -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) {

View File

@ -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
}

View File

@ -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__ =

View File

@ -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[]

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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)
})
})
})

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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",

View File

@ -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
```

View File

@ -12,6 +12,6 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.2.10"
"vite": "^5.2.11"
}
}

View File

@ -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)
}
}

View File

@ -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};`
}

View File

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

View File

@ -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"
},

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,8 @@ export default defineConfig({
'packages/runtime-dom/src/components/Transition*',
// mostly entries
'packages/vue-compat/**',
'packages/sfc-playground/**',
'scripts/**',
],
},
},