refactor(compiler): split slot / slot outlet / slot scope handling into separate transforms

This commit is contained in:
Evan You 2019-09-28 00:19:24 -04:00
parent 6377af483b
commit 6461b3853e
13 changed files with 532 additions and 433 deletions

View File

@ -8,7 +8,9 @@ return function render() {
id: \\"foo\\", id: \\"foo\\",
[prop]: bar, [prop]: bar,
[foo + bar]: bar [foo + bar]: bar
}, [createVNode(\\"p\\", { \\"some-key\\": \\"foo\\" })], [ }, [
createVNode(\\"p\\", { \\"some-key\\": \\"foo\\" })
], [
foo, foo,
createVNode(\\"p\\") createVNode(\\"p\\")
]) ])

View File

@ -14,7 +14,9 @@ return function render() {
? _createVNode(\\"div\\", 0, \\"yes\\") ? _createVNode(\\"div\\", 0, \\"yes\\")
: \\"no\\", : \\"no\\",
_renderList(list, (value, index) => { _renderList(list, (value, index) => {
return _createVNode(\\"div\\", 0, [_createVNode(\\"span\\", 0, _toString(value + index))]) return _createVNode(\\"div\\", 0, [
_createVNode(\\"span\\", 0, _toString(value + index))
])
}) })
]) ])
} }
@ -35,7 +37,9 @@ return function render() {
? createVNode(\\"div\\", 0, \\"yes\\") ? createVNode(\\"div\\", 0, \\"yes\\")
: \\"no\\", : \\"no\\",
renderList(_ctx.list, (value, index) => { renderList(_ctx.list, (value, index) => {
return createVNode(\\"div\\", 0, [createVNode(\\"span\\", 0, toString(value + index))]) return createVNode(\\"div\\", 0, [
createVNode(\\"span\\", 0, toString(value + index))
])
}) })
]) ])
}" }"
@ -55,7 +59,9 @@ export default function render() {
? createVNode(\\"div\\", 0, \\"yes\\") ? createVNode(\\"div\\", 0, \\"yes\\")
: \\"no\\", : \\"no\\",
_renderList(_ctx.list, (value, index) => { _renderList(_ctx.list, (value, index) => {
return createVNode(\\"div\\", 0, [createVNode(\\"span\\", 0, _toString(value + index))]) return createVNode(\\"div\\", 0, [
createVNode(\\"span\\", 0, _toString(value + index))
])
}) })
]) ])
}" }"

View File

@ -553,7 +553,9 @@ describe('compiler: codegen', () => {
id: "foo", id: "foo",
[prop]: bar, [prop]: bar,
[foo + bar]: bar [foo + bar]: bar
}, [${CREATE_VNODE}("p", { "some-key": "foo" })], [ }, [
${CREATE_VNODE}("p", { "some-key": "foo" })
], [
foo, foo,
${CREATE_VNODE}("p") ${CREATE_VNODE}("p")
])`) ])`)

View File

@ -0,0 +1,324 @@
import {
CompilerOptions,
parse,
transform,
ElementNode,
NodeTypes
} from '../../src'
import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn'
import { transformBind } from '../../src/transforms/vBind'
import { transformExpression } from '../../src/transforms/transformExpression'
import { RENDER_SLOT } from '../../src/runtimeConstants'
import { transformSlotOutlet } from '../../src/transforms/transfromSlotOutlet'
function parseWithSlots(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [
...(options.prefixIdentifiers ? [transformExpression] : []),
transformSlotOutlet,
transformElement
],
directiveTransforms: {
on: transformOn,
bind: transformBind
},
...options
})
return ast
}
describe('compiler: transform <slot> outlets', () => {
test('default slot outlet', () => {
const ast = parseWithSlots(`<slot/>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [`$slots.default`]
})
})
test('statically named slot outlet', () => {
const ast = parseWithSlots(`<slot name="foo" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [`$slots.foo`]
})
})
test('statically named slot outlet w/ name that needs quotes', () => {
const ast = parseWithSlots(`<slot name="foo-bar" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [`$slots["foo-bar"]`]
})
})
test('dynamically named slot outlet', () => {
const ast = parseWithSlots(`<slot :name="foo" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
{
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`$slots[`,
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `foo`,
isStatic: false
},
`]`
]
}
]
})
})
test('dynamically named slot outlet w/ prefixIdentifiers: true', () => {
const ast = parseWithSlots(`<slot :name="foo + bar" />`, {
prefixIdentifiers: true
})
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_SLOT,
arguments: [
{
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`_ctx.$slots[`,
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.foo`,
isStatic: false
},
` + `,
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.bar`,
isStatic: false
},
`]`
]
}
]
})
})
test('default slot outlet with props', () => {
const ast = parseWithSlots(`<slot foo="bar" :baz="qux" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.default`,
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: true
}
},
{
key: {
content: `baz`,
isStatic: true
},
value: {
content: `qux`,
isStatic: false
}
}
]
}
]
})
})
test('statically named slot outlet with props', () => {
const ast = parseWithSlots(`<slot name="foo" foo="bar" :baz="qux" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.foo`,
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
// props should not include name
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: true
}
},
{
key: {
content: `baz`,
isStatic: true
},
value: {
content: `qux`,
isStatic: false
}
}
]
}
]
})
})
test('dynamically named slot outlet with props', () => {
const ast = parseWithSlots(`<slot :name="foo" foo="bar" :baz="qux" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
{
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$slots[`, { content: `foo` }, `]`]
},
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
// props should not include name
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: true
}
},
{
key: {
content: `baz`,
isStatic: true
},
value: {
content: `qux`,
isStatic: false
}
}
]
}
]
})
})
test('default slot outlet with fallback', () => {
const ast = parseWithSlots(`<slot><div/></slot>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.default`,
`{}`,
[
{
type: NodeTypes.ELEMENT,
tag: `div`
}
]
]
})
})
test('named slot outlet with fallback', () => {
const ast = parseWithSlots(`<slot name="foo"><div/></slot>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.foo`,
`{}`,
[
{
type: NodeTypes.ELEMENT,
tag: `div`
}
]
]
})
})
test('default slot outlet with props & fallback', () => {
const ast = parseWithSlots(`<slot :foo="bar"><div/></slot>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.default`,
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: false
}
}
]
},
[
{
type: NodeTypes.ELEMENT,
tag: `div`
}
]
]
})
})
test('named slot outlet with props & fallback', () => {
const ast = parseWithSlots(`<slot name="foo" :foo="bar"><div/></slot>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.foo`,
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: false
}
}
]
},
[
{
type: NodeTypes.ELEMENT,
tag: `div`
}
]
]
})
})
})

View File

@ -1,23 +1,17 @@
import { import { CompilerOptions, parse, transform, generate } from '../../src'
CompilerOptions,
parse,
transform,
ElementNode,
NodeTypes,
generate
} from '../../src'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
import { transformBind } from '../../src/transforms/vBind' import { transformBind } from '../../src/transforms/vBind'
import { transformExpression } from '../../src/transforms/transformExpression' import { transformExpression } from '../../src/transforms/transformExpression'
import { RENDER_SLOT } from '../../src/runtimeConstants' import { trackSlotScopes } from '../../src/transforms/vSlot'
function parseWithSlots(template: string, options: CompilerOptions = {}) { function parseWithSlots(template: string, options: CompilerOptions = {}) {
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
nodeTransforms: [ nodeTransforms: [
...(options.prefixIdentifiers ? [transformExpression] : []), ...(options.prefixIdentifiers
// slot transform is part of transformElement ? [transformExpression, trackSlotScopes]
: []),
transformElement transformElement
], ],
directiveTransforms: { directiveTransforms: {
@ -29,302 +23,19 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
return ast return ast
} }
describe('compiler: transform slots', () => { describe('compiler: transform component slots', () => {
test('default slot outlet', () => {
const ast = parseWithSlots(`<slot/>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [`$slots.default`]
})
})
test('statically named slot outlet', () => {
const ast = parseWithSlots(`<slot name="foo" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [`$slots.foo`]
})
})
test('statically named slot outlet w/ name that needs quotes', () => {
const ast = parseWithSlots(`<slot name="foo-bar" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [`$slots["foo-bar"]`]
})
})
test('dynamically named slot outlet', () => {
const ast = parseWithSlots(`<slot :name="foo" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
{
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`$slots[`,
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `foo`,
isStatic: false
},
`]`
]
}
]
})
})
test('dynamically named slot outlet w/ prefixIdentifiers: true', () => {
const ast = parseWithSlots(`<slot :name="foo + bar" />`, {
prefixIdentifiers: true
})
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_SLOT,
arguments: [
{
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
`_ctx.$slots[`,
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.foo`,
isStatic: false
},
` + `,
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_ctx.bar`,
isStatic: false
},
`]`
]
}
]
})
})
test('default slot outlet with props', () => {
const ast = parseWithSlots(`<slot foo="bar" :baz="qux" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.default`,
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: true
}
},
{
key: {
content: `baz`,
isStatic: true
},
value: {
content: `qux`,
isStatic: false
}
}
]
}
]
})
})
test('statically named slot outlet with props', () => {
const ast = parseWithSlots(`<slot name="foo" foo="bar" :baz="qux" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.foo`,
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
// props should not include name
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: true
}
},
{
key: {
content: `baz`,
isStatic: true
},
value: {
content: `qux`,
isStatic: false
}
}
]
}
]
})
})
test('dynamically named slot outlet with props', () => {
const ast = parseWithSlots(`<slot :name="foo" foo="bar" :baz="qux" />`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
{
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$slots[`, { content: `foo` }, `]`]
},
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
// props should not include name
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: true
}
},
{
key: {
content: `baz`,
isStatic: true
},
value: {
content: `qux`,
isStatic: false
}
}
]
}
]
})
})
test('default slot outlet with fallback', () => {
const ast = parseWithSlots(`<slot><div/></slot>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.default`,
`{}`,
[
{
type: NodeTypes.ELEMENT,
tag: `div`
}
]
]
})
})
test('named slot outlet with fallback', () => {
const ast = parseWithSlots(`<slot name="foo"><div/></slot>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.foo`,
`{}`,
[
{
type: NodeTypes.ELEMENT,
tag: `div`
}
]
]
})
})
test('default slot outlet with props & fallback', () => {
const ast = parseWithSlots(`<slot :foo="bar"><div/></slot>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.default`,
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: false
}
}
]
},
[
{
type: NodeTypes.ELEMENT,
tag: `div`
}
]
]
})
})
test('named slot outlet with props & fallback', () => {
const ast = parseWithSlots(`<slot name="foo" :foo="bar"><div/></slot>`)
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${RENDER_SLOT}`,
arguments: [
`$slots.foo`,
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: {
content: `foo`,
isStatic: true
},
value: {
content: `bar`,
isStatic: false
}
}
]
},
[
{
type: NodeTypes.ELEMENT,
tag: `div`
}
]
]
})
})
test('generate slot', () => { test('generate slot', () => {
const ast = parseWithSlots(`<Comp><div/></Comp>`) const ast = parseWithSlots(
const { code } = generate(ast) `
<Comp>
<Comp v-slot="{ dur }">
hello {{ dur }}
</Comp>
</Comp>
`,
{ prefixIdentifiers: true }
)
const { code } = generate(ast, { prefixIdentifiers: true })
console.log(code) console.log(code)
}) })
}) })

View File

@ -117,6 +117,9 @@ export interface SimpleExpressionNode extends Node {
type: NodeTypes.SIMPLE_EXPRESSION type: NodeTypes.SIMPLE_EXPRESSION
content: string content: string
isStatic: boolean isStatic: boolean
// an expression parsed as the params of a function will track
// the identifiers declared inside the function body.
identifiers?: string[]
} }
export interface InterpolationNode extends Node { export interface InterpolationNode extends Node {
@ -128,6 +131,9 @@ export interface InterpolationNode extends Node {
export interface CompoundExpressionNode extends Node { export interface CompoundExpressionNode extends Node {
type: NodeTypes.COMPOUND_EXPRESSION type: NodeTypes.COMPOUND_EXPRESSION
children: (SimpleExpressionNode | string)[] children: (SimpleExpressionNode | string)[]
// an expression parsed as the params of a function will track
// the identifiers declared inside the function body.
identifiers?: string[]
} }
export interface IfNode extends Node { export interface IfNode extends Node {

View File

@ -5,12 +5,14 @@ import { RootNode } from './ast'
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
import { transformIf } from './transforms/vIf' import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor' import { transformFor } from './transforms/vFor'
import { transformExpression } from './transforms/transformExpression'
import { transformStyle } from './transforms/transformStyle'
import { transformSlotOutlet } from './transforms/transfromSlotOutlet'
import { transformElement } from './transforms/transformElement' import { transformElement } from './transforms/transformElement'
import { transformOn } from './transforms/vOn' import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind' import { transformBind } from './transforms/vBind'
import { transformExpression } from './transforms/transformExpression'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors' import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
import { transformStyle } from './transforms/transformStyle' import { trackSlotScopes } from './transforms/vSlot'
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
@ -41,8 +43,9 @@ export function baseCompile(
nodeTransforms: [ nodeTransforms: [
transformIf, transformIf,
transformFor, transformFor,
...(prefixIdentifiers ? [transformExpression] : []), ...(prefixIdentifiers ? [transformExpression, trackSlotScopes] : []),
transformStyle, transformStyle,
transformSlotOutlet,
transformElement, transformElement,
...(options.nodeTransforms || []) // user transforms ...(options.nodeTransforms || []) // user transforms
], ],

View File

@ -8,8 +8,7 @@ import {
Property, Property,
ExpressionNode, ExpressionNode,
createSimpleExpression, createSimpleExpression,
JSChildNode, JSChildNode
SimpleExpressionNode
} from './ast' } from './ast'
import { isString, isArray } from '@vue/shared' import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors' import { CompilerError, defaultOnError } from './errors'
@ -64,8 +63,8 @@ export interface TransformContext extends Required<TransformOptions> {
replaceNode(node: ChildNode): void replaceNode(node: ChildNode): void
removeNode(node?: ChildNode): void removeNode(node?: ChildNode): void
onNodeRemoved: () => void onNodeRemoved: () => void
addIdentifier(exp: SimpleExpressionNode): void addIdentifier(id: string): void
removeIdentifier(exp: SimpleExpressionNode): void removeIdentifier(id: string): void
hoist(exp: JSChildNode): ExpressionNode hoist(exp: JSChildNode): ExpressionNode
} }
@ -127,15 +126,15 @@ function createTransformContext(
context.parent.children.splice(removalIndex, 1) context.parent.children.splice(removalIndex, 1)
}, },
onNodeRemoved: () => {}, onNodeRemoved: () => {},
addIdentifier({ content }) { addIdentifier(id) {
const { identifiers } = context const { identifiers } = context
if (identifiers[content] === undefined) { if (identifiers[id] === undefined) {
identifiers[content] = 0 identifiers[id] = 0
} }
;(identifiers[content] as number)++ ;(identifiers[id] as number)++
}, },
removeIdentifier({ content }) { removeIdentifier(id) {
;(context.identifiers[content] as number)-- ;(context.identifiers[id] as number)--
}, },
hoist(exp) { hoist(exp) {
context.hoists.push(exp) context.hoists.push(exp)

View File

@ -27,7 +27,7 @@ import {
TO_HANDLERS TO_HANDLERS
} from '../runtimeConstants' } from '../runtimeConstants'
import { getInnerRange } from '../utils' import { getInnerRange } from '../utils'
import { buildSlotOutlet, buildSlots } from './vSlot' import { buildSlots } from './vSlot'
const toValidId = (str: string): string => str.replace(/[^\w]/g, '') const toValidId = (str: string): string => str.replace(/[^\w]/g, '')
@ -95,10 +95,7 @@ export const transformElement: NodeTransform = (node, context) => {
} else { } else {
node.codegenNode = vnode node.codegenNode = vnode
} }
} else if (node.tagType === ElementTypes.SLOT) {
buildSlotOutlet(node, context)
} }
// node.tagType can also be TEMPLATE, in which case nothing needs to be done
} }
} }

View File

@ -87,12 +87,12 @@ export function processExpression(
_parseScript || (_parseScript = require('meriyah').parseScript) _parseScript || (_parseScript = require('meriyah').parseScript)
const walk = _walk || (_walk = require('estree-walker').walk) const walk = _walk || (_walk = require('estree-walker').walk)
let ast let ast: any
// if the expression is supposed to be used in a function params position // if the expression is supposed to be used in a function params position
// we need to parse it differently. // we need to parse it differently.
const source = `(${node.content})${asParams ? `=>{}` : ``}` const source = `(${node.content})${asParams ? `=>{}` : ``}`
try { try {
ast = parseScript(source, { ranges: true }) as any ast = parseScript(source, { ranges: true })
} catch (e) { } catch (e) {
context.onError(e) context.onError(e)
return node return node
@ -139,11 +139,22 @@ export function processExpression(
parent.right === child parent.right === child
) )
) { ) {
knownIds[child.name] = true const { name } = child
if (
(node as any)._scopeIds &&
(node as any)._scopeIds.has(name)
) {
return
}
if (name in knownIds) {
knownIds[name]++
} else {
knownIds[name] = 1
}
;( ;(
(node as any)._scopeIds || (node as any)._scopeIds ||
((node as any)._scopeIds = new Set()) ((node as any)._scopeIds = new Set())
).add(child.name) ).add(name)
} }
} }
}) })
@ -151,9 +162,12 @@ export function processExpression(
} }
}, },
leave(node: any) { leave(node: any) {
if (node._scopeIds) { if (node !== ast.body[0].expression && node._scopeIds) {
node._scopeIds.forEach((id: string) => { node._scopeIds.forEach((id: string) => {
delete knownIds[id] knownIds[id]--
if (knownIds[id] === 0) {
delete knownIds[id]
}
}) })
} }
} }
@ -185,11 +199,14 @@ export function processExpression(
} }
}) })
let ret
if (children.length) { if (children.length) {
return createCompoundExpression(children, node.loc) ret = createCompoundExpression(children, node.loc)
} else { } else {
return node ret = node
} }
ret.identifiers = Object.keys(knownIds)
return ret
} }
const isFunction = (node: Node): node is Function => const isFunction = (node: Node): node is Function =>

View File

@ -0,0 +1,98 @@
import { NodeTransform } from '../transform'
import {
NodeTypes,
ElementTypes,
CompoundExpressionNode,
createCompoundExpression,
CallExpression,
createCallExpression
} from '../ast'
import { isSimpleIdentifier } from '../utils'
import { buildProps } from './transformElement'
import { createCompilerError, ErrorCodes } from '../errors'
import { RENDER_SLOT } from '../runtimeConstants'
export const transformSlotOutlet: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT) {
const { props, children, loc } = node
const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
let slot: string | CompoundExpressionNode = $slots + `.default`
// check for <slot name="xxx" OR :name="xxx" />
let nameIndex: number = -1
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) {
if (prop.name === `name` && prop.value) {
// static name="xxx"
const name = prop.value.content
const accessor = isSimpleIdentifier(name)
? `.${name}`
: `[${JSON.stringify(name)}]`
slot = `${$slots}${accessor}`
nameIndex = i
break
}
} else if (prop.name === `bind`) {
const { arg, exp } = prop
if (
arg &&
exp &&
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
arg.isStatic &&
arg.content === `name`
) {
// dynamic :name="xxx"
slot = createCompoundExpression(
[
$slots + `[`,
...(exp.type === NodeTypes.SIMPLE_EXPRESSION
? [exp]
: exp.children),
`]`
],
loc
)
nameIndex = i
break
}
}
}
const slotArgs: CallExpression['arguments'] = [slot]
const propsWithoutName =
nameIndex > -1
? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
: props
const hasProps = propsWithoutName.length
if (hasProps) {
const { props: propsExpression, directives } = buildProps(
propsWithoutName,
loc,
context
)
if (directives.length) {
context.onError(
createCompilerError(
ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
directives[0].loc
)
)
}
slotArgs.push(propsExpression)
}
if (children.length) {
if (!hasProps) {
slotArgs.push(`{}`)
}
slotArgs.push(children)
}
node.codegenNode = createCallExpression(
context.helper(RENDER_SLOT),
slotArgs,
loc
)
}
}

View File

@ -43,15 +43,15 @@ export const transformFor = createStructuralDirectiveTransform(
const { addIdentifier, removeIdentifier } = context const { addIdentifier, removeIdentifier } = context
// inject identifiers to context // inject identifiers to context
value && addIdentifier(value) value && addIdentifier(value.content)
key && addIdentifier(key) key && addIdentifier(key.content)
index && addIdentifier(index) index && addIdentifier(index.content)
return () => { return () => {
// remove injected identifiers on exit // remove injected identifiers on exit
value && removeIdentifier(value) value && removeIdentifier(value.content)
key && removeIdentifier(key) key && removeIdentifier(key.content)
index && removeIdentifier(index) index && removeIdentifier(index.content)
} }
} else { } else {
context.onError( context.onError(

View File

@ -3,10 +3,6 @@ import {
ObjectExpression, ObjectExpression,
createObjectExpression, createObjectExpression,
NodeTypes, NodeTypes,
createCompoundExpression,
createCallExpression,
CompoundExpressionNode,
CallExpression,
createObjectProperty, createObjectProperty,
createSimpleExpression, createSimpleExpression,
createFunctionExpression, createFunctionExpression,
@ -17,16 +13,37 @@ import {
ChildNode, ChildNode,
SourceLocation SourceLocation
} from '../ast' } from '../ast'
import { TransformContext } from '../transform' import { TransformContext, NodeTransform } from '../transform'
import { buildProps } from './transformElement'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { isSimpleIdentifier } from '../utils'
import { RENDER_SLOT } from '../runtimeConstants'
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode => const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
p.type === NodeTypes.DIRECTIVE && p.name === 'slot' p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
// A NodeTransform that tracks scope identifiers for scoped slots so that they
// don't get prefixed by transformExpression. This transform is only applied
// in non-browser builds with { prefixIdentifiers: true }
export const trackSlotScopes: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.COMPONENT ||
node.tagType === ElementTypes.TEMPLATE)
) {
const vSlot = node.props.find(isVSlot)
if (vSlot && vSlot.exp) {
const { identifiers } = vSlot.exp
if (identifiers) {
identifiers.forEach(context.addIdentifier)
return () => {
identifiers.forEach(context.removeIdentifier)
}
}
}
}
}
// Instead of being a DirectiveTransform, v-slot processing is called during
// transformElement to build the slots object for a component.
export function buildSlots( export function buildSlots(
{ props, children, loc }: ElementNode, { props, children, loc }: ElementNode,
context: TransformContext context: TransformContext
@ -128,86 +145,3 @@ function buildSlot(
loc loc
) )
} }
export function buildSlotOutlet(node: ElementNode, context: TransformContext) {
const { props, children, loc } = node
const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
let slot: string | CompoundExpressionNode = $slots + `.default`
// check for <slot name="xxx" OR :name="xxx" />
let nameIndex: number = -1
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) {
if (prop.name === `name` && prop.value) {
// static name="xxx"
const name = prop.value.content
const accessor = isSimpleIdentifier(name)
? `.${name}`
: `[${JSON.stringify(name)}]`
slot = `${$slots}${accessor}`
nameIndex = i
break
}
} else if (prop.name === `bind`) {
const { arg, exp } = prop
if (
arg &&
exp &&
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
arg.isStatic &&
arg.content === `name`
) {
// dynamic :name="xxx"
slot = createCompoundExpression(
[
$slots + `[`,
...(exp.type === NodeTypes.SIMPLE_EXPRESSION
? [exp]
: exp.children),
`]`
],
loc
)
nameIndex = i
break
}
}
}
const slotArgs: CallExpression['arguments'] = [slot]
const propsWithoutName =
nameIndex > -1
? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
: props
const hasProps = propsWithoutName.length
if (hasProps) {
const { props: propsExpression, directives } = buildProps(
propsWithoutName,
loc,
context
)
if (directives.length) {
context.onError(
createCompilerError(
ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
directives[0].loc
)
)
}
slotArgs.push(propsExpression)
}
if (children.length) {
if (!hasProps) {
slotArgs.push(`{}`)
}
slotArgs.push(children)
}
node.codegenNode = createCallExpression(
context.helper(RENDER_SLOT),
slotArgs,
loc
)
}