diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 9816f7f5257..95bc65f357b 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -31,4 +31,4 @@ jobs: - name: Run prettier run: pnpm run format - - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef + - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d9a2f75038..d490aa19d95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,31 @@ +## [3.5.17](https://github.com/vuejs/core/compare/v3.5.16...v3.5.17) (2025-06-18) + + +### Bug Fixes + +* **compat:** allow v-model built in modifiers on component ([#12654](https://github.com/vuejs/core/issues/12654)) ([cb14b86](https://github.com/vuejs/core/commit/cb14b860f150c4a83bcd52cd26096b7a5aa3a2bf)), closes [#12652](https://github.com/vuejs/core/issues/12652) +* **compile-sfc:** handle mapped types work with omit and pick ([#12648](https://github.com/vuejs/core/issues/12648)) ([4eb46e4](https://github.com/vuejs/core/commit/4eb46e443f1878199755cb73d481d318a9714392)), closes [#12647](https://github.com/vuejs/core/issues/12647) +* **compiler-core:** do not increase newlines in `InEntity` state ([#13362](https://github.com/vuejs/core/issues/13362)) ([f05a8d6](https://github.com/vuejs/core/commit/f05a8d613bd873b811cfdb9979ccac8382dba322)) +* **compiler-core:** ignore whitespace when matching adjacent v-if ([#12321](https://github.com/vuejs/core/issues/12321)) ([10ebcef](https://github.com/vuejs/core/commit/10ebcef8c870dbc042b0ea49b1424b2e8f692145)), closes [#9173](https://github.com/vuejs/core/issues/9173) +* **compiler-core:** prevent comments from blocking static node hoisting ([#13345](https://github.com/vuejs/core/issues/13345)) ([55dad62](https://github.com/vuejs/core/commit/55dad625acd9e9ddd5a933d5e323ecfdec1a612f)), closes [#13344](https://github.com/vuejs/core/issues/13344) +* **compiler-sfc:** improved type resolution for function type aliases ([#13452](https://github.com/vuejs/core/issues/13452)) ([f3479aa](https://github.com/vuejs/core/commit/f3479aac9625f4459e650d1c0a70e73863147903)), closes [#13444](https://github.com/vuejs/core/issues/13444) +* **custom-element:** ensure configureApp is applied to async component ([#12607](https://github.com/vuejs/core/issues/12607)) ([5ba1afb](https://github.com/vuejs/core/commit/5ba1afba09c3ea56c1c17484f5d8aeae210ce52a)), closes [#12448](https://github.com/vuejs/core/issues/12448) +* **custom-element:** prevent injecting child styles if shadowRoot is false ([#12769](https://github.com/vuejs/core/issues/12769)) ([73055d8](https://github.com/vuejs/core/commit/73055d8d9578d485e3fe846726b50666e1aa56f5)), closes [#12630](https://github.com/vuejs/core/issues/12630) +* **reactivity:** add `__v_skip` flag to `Dep` to prevent reactive conversion ([#12804](https://github.com/vuejs/core/issues/12804)) ([e8d8f5f](https://github.com/vuejs/core/commit/e8d8f5f604e821acc46b4200d5b06979c05af1c2)), closes [#12803](https://github.com/vuejs/core/issues/12803) +* **runtime-core:** unset old ref during patching when new ref is absent ([#12900](https://github.com/vuejs/core/issues/12900)) ([47ddf98](https://github.com/vuejs/core/commit/47ddf986021dff8de68b0da72787e53a6c19de4c)), closes [#12898](https://github.com/vuejs/core/issues/12898) +* **slots:** make cache indexes marker non-enumerable ([#13469](https://github.com/vuejs/core/issues/13469)) ([919c447](https://github.com/vuejs/core/commit/919c44744bba1f0c661c87d2059c3b429611aa7e)), closes [#13468](https://github.com/vuejs/core/issues/13468) +* **ssr:** handle initial selected state for select with v-model + v-for/v-if option ([#13487](https://github.com/vuejs/core/issues/13487)) ([1552095](https://github.com/vuejs/core/commit/15520954f9f1c7f834175938a50dba5d4be0e6c4)), closes [#13486](https://github.com/vuejs/core/issues/13486) +* **types:** typo of `vOnce` and `vSlot` ([#13343](https://github.com/vuejs/core/issues/13343)) ([762fae4](https://github.com/vuejs/core/commit/762fae4b57ad60602e5c84465a3bff562785b314)) + + + ## [3.5.16](https://github.com/vuejs/core/compare/v3.5.15...v3.5.16) (2025-05-29) ### 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..b0404823f43 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, - "version": "3.5.16", - "packageManager": "pnpm@10.11.0", + "version": "3.5.17", + "packageManager": "pnpm@10.12.1", "type": "module", "scripts": { "dev": "node scripts/dev.js", @@ -65,13 +65,13 @@ "@babel/parser": "catalog:", "@babel/types": "catalog:", "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "5.0.4", - "@swc/core": "^1.11.29", + "@swc/core": "^1.12.1", "@types/hash-sum": "^1.0.2", - "@types/node": "^22.15.21", + "@types/node": "^22.15.31", "@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", @@ -97,7 +97,7 @@ "pug": "^3.0.3", "puppeteer": "~24.9.0", "rimraf": "^6.0.1", - "rollup": "^4.41.1", + "rollup": "^4.43.0", "rollup-plugin-dts": "^6.2.1", "rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-polyfill-node": "^0.13.0", diff --git a/packages-private/sfc-playground/package.json b/packages-private/sfc-playground/package.json index 418b43d2281..731ff944254 100644 --- a/packages-private/sfc-playground/package.json +++ b/packages-private/sfc-playground/package.json @@ -13,7 +13,7 @@ "vite": "catalog:" }, "dependencies": { - "@vue/repl": "^4.5.1", + "@vue/repl": "^4.6.1", "file-saver": "^2.0.5", "jszip": "^3.10.1", "vue": "workspace:*" diff --git a/packages-private/sfc-playground/src/App.vue b/packages-private/sfc-playground/src/App.vue index 740d770260c..455137ba6f6 100644 --- a/packages-private/sfc-playground/src/App.vue +++ b/packages-private/sfc-playground/src/App.vue @@ -141,6 +141,8 @@ onMounted(() => { :editorOptions="{ autoSaveText: false }" :store="store" :showCompileOutput="true" + :showSsrOutput="useSSRMode" + :showOpenSourceMap="true" :autoResize="true" :clearConsole="false" :preview-options="{ 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/package.json b/packages/compiler-core/package.json index 48f33e4de60..a3c188a6699 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.5.16", + "version": "3.5.17", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", 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