diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d9a2f75038..51e718af1e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,8 +3,8 @@
### Reverts
-* Revert "fix(compiler-sfc): add scoping tag to trailing universal selector (#1…" (#13406) ([19f23b1](https://github.com/vuejs/core/commit/19f23b180bb679e38db95d6a10a420abeedc8e1c)), closes [#1](https://github.com/vuejs/core/issues/1) [#13406](https://github.com/vuejs/core/issues/13406)
-* Revert "fix(compiler-sfc): add error handling for defineModel() without varia…" (#13390) ([42f879f](https://github.com/vuejs/core/commit/42f879fcab48e0e1011967a771b4ad9e8838d760)), closes [#13390](https://github.com/vuejs/core/issues/13390)
+* Revert "fix(compiler-sfc): add scoping tag to trailing universal selector" (#13406) ([19f23b1](https://github.com/vuejs/core/commit/19f23b180bb679e38db95d6a10a420abeedc8e1c)), closes [#13406](https://github.com/vuejs/core/issues/13406)
+* Revert "fix(compiler-sfc): add error handling for defineModel() without variable" (#13390) ([42f879f](https://github.com/vuejs/core/commit/42f879fcab48e0e1011967a771b4ad9e8838d760)), closes [#13390](https://github.com/vuejs/core/issues/13390)
diff --git a/package.json b/package.json
index 16dd2f22e30..98c1e096855 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"version": "3.5.16",
- "packageManager": "pnpm@10.11.0",
+ "packageManager": "pnpm@10.11.1",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@@ -71,7 +71,7 @@
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.11.29",
"@types/hash-sum": "^1.0.2",
- "@types/node": "^22.15.21",
+ "@types/node": "^22.15.29",
"@types/semver": "^7.7.0",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^3.1.4",
@@ -79,7 +79,7 @@
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
- "esbuild": "^0.25.4",
+ "esbuild": "^0.25.5",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.27.0",
"eslint-plugin-import-x": "^4.13.1",
diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts
index 4e5a9616511..cdc2b09fd48 100644
--- a/packages/compiler-core/__tests__/parse.spec.ts
+++ b/packages/compiler-core/__tests__/parse.spec.ts
@@ -2271,6 +2271,11 @@ describe('compiler: parse', () => {
expect(span.loc.start.offset).toBe(0)
expect(span.loc.end.offset).toBe(27)
})
+
+ test('correct loc when a line in attribute value ends with &', () => {
+ const [span] = baseParse(``).children
+ expect(span.loc.end.line).toBe(2)
+ })
})
describe('decodeEntities option', () => {
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap
index 375a0c8674a..b8bef22c478 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap
@@ -8,7 +8,7 @@ return function render(_ctx, _cache) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
- _createElementVNode("div", { key: "foo" }, null, -1 /* HOISTED */)
+ _createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */)
])))
}
}"
@@ -25,11 +25,11 @@ return function render(_ctx, _cache) {
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
- ], -1 /* HOISTED */),
+ ], -1 /* CACHED */),
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
- ], -1 /* HOISTED */)
+ ], -1 /* CACHED */)
])))
}
}"
@@ -45,7 +45,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", null, [
_createCommentVNode("comment")
- ], -1 /* HOISTED */)
+ ], -1 /* CACHED */)
])))
}
}"
@@ -59,9 +59,9 @@ return function render(_ctx, _cache) {
const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
- _createElementVNode("span", null, null, -1 /* HOISTED */),
+ _createElementVNode("span", null, null, -1 /* CACHED */),
_createTextVNode("foo"),
- _createElementVNode("div", null, null, -1 /* HOISTED */)
+ _createElementVNode("div", null, null, -1 /* CACHED */)
])))
}
}"
@@ -75,7 +75,7 @@ return function render(_ctx, _cache) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
- _createElementVNode("span", { class: "inline" }, "hello", -1 /* HOISTED */)
+ _createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */)
])))
}
}"
@@ -148,7 +148,7 @@ return function render(_ctx, _cache) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
- _createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* HOISTED */)
+ _createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */)
])))
}
}"
@@ -162,7 +162,7 @@ return function render(_ctx, _cache) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
- _createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* HOISTED */)
+ _createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */)
])))
}
}"
@@ -178,7 +178,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(1, (i) => {
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
- _createElementVNode("span", { class: "hi" }, null, -1 /* HOISTED */)
+ _createElementVNode("span", { class: "hi" }, null, -1 /* CACHED */)
]))]))
}), 256 /* UNKEYED_FRAGMENT */))
]))
@@ -216,7 +216,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_withDirectives((_openBlock(), _createElementBlock("svg", null, _cache[0] || (_cache[0] = [
- _createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* HOISTED */)
+ _createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */)
]))), [
[_directive_foo]
])
@@ -402,7 +402,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
ok
? (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
- _createElementVNode("span", null, null, -1 /* HOISTED */)
+ _createElementVNode("span", null, null, -1 /* CACHED */)
])))
: _createCommentVNode("v-if", true)
]))
@@ -410,6 +410,32 @@ return function render(_ctx, _cache) {
}"
`;
+exports[`compiler: cacheStatic transform > should hoist props for root with single element excluding comments 1`] = `
+"const _Vue = Vue
+const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
+
+const _hoisted_1 = { id: "a" }
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_openBlock(), _createElementBlock(_Fragment, null, [
+ _createCommentVNode("comment"),
+ _createElementVNode("div", _hoisted_1, _cache[0] || (_cache[0] = [
+ _createElementVNode("div", { id: "b" }, [
+ _createElementVNode("div", { id: "c" }, [
+ _createElementVNode("div", { id: "d" }, [
+ _createElementVNode("div", { id: "e" }, "hello")
+ ])
+ ])
+ ], -1 /* CACHED */)
+ ]))
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ }
+}"
+`;
+
exports[`compiler: cacheStatic transform > should hoist v-for children if static 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
@@ -423,7 +449,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
- _createElementVNode("span", null, null, -1 /* HOISTED */)
+ _createElementVNode("span", null, null, -1 /* CACHED */)
])))
}), 256 /* UNKEYED_FRAGMENT */))
]))
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
index 0d9c0e743de..2cd13bab036 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
@@ -246,6 +246,28 @@ return function render(_ctx, _cache) {
}"
`;
+exports[`compiler: transform component slots > with whitespace: 'preserve' > named slot with v-if + v-else 1`] = `
+"const { resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, openBlock: _openBlock, createBlock: _createBlock } = Vue
+
+return function render(_ctx, _cache) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [
+ ok
+ ? {
+ name: "one",
+ fn: _withCtx(() => ["foo"]),
+ key: "0"
+ }
+ : {
+ name: "two",
+ fn: _withCtx(() => ["baz"]),
+ key: "1"
+ }
+ ]), 1024 /* DYNAMIC_SLOTS */))
+}"
+`;
+
exports[`compiler: transform component slots > with whitespace: 'preserve' > should not generate whitespace only default slot 1`] = `
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue
diff --git a/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts b/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts
index 358c0e31c3d..74f6caca328 100644
--- a/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts
@@ -543,6 +543,32 @@ describe('compiler: cacheStatic transform', () => {
expect(generate(root).code).toMatchSnapshot()
})
+ test('should hoist props for root with single element excluding comments', () => {
+ // deeply nested div to trigger stringification condition
+ const root = transformWithCache(
+ `
`,
+ )
+ expect(root.cached.length).toBe(1)
+ expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'a' })])
+
+ expect((root.codegenNode as VNodeCall).children).toMatchObject([
+ {
+ type: NodeTypes.COMMENT,
+ content: 'comment',
+ },
+ {
+ type: NodeTypes.ELEMENT,
+ codegenNode: {
+ type: NodeTypes.VNODE_CALL,
+ tag: `"div"`,
+ props: { content: `_hoisted_1` },
+ children: { type: NodeTypes.JS_CACHE_EXPRESSION },
+ },
+ },
+ ])
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
describe('prefixIdentifiers', () => {
test('cache nested static tree with static interpolation', () => {
const root = transformWithCache(
diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
index 4766c2ca9d8..e0f44a064fb 100644
--- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
@@ -988,5 +988,19 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
+
+ test('named slot with v-if + v-else', () => {
+ const source = `
+
+ foo
+ baz
+
+ `
+ const { root } = parseWithSlots(source, {
+ whitespace: 'preserve',
+ })
+
+ expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
+ })
})
})
diff --git a/packages/compiler-core/src/parser.ts b/packages/compiler-core/src/parser.ts
index 7d1b01360c4..3eb3a976f4e 100644
--- a/packages/compiler-core/src/parser.ts
+++ b/packages/compiler-core/src/parser.ts
@@ -647,7 +647,7 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
// whitespace management
if (!tokenizer.inRCDATA) {
- el.children = condenseWhitespace(children, tag)
+ el.children = condenseWhitespace(children)
}
if (ns === Namespaces.HTML && currentOptions.isIgnoreNewlineTag(tag)) {
@@ -832,10 +832,7 @@ function isUpperCase(c: number) {
}
const windowsNewlineRE = /\r\n/g
-function condenseWhitespace(
- nodes: TemplateChildNode[],
- tag?: string,
-): TemplateChildNode[] {
+function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
const shouldCondense = currentOptions.whitespace !== 'preserve'
let removedWhitespace = false
for (let i = 0; i < nodes.length; i++) {
diff --git a/packages/compiler-core/src/tokenizer.ts b/packages/compiler-core/src/tokenizer.ts
index 329e8b48181..b8a74790259 100644
--- a/packages/compiler-core/src/tokenizer.ts
+++ b/packages/compiler-core/src/tokenizer.ts
@@ -929,7 +929,7 @@ export default class Tokenizer {
this.buffer = input
while (this.index < this.buffer.length) {
const c = this.buffer.charCodeAt(this.index)
- if (c === CharCodes.NewLine) {
+ if (c === CharCodes.NewLine && this.state !== State.InEntity) {
this.newlines.push(this.index)
}
switch (this.state) {
diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts
index aeb96cc2b4a..9d8fd842935 100644
--- a/packages/compiler-core/src/transform.ts
+++ b/packages/compiler-core/src/transform.ts
@@ -37,7 +37,7 @@ import {
helperNameMap,
} from './runtimeHelpers'
import { isVSlot } from './utils'
-import { cacheStatic, isSingleElementRoot } from './transforms/cacheStatic'
+import { cacheStatic, getSingleElementRoot } from './transforms/cacheStatic'
import type { CompilerCompatOptions } from './compat/compatConfig'
// There are two types of transforms:
@@ -356,12 +356,12 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
if (children.length === 1) {
- const child = children[0]
+ const singleElementRootChild = getSingleElementRoot(root)
// if the single child is an element, turn it into a block.
- if (isSingleElementRoot(root, child) && child.codegenNode) {
+ if (singleElementRootChild && singleElementRootChild.codegenNode) {
// single element root is never hoisted so codegenNode will never be
// SimpleExpressionNode
- const codegenNode = child.codegenNode
+ const codegenNode = singleElementRootChild.codegenNode
if (codegenNode.type === NodeTypes.VNODE_CALL) {
convertToBlock(codegenNode, context)
}
@@ -370,7 +370,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
// - single , IfNode, ForNode: already blocks.
// - single text node: always patched.
// root codegen falls through via genNode()
- root.codegenNode = child
+ root.codegenNode = children[0]
}
} else if (children.length > 1) {
// root has multiple nodes - return a fragment block.
diff --git a/packages/compiler-core/src/transforms/cacheStatic.ts b/packages/compiler-core/src/transforms/cacheStatic.ts
index e5d67380640..239ee689a9f 100644
--- a/packages/compiler-core/src/transforms/cacheStatic.ts
+++ b/packages/compiler-core/src/transforms/cacheStatic.ts
@@ -41,20 +41,19 @@ export function cacheStatic(root: RootNode, context: TransformContext): void {
context,
// Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes.
- isSingleElementRoot(root, root.children[0]),
+ !!getSingleElementRoot(root),
)
}
-export function isSingleElementRoot(
+export function getSingleElementRoot(
root: RootNode,
- child: TemplateChildNode,
-): child is PlainElementNode | ComponentNode | TemplateNode {
- const { children } = root
- return (
- children.length === 1 &&
- child.type === NodeTypes.ELEMENT &&
- !isSlotOutlet(child)
- )
+): PlainElementNode | ComponentNode | TemplateNode | null {
+ const children = root.children.filter(x => x.type !== NodeTypes.COMMENT)
+ return children.length === 1 &&
+ children[0].type === NodeTypes.ELEMENT &&
+ !isSlotOutlet(children[0])
+ ? children[0]
+ : null
}
function walk(
diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts
index 0dca0ba9ab4..a639caf2cae 100644
--- a/packages/compiler-core/src/transforms/vFor.ts
+++ b/packages/compiler-core/src/transforms/vFor.ts
@@ -263,7 +263,7 @@ export function processFor(
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (forNode: ForNode) => (() => void) | undefined,
-) {
+): (() => void) | undefined {
if (!dir.exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc),
diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts
index 28625439a47..43296dcc9b6 100644
--- a/packages/compiler-core/src/transforms/vSlot.ts
+++ b/packages/compiler-core/src/transforms/vSlot.ts
@@ -222,7 +222,7 @@ export function buildSlots(
let prev
while (j--) {
prev = children[j]
- if (prev.type !== NodeTypes.COMMENT) {
+ if (prev.type !== NodeTypes.COMMENT && isNonWhitespaceContent(prev)) {
break
}
}
diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
index 2ed15ef5e62..5bc40d3fab5 100644
--- a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
+++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
@@ -6,7 +6,7 @@ exports[`stringify static html > eligible content (elements > 20) + non-eligible
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("", 20),
- _createElementVNode("div", { key: "1" }, "1", -1 /* HOISTED */),
+ _createElementVNode("div", { key: "1" }, "1", -1 /* CACHED */),
_createStaticVNode("", 20)
])))
}"
@@ -54,7 +54,7 @@ return function render(_ctx, _cache) {
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" })
- ], -1 /* HOISTED */)
+ ], -1 /* CACHED */)
])))
}"
`;
@@ -70,11 +70,27 @@ return function render(_ctx, _cache) {
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 })
- ], -1 /* HOISTED */)
+ ], -1 /* CACHED */)
])))
}"
`;
+exports[`stringify static html > should bail for comments 1`] = `
+"const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
+
+const _hoisted_1 = { class: "a" }
+
+return function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock(_Fragment, null, [
+ _createCommentVNode(" Comment 1 "),
+ _createElementVNode("div", _hoisted_1, [
+ _createCommentVNode(" Comment 2 "),
+ _cache[0] || (_cache[0] = _createStaticVNode("", 5))
+ ])
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+}"
+`;
+
exports[`stringify static html > should bail on bindings that are cached but not stringifiable 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
@@ -87,7 +103,7 @@ return function render(_ctx, _cache) {
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("img", { src: _imports_0_ })
- ], -1 /* HOISTED */)
+ ], -1 /* CACHED */)
])))
}"
`;
diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
index 79e6fc9c6e8..f58e207d6cf 100644
--- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
+++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts
@@ -491,6 +491,16 @@ describe('stringify static html', () => {
expect(code).toMatchSnapshot()
})
+ test('should bail for comments', () => {
+ const { code } = compileWithStringify(
+ `${repeat(
+ ``,
+ StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
+ )}
`,
+ )
+ expect(code).toMatchSnapshot()
+ })
+
test('should bail for