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( + `
hello
`, + ) + 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 = ` + + + + + ` + 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