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( + `
+
{{a}} {{b}}
+
`, + ).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 => {