Skip to content

feat: v-scope #7218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 */)
]))()
]))()
]))
}"
`;
84 changes: 84 additions & 0 deletions packages/compiler-core/__tests__/transforms/vScope.spec.ts
Original file line number Diff line number Diff line change
@@ -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)],
],
]
`)
})
})
3 changes: 2 additions & 1 deletion packages/compiler-core/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions packages/compiler-core/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,11 +884,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(`)`)
}
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler-core/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -36,6 +37,7 @@ export function getBaseTransformPreset(
transformOnce,
transformIf,
transformMemo,
transformScope,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers
Expand All @@ -50,6 +52,7 @@ export function getBaseTransformPreset(
transformSlotOutlet,
transformElement,
trackSlotScopes,
trackVScopeScopes,
transformText,
],
{
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export {
trackVForSlotScopes,
trackSlotScopes,
} from './transforms/vSlot'
export {
transformScope,
trackVScopeScopes,
transformScopeExpression,
} from './transforms/vScope'
export {
transformElement,
resolveComponentType,
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-core/src/transforms/cacheStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,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
Expand Down
16 changes: 15 additions & 1 deletion packages/compiler-core/src/transforms/vFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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)
if (keyProp && keyProp.type === NodeTypes.DIRECTIVE && !keyProp.exp) {
// resolve :key shorthand #10882
Expand Down Expand Up @@ -234,10 +236,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,
)
Expand Down
10 changes: 9 additions & 1 deletion packages/compiler-core/src/transforms/vIf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Loading