This commit is contained in:
edison 2025-06-19 00:14:11 +08:00 committed by GitHub
commit 929ce1a793
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 376 additions and 10 deletions

View File

@ -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 */)
]))()
]))()
]))
}"
`;

View File

@ -0,0 +1,84 @@
import { type CompilerOptions, baseCompile } from '../../src'
describe('compiler: v-scope transform', () => {
function compile(content: string, options: CompilerOptions = {}) {
return baseCompile(`<div>${content}</div>`, {
mode: 'module',
prefixIdentifiers: true,
...options,
}).code
}
test('should work', () => {
expect(
compile(
`<div v-scope="{ a:1, b:2 }">
{{a}} {{b}}
</div>`,
),
).toMatchSnapshot()
})
test('nested v-scope', () => {
expect(
compile(
`<div v-scope="{ a:1 }">
<span v-scope="{ b:1 }">{{ a }}{{ b }}</span>
</div>`,
),
).toMatchSnapshot()
})
test('work with variable', () => {
expect(
compile(
`<div v-scope="{ a:msg }">
<span v-scope="{ b:a }">{{ b }}</span>
</div>`,
),
).toMatchSnapshot()
})
test('complex expression', () => {
expect(
compile(`
<div v-scope="{ a:foo + bar }">
<span v-scope="{ b:a + baz }">{{ b }}</span>
</div>
<div v-scope="{ exp:getExp() }">{{ exp }}</div>
`),
).toMatchSnapshot()
})
test('on v-for', () => {
expect(
compile(`
<div v-for="i in [1,2,3]" v-scope="{ a:i+1 }">
{{ a }}
</div>
`),
).toMatchSnapshot()
})
test('ok v-if', () => {
expect(
compile(`
<div v-if="ok" v-scope="{ a:true }" >
{{ a }}
</div>
`),
).toMatchSnapshot()
})
test('error', () => {
const onError = vi.fn()
expect(compile(`<div v-scope="{ a:, b:1 }">{{ a }}</div>`, { onError }))
expect(onError.mock.calls).toMatchInlineSnapshot(`
[
[
[SyntaxError: Error parsing JavaScript expression: Unexpected token (1:5)],
],
]
`)
})
})

View File

@ -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

View File

@ -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(`)`)
}

View File

@ -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,
],
{

View File

@ -59,6 +59,11 @@ export {
trackVForSlotScopes,
trackSlotScopes,
} from './transforms/vSlot'
export {
transformScope,
trackVScopeScopes,
transformScopeExpression,
} from './transforms/vScope'
export {
transformElement,
resolveComponentType,

View File

@ -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

View File

@ -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,
)

View File

@ -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)

View File

@ -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)
}
}
}
}
}

View File

@ -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),

View File

@ -0,0 +1,66 @@
import { compile } from '../src'
describe('ssr: v-scope', () => {
test('basic', () => {
expect(compile(`<div v-scope="{ a:1 }">{{a}}</div>`).code)
.toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>\`)
;((a = 1) => {
_push(\`\${_ssrInterpolate(a)}\`)
})()
_push(\`</div>\`)
}"
`)
})
test('nested', () => {
expect(
compile(
`<div v-scope="{ a:1 }">
<div v-scope="{ b:2 }">{{a}} {{b}}</div>
</div>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>\`)
;((a = 1) => {
_push(\`<div>\`)
;((b = 2) => {
_push(\`\${_ssrInterpolate(a)} \${_ssrInterpolate(b)}\`)
})()
_push(\`</div>\`)
})()
_push(\`</div>\`)
}"
`)
})
test('ok v-if', () => {
expect(
compile(`
<div v-if="ok" v-scope="{ a:true }" >
{{ a }}
</div>
`).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.ok) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>\`)
;((a = true) => {
_push(\`\${_ssrInterpolate(a)}\`)
})()
_push(\`</div>\`)
} else {
_push(\`<!---->\`)
}
}"
`)
})
})

View File

@ -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
],

View File

@ -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)) {

View File

@ -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 = <T extends (str: string) => string>(fn: T): T => {