diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vScope.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vScope.spec.ts.snap
new file mode 100644
index 000000000..c80c89364
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vScope.spec.ts.snap
@@ -0,0 +1,86 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: v-scope transform > complex expression 1`] = `
+"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock("div", null, [
+ ((a = _ctx.foo + _ctx.bar) => _createElementVNode("div", null, [
+ ((b = a + _ctx.baz) => _createElementVNode("span", null, [
+ _createTextVNode(_toDisplayString(b), 1 /* TEXT */)
+ ]))()
+ ]))(),
+ ((exp = _ctx.getExp()) => _createElementVNode("div", null, [
+ _createTextVNode(_toDisplayString(exp), 1 /* TEXT */)
+ ]))()
+ ]))
+}"
+`;
+
+exports[`compiler: v-scope transform > nested v-scope 1`] = `
+"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock("div", null, [
+ ((a = 1) => _createElementVNode("div", null, [
+ ((b = 1) => _createElementVNode("span", null, [
+ _createTextVNode(_toDisplayString(a) + _toDisplayString(b), 1 /* TEXT */)
+ ]))()
+ ]))()
+ ]))
+}"
+`;
+
+exports[`compiler: v-scope transform > ok v-if 1`] = `
+"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode } from "vue"
+
+export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock("div", null, [
+ (_ctx.ok)
+ ? ((a = true) => (_openBlock(), _createElementBlock("div", { key: 0 }, [
+ _createTextVNode(_toDisplayString(a), 1 /* TEXT */)
+ ])))()
+ : _createCommentVNode("v-if", true)
+ ]))
+}"
+`;
+
+exports[`compiler: v-scope transform > on v-for 1`] = `
+"import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode } from "vue"
+
+export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock("div", null, [
+ (_openBlock(), _createElementBlock(_Fragment, null, _renderList([1,2,3], (i) => {
+ return ((a = i+1) => _createElementVNode("div", null, [
+ _createTextVNode(_toDisplayString(a), 1 /* TEXT */)
+ ]))()
+ }), 64 /* STABLE_FRAGMENT */))
+ ]))
+}"
+`;
+
+exports[`compiler: v-scope transform > should work 1`] = `
+"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock("div", null, [
+ ((a = 1, b = 2) => _createElementVNode("div", null, [
+ _createTextVNode(_toDisplayString(a) + " " + _toDisplayString(b), 1 /* TEXT */)
+ ]))()
+ ]))
+}"
+`;
+
+exports[`compiler: v-scope transform > work with variable 1`] = `
+"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
+
+export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock("div", null, [
+ ((a = _ctx.msg) => _createElementVNode("div", null, [
+ ((b = a) => _createElementVNode("span", null, [
+ _createTextVNode(_toDisplayString(b), 1 /* TEXT */)
+ ]))()
+ ]))()
+ ]))
+}"
+`;
diff --git a/packages/compiler-core/__tests__/transforms/vScope.spec.ts b/packages/compiler-core/__tests__/transforms/vScope.spec.ts
new file mode 100644
index 000000000..302f2dbcb
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/vScope.spec.ts
@@ -0,0 +1,84 @@
+import { type CompilerOptions, baseCompile } from '../../src'
+
+describe('compiler: v-scope transform', () => {
+ function compile(content: string, options: CompilerOptions = {}) {
+ return baseCompile(`
${content}
`, {
+ mode: 'module',
+ prefixIdentifiers: true,
+ ...options,
+ }).code
+ }
+
+ test('should work', () => {
+ expect(
+ compile(
+ `
+ {{a}} {{b}}
+
`,
+ ),
+ ).toMatchSnapshot()
+ })
+
+ test('nested v-scope', () => {
+ expect(
+ compile(
+ `
+ {{ a }}{{ b }}
+
`,
+ ),
+ ).toMatchSnapshot()
+ })
+
+ test('work with variable', () => {
+ expect(
+ compile(
+ `
+ {{ b }}
+
`,
+ ),
+ ).toMatchSnapshot()
+ })
+
+ test('complex expression', () => {
+ expect(
+ compile(`
+
+ {{ b }}
+
+ {{ exp }}
+ `),
+ ).toMatchSnapshot()
+ })
+
+ test('on v-for', () => {
+ expect(
+ compile(`
+
+ {{ a }}
+
+ `),
+ ).toMatchSnapshot()
+ })
+
+ test('ok v-if', () => {
+ expect(
+ compile(`
+
+ {{ a }}
+
+ `),
+ ).toMatchSnapshot()
+ })
+
+ test('error', () => {
+ const onError = vi.fn()
+ expect(compile(`{{ a }}
`, { onError }))
+ expect(onError.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ [SyntaxError: Error parsing JavaScript expression: Unexpected token (1:5)],
+ ],
+ ]
+ `)
+ })
+})
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index 2d6df9d90..dbf4d9f7f 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -144,6 +144,7 @@ export interface PlainElementNode extends BaseElementNode {
| SimpleExpressionNode // when hoisted
| CacheExpression // when cached by v-once
| MemoExpression // when cached by v-memo
+ | CallExpression
| undefined
ssrCodegenNode?: TemplateLiteral
}
@@ -360,7 +361,7 @@ export type JSChildNode =
export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION
- callee: string | symbol
+ callee: string | symbol | FunctionExpression
arguments: (
| string
| symbol
diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts
index 6b4559fab..a9c6e70b8 100644
--- a/packages/compiler-core/src/codegen.ts
+++ b/packages/compiler-core/src/codegen.ts
@@ -886,11 +886,24 @@ function genNullableArgs(args: any[]): CallExpression['arguments'] {
// JavaScript
function genCallExpression(node: CallExpression, context: CodegenContext) {
const { push, helper, pure } = context
- const callee = isString(node.callee) ? node.callee : helper(node.callee)
+ let callee
+ if (isString(node.callee)) {
+ callee = node.callee
+ } else if (isSymbol(node.callee)) {
+ callee = helper(node.callee)
+ } else {
+ // anonymous function.
+ if (context.inSSR) push(';')
+ push(`(`)
+ genNode(node.callee, context)
+ push(`)`)
+ }
+
if (pure) {
push(PURE_ANNOTATION)
}
- push(callee + `(`, NewlineType.None, node)
+ callee && push(callee)
+ push(`(`, NewlineType.None, node)
genNodeList(node.arguments, context)
push(`)`)
}
diff --git a/packages/compiler-core/src/compile.ts b/packages/compiler-core/src/compile.ts
index a697c9d22..48e0d55e4 100644
--- a/packages/compiler-core/src/compile.ts
+++ b/packages/compiler-core/src/compile.ts
@@ -22,6 +22,7 @@ import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter'
import { ErrorCodes, createCompilerError, defaultOnError } from './errors'
import { transformMemo } from './transforms/vMemo'
+import { trackVScopeScopes, transformScope } from './transforms/vScope'
export type TransformPreset = [
NodeTransform[],
@@ -36,6 +37,7 @@ export function getBaseTransformPreset(
transformOnce,
transformIf,
transformMemo,
+ transformScope,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers
@@ -50,6 +52,7 @@ export function getBaseTransformPreset(
transformSlotOutlet,
transformElement,
trackSlotScopes,
+ trackVScopeScopes,
transformText,
],
{
diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts
index 29e5f6813..29d6e208f 100644
--- a/packages/compiler-core/src/index.ts
+++ b/packages/compiler-core/src/index.ts
@@ -59,6 +59,11 @@ export {
trackVForSlotScopes,
trackSlotScopes,
} from './transforms/vSlot'
+export {
+ transformScope,
+ trackVScopeScopes,
+ transformScopeExpression,
+} from './transforms/vScope'
export {
transformElement,
resolveComponentType,
diff --git a/packages/compiler-core/src/transforms/cacheStatic.ts b/packages/compiler-core/src/transforms/cacheStatic.ts
index 239ee689a..3bbbd1aec 100644
--- a/packages/compiler-core/src/transforms/cacheStatic.ts
+++ b/packages/compiler-core/src/transforms/cacheStatic.ts
@@ -412,7 +412,7 @@ function getConstantTypeOfHelperCall(
): ConstantTypes {
if (
value.type === NodeTypes.JS_CALL_EXPRESSION &&
- !isString(value.callee) &&
+ isSymbol(value.callee) &&
allowHoistedHelperSet.has(value.callee)
) {
const arg = value.arguments[0] as JSChildNode
diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts
index a639caf2c..7742ac44a 100644
--- a/packages/compiler-core/src/transforms/vFor.ts
+++ b/packages/compiler-core/src/transforms/vFor.ts
@@ -49,6 +49,7 @@ import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { PatchFlags } from '@vue/shared'
import { transformBindShorthand } from './vBind'
+import { transformScopeExpression } from './vScope'
export const transformFor: NodeTransform = createStructuralDirectiveTransform(
'for',
@@ -62,6 +63,7 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
]) as ForRenderListExpression
const isTemplate = isTemplateNode(node)
const memo = findDir(node, 'memo')
+ const vScope = findDir(node, 'scope')
const keyProp = findProp(node, `key`, false, true)
const isDirKey = keyProp && keyProp.type === NodeTypes.DIRECTIVE
if (isDirKey && !keyProp.exp) {
@@ -244,10 +246,22 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
// increment cache count
context.cached.push(null)
} else {
+ let child
+ if (vScope) {
+ child = createCallExpression(
+ createFunctionExpression(
+ transformScopeExpression(vScope.exp!),
+ childBlock,
+ ),
+ )
+ } else {
+ child = childBlock
+ }
+
renderExp.arguments.push(
createFunctionExpression(
createForLoopParams(forNode.parseResult),
- childBlock,
+ child,
true /* force newline */,
) as ForIteratorExpression,
)
diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts
index 54c505407..cdd582b12 100644
--- a/packages/compiler-core/src/transforms/vIf.ts
+++ b/packages/compiler-core/src/transforms/vIf.ts
@@ -8,16 +8,19 @@ import {
type AttributeNode,
type BlockCodegenNode,
type CacheExpression,
+ type CallExpression,
ConstantTypes,
type DirectiveNode,
type ElementNode,
ElementTypes,
+ type FunctionExpression,
type IfBranchNode,
type IfConditionalExpression,
type IfNode,
type MemoExpression,
NodeTypes,
type SimpleExpressionNode,
+ type VNodeCall,
convertToBlock,
createCallExpression,
createConditionalExpression,
@@ -293,7 +296,12 @@ function createChildrenCodegenNode(
const ret = (firstChild as ElementNode).codegenNode as
| BlockCodegenNode
| MemoExpression
- const vnodeCall = getMemoedVNodeCall(ret)
+
+ const vnodeCall = findDir(firstChild, 'scope')
+ ? (((ret as CallExpression).callee as FunctionExpression)
+ .returns as VNodeCall)
+ : getMemoedVNodeCall(ret)
+
// Change createVNode to createBlock.
if (vnodeCall.type === NodeTypes.VNODE_CALL) {
convertToBlock(vnodeCall, context)
diff --git a/packages/compiler-core/src/transforms/vScope.ts b/packages/compiler-core/src/transforms/vScope.ts
new file mode 100644
index 000000000..b1bd7e36b
--- /dev/null
+++ b/packages/compiler-core/src/transforms/vScope.ts
@@ -0,0 +1,71 @@
+import type { NodeTransform } from '../transform'
+import { findDir } from '../utils'
+import {
+ type ExpressionNode,
+ NodeTypes,
+ type PlainElementNode,
+ type SimpleExpressionNode,
+ createCallExpression,
+ createFunctionExpression,
+ createSimpleExpression,
+} from '../ast'
+import { stringifyExpression } from './transformExpression'
+
+const seen = new WeakSet()
+const extractKeyValueRE = /(\w+)\s*:\s*"*(.*?)"*\s*[,}\n]/g
+
+export const transformScope: NodeTransform = (node, context) => {
+ if (node.type === NodeTypes.ELEMENT) {
+ const dir = findDir(node, 'scope')
+ if (!dir || seen.has(node)) {
+ return
+ }
+ seen.add(node)
+ return () => {
+ const codegenNode =
+ node.codegenNode ||
+ (context.currentNode as PlainElementNode).codegenNode
+ if (codegenNode && codegenNode.type === NodeTypes.VNODE_CALL) {
+ node.codegenNode = createCallExpression(
+ createFunctionExpression(
+ transformScopeExpression(dir.exp!),
+ codegenNode,
+ ),
+ )
+ }
+ }
+ }
+}
+
+export function transformScopeExpression(
+ exp: string | ExpressionNode,
+): ExpressionNode[] {
+ const params: SimpleExpressionNode[] = []
+ const rExpString = stringifyExpression(exp)
+ let match
+ while ((match = extractKeyValueRE.exec(rExpString))) {
+ params.push(createSimpleExpression(`${match[1]} = ${match[2]}`))
+ }
+ return params
+}
+
+export const trackVScopeScopes: NodeTransform = (node, context) => {
+ if (node.type === NodeTypes.ELEMENT) {
+ const vScope = findDir(node, 'scope')
+ if (vScope) {
+ const keys: string[] = []
+ let match
+ while ((match = extractKeyValueRE.exec(vScope.exp!.loc.source))) {
+ keys.push(match[1])
+ }
+ if (!__BROWSER__ && context.prefixIdentifiers) {
+ keys.forEach(context.addIdentifiers)
+ }
+ return () => {
+ if (!__BROWSER__ && context.prefixIdentifiers) {
+ keys.forEach(context.removeIdentifiers)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index b49d70bb2..6ef25332d 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -37,7 +37,7 @@ import {
TO_HANDLERS,
WITH_MEMO,
} from './runtimeHelpers'
-import { NOOP, isObject, isString } from '@vue/shared'
+import { NOOP, isObject, isString, isSymbol } from '@vue/shared'
import type { PropsExpression } from './transforms/transformElement'
import { parseExpression } from '@babel/parser'
import type { Expression, Node } from '@babel/types'
@@ -373,7 +373,7 @@ function getUnnormalizedProps(
props.type === NodeTypes.JS_CALL_EXPRESSION
) {
const callee = props.callee
- if (!isString(callee) && propsHelperSet.has(callee)) {
+ if (isSymbol(callee) && propsHelperSet.has(callee)) {
return getUnnormalizedProps(
props.arguments[0] as PropsExpression,
callPath.concat(props),
diff --git a/packages/compiler-ssr/__tests__/ssrVScope.spec.ts b/packages/compiler-ssr/__tests__/ssrVScope.spec.ts
new file mode 100644
index 000000000..3428ea650
--- /dev/null
+++ b/packages/compiler-ssr/__tests__/ssrVScope.spec.ts
@@ -0,0 +1,66 @@
+import { compile } from '../src'
+
+describe('ssr: v-scope', () => {
+ test('basic', () => {
+ expect(compile(`{{a}}
`).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ ;((a = 1) => {
+ _push(\`\${_ssrInterpolate(a)}\`)
+ })()
+ _push(\`
\`)
+ }"
+ `)
+ })
+
+ test('nested', () => {
+ expect(
+ compile(
+ ``,
+ ).code,
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ ;((a = 1) => {
+ _push(\`
\`)
+ ;((b = 2) => {
+ _push(\`\${_ssrInterpolate(a)} \${_ssrInterpolate(b)}\`)
+ })()
+ _push(\`
\`)
+ })()
+ _push(\`
\`)
+ }"
+ `)
+ })
+
+ test('ok v-if', () => {
+ expect(
+ compile(`
+
+ {{ a }}
+
+ `).code,
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.ok) {
+ _push(\`\`)
+ ;((a = true) => {
+ _push(\`\${_ssrInterpolate(a)}\`)
+ })()
+ _push(\`
\`)
+ } else {
+ _push(\`\`)
+ }
+ }"
+ `)
+ })
+})
diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts
index f8a686555..16492172d 100644
--- a/packages/compiler-ssr/src/index.ts
+++ b/packages/compiler-ssr/src/index.ts
@@ -8,6 +8,7 @@ import {
parserOptions,
trackSlotScopes,
trackVForSlotScopes,
+ trackVScopeScopes,
transform,
transformBind,
transformExpression,
@@ -65,6 +66,7 @@ export function compile(
ssrTransformElement,
ssrTransformComponent,
trackSlotScopes,
+ trackVScopeScopes,
transformStyle,
...(options.nodeTransforms || []), // user transforms
],
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts
index 4a12b0f7b..103a3e410 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts
@@ -24,6 +24,7 @@ import {
createCompilerError,
createCompoundExpression,
createConditionalExpression,
+ createFunctionExpression,
createInterpolation,
createSequenceExpression,
createSimpleExpression,
@@ -32,6 +33,7 @@ import {
hasDynamicKeyVBind,
isStaticArgOf,
isStaticExp,
+ transformScopeExpression,
} from '@vue/compiler-dom'
import {
NO,
@@ -56,6 +58,7 @@ import {
import {
type SSRTransformContext,
processChildren,
+ processChildrenAsStatement,
} from '../ssrCodegenTransform'
// for directives with children overwrite (e.g. v-html & v-text), we need to
@@ -460,7 +463,17 @@ export function ssrProcessElement(
if (rawChildren) {
context.pushStringPart(rawChildren)
} else if (node.children.length) {
- processChildren(node, context)
+ // process v-scope
+ const dir = findDir(node, 'scope')
+ if (dir) {
+ const scopeFn = createFunctionExpression(
+ transformScopeExpression(dir.exp!),
+ )
+ scopeFn.body = processChildrenAsStatement(node, context)
+ context.pushStatement(createCallExpression(scopeFn))
+ } else {
+ processChildren(node, context)
+ }
}
if (!isVoidTag(node.tag)) {
diff --git a/packages/shared/src/general.ts b/packages/shared/src/general.ts
index 9c6a23132..b071615d6 100644
--- a/packages/shared/src/general.ts
+++ b/packages/shared/src/general.ts
@@ -90,7 +90,7 @@ export const isReservedProp: (key: string) => boolean = /*@__PURE__*/ makeMap(
export const isBuiltInDirective: (key: string) => boolean =
/*@__PURE__*/ makeMap(
- 'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo',
+ 'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo,scope',
)
const cacheStringFunction = string>(fn: T): T => {