refactor(compiler): split slot / slot outlet / slot scope handling into separate transforms
This commit is contained in:
parent
6377af483b
commit
6461b3853e
@ -8,7 +8,9 @@ return function render() {
|
||||
id: \\"foo\\",
|
||||
[prop]: bar,
|
||||
[foo + bar]: bar
|
||||
}, [createVNode(\\"p\\", { \\"some-key\\": \\"foo\\" })], [
|
||||
}, [
|
||||
createVNode(\\"p\\", { \\"some-key\\": \\"foo\\" })
|
||||
], [
|
||||
foo,
|
||||
createVNode(\\"p\\")
|
||||
])
|
||||
|
@ -14,7 +14,9 @@ return function render() {
|
||||
? _createVNode(\\"div\\", 0, \\"yes\\")
|
||||
: \\"no\\",
|
||||
_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\\")
|
||||
: \\"no\\",
|
||||
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\\")
|
||||
: \\"no\\",
|
||||
_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))
|
||||
])
|
||||
})
|
||||
])
|
||||
}"
|
||||
|
@ -553,7 +553,9 @@ describe('compiler: codegen', () => {
|
||||
id: "foo",
|
||||
[prop]: bar,
|
||||
[foo + bar]: bar
|
||||
}, [${CREATE_VNODE}("p", { "some-key": "foo" })], [
|
||||
}, [
|
||||
${CREATE_VNODE}("p", { "some-key": "foo" })
|
||||
], [
|
||||
foo,
|
||||
${CREATE_VNODE}("p")
|
||||
])`)
|
||||
|
@ -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`
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
@ -1,23 +1,17 @@
|
||||
import {
|
||||
CompilerOptions,
|
||||
parse,
|
||||
transform,
|
||||
ElementNode,
|
||||
NodeTypes,
|
||||
generate
|
||||
} from '../../src'
|
||||
import { CompilerOptions, parse, transform, generate } 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 { trackSlotScopes } from '../../src/transforms/vSlot'
|
||||
|
||||
function parseWithSlots(template: string, options: CompilerOptions = {}) {
|
||||
const ast = parse(template)
|
||||
transform(ast, {
|
||||
nodeTransforms: [
|
||||
...(options.prefixIdentifiers ? [transformExpression] : []),
|
||||
// slot transform is part of transformElement
|
||||
...(options.prefixIdentifiers
|
||||
? [transformExpression, trackSlotScopes]
|
||||
: []),
|
||||
transformElement
|
||||
],
|
||||
directiveTransforms: {
|
||||
@ -29,302 +23,19 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
|
||||
return ast
|
||||
}
|
||||
|
||||
describe('compiler: transform 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`
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
describe('compiler: transform component slots', () => {
|
||||
test('generate slot', () => {
|
||||
const ast = parseWithSlots(`<Comp><div/></Comp>`)
|
||||
const { code } = generate(ast)
|
||||
const ast = parseWithSlots(
|
||||
`
|
||||
<Comp>
|
||||
<Comp v-slot="{ dur }">
|
||||
hello {{ dur }}
|
||||
</Comp>
|
||||
</Comp>
|
||||
`,
|
||||
{ prefixIdentifiers: true }
|
||||
)
|
||||
const { code } = generate(ast, { prefixIdentifiers: true })
|
||||
console.log(code)
|
||||
})
|
||||
})
|
||||
|
@ -117,6 +117,9 @@ export interface SimpleExpressionNode extends Node {
|
||||
type: NodeTypes.SIMPLE_EXPRESSION
|
||||
content: string
|
||||
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 {
|
||||
@ -128,6 +131,9 @@ export interface InterpolationNode extends Node {
|
||||
export interface CompoundExpressionNode extends Node {
|
||||
type: NodeTypes.COMPOUND_EXPRESSION
|
||||
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 {
|
||||
|
@ -5,12 +5,14 @@ import { RootNode } from './ast'
|
||||
import { isString } from '@vue/shared'
|
||||
import { transformIf } from './transforms/vIf'
|
||||
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 { transformOn } from './transforms/vOn'
|
||||
import { transformBind } from './transforms/vBind'
|
||||
import { transformExpression } from './transforms/transformExpression'
|
||||
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
|
||||
import { transformStyle } from './transforms/transformStyle'
|
||||
import { trackSlotScopes } from './transforms/vSlot'
|
||||
|
||||
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
|
||||
|
||||
@ -41,8 +43,9 @@ export function baseCompile(
|
||||
nodeTransforms: [
|
||||
transformIf,
|
||||
transformFor,
|
||||
...(prefixIdentifiers ? [transformExpression] : []),
|
||||
...(prefixIdentifiers ? [transformExpression, trackSlotScopes] : []),
|
||||
transformStyle,
|
||||
transformSlotOutlet,
|
||||
transformElement,
|
||||
...(options.nodeTransforms || []) // user transforms
|
||||
],
|
||||
|
@ -8,8 +8,7 @@ import {
|
||||
Property,
|
||||
ExpressionNode,
|
||||
createSimpleExpression,
|
||||
JSChildNode,
|
||||
SimpleExpressionNode
|
||||
JSChildNode
|
||||
} from './ast'
|
||||
import { isString, isArray } from '@vue/shared'
|
||||
import { CompilerError, defaultOnError } from './errors'
|
||||
@ -64,8 +63,8 @@ export interface TransformContext extends Required<TransformOptions> {
|
||||
replaceNode(node: ChildNode): void
|
||||
removeNode(node?: ChildNode): void
|
||||
onNodeRemoved: () => void
|
||||
addIdentifier(exp: SimpleExpressionNode): void
|
||||
removeIdentifier(exp: SimpleExpressionNode): void
|
||||
addIdentifier(id: string): void
|
||||
removeIdentifier(id: string): void
|
||||
hoist(exp: JSChildNode): ExpressionNode
|
||||
}
|
||||
|
||||
@ -127,15 +126,15 @@ function createTransformContext(
|
||||
context.parent.children.splice(removalIndex, 1)
|
||||
},
|
||||
onNodeRemoved: () => {},
|
||||
addIdentifier({ content }) {
|
||||
addIdentifier(id) {
|
||||
const { identifiers } = context
|
||||
if (identifiers[content] === undefined) {
|
||||
identifiers[content] = 0
|
||||
if (identifiers[id] === undefined) {
|
||||
identifiers[id] = 0
|
||||
}
|
||||
;(identifiers[content] as number)++
|
||||
;(identifiers[id] as number)++
|
||||
},
|
||||
removeIdentifier({ content }) {
|
||||
;(context.identifiers[content] as number)--
|
||||
removeIdentifier(id) {
|
||||
;(context.identifiers[id] as number)--
|
||||
},
|
||||
hoist(exp) {
|
||||
context.hoists.push(exp)
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
TO_HANDLERS
|
||||
} from '../runtimeConstants'
|
||||
import { getInnerRange } from '../utils'
|
||||
import { buildSlotOutlet, buildSlots } from './vSlot'
|
||||
import { buildSlots } from './vSlot'
|
||||
|
||||
const toValidId = (str: string): string => str.replace(/[^\w]/g, '')
|
||||
|
||||
@ -95,10 +95,7 @@ export const transformElement: NodeTransform = (node, context) => {
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,12 +87,12 @@ export function processExpression(
|
||||
_parseScript || (_parseScript = require('meriyah').parseScript)
|
||||
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
|
||||
// we need to parse it differently.
|
||||
const source = `(${node.content})${asParams ? `=>{}` : ``}`
|
||||
try {
|
||||
ast = parseScript(source, { ranges: true }) as any
|
||||
ast = parseScript(source, { ranges: true })
|
||||
} catch (e) {
|
||||
context.onError(e)
|
||||
return node
|
||||
@ -139,11 +139,22 @@ export function processExpression(
|
||||
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 = new Set())
|
||||
).add(child.name)
|
||||
).add(name)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -151,9 +162,12 @@ export function processExpression(
|
||||
}
|
||||
},
|
||||
leave(node: any) {
|
||||
if (node._scopeIds) {
|
||||
if (node !== ast.body[0].expression && node._scopeIds) {
|
||||
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) {
|
||||
return createCompoundExpression(children, node.loc)
|
||||
ret = createCompoundExpression(children, node.loc)
|
||||
} else {
|
||||
return node
|
||||
ret = node
|
||||
}
|
||||
ret.identifiers = Object.keys(knownIds)
|
||||
return ret
|
||||
}
|
||||
|
||||
const isFunction = (node: Node): node is Function =>
|
||||
|
98
packages/compiler-core/src/transforms/transfromSlotOutlet.ts
Normal file
98
packages/compiler-core/src/transforms/transfromSlotOutlet.ts
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
@ -43,15 +43,15 @@ export const transformFor = createStructuralDirectiveTransform(
|
||||
const { addIdentifier, removeIdentifier } = context
|
||||
|
||||
// inject identifiers to context
|
||||
value && addIdentifier(value)
|
||||
key && addIdentifier(key)
|
||||
index && addIdentifier(index)
|
||||
value && addIdentifier(value.content)
|
||||
key && addIdentifier(key.content)
|
||||
index && addIdentifier(index.content)
|
||||
|
||||
return () => {
|
||||
// remove injected identifiers on exit
|
||||
value && removeIdentifier(value)
|
||||
key && removeIdentifier(key)
|
||||
index && removeIdentifier(index)
|
||||
value && removeIdentifier(value.content)
|
||||
key && removeIdentifier(key.content)
|
||||
index && removeIdentifier(index.content)
|
||||
}
|
||||
} else {
|
||||
context.onError(
|
||||
|
@ -3,10 +3,6 @@ import {
|
||||
ObjectExpression,
|
||||
createObjectExpression,
|
||||
NodeTypes,
|
||||
createCompoundExpression,
|
||||
createCallExpression,
|
||||
CompoundExpressionNode,
|
||||
CallExpression,
|
||||
createObjectProperty,
|
||||
createSimpleExpression,
|
||||
createFunctionExpression,
|
||||
@ -17,16 +13,37 @@ import {
|
||||
ChildNode,
|
||||
SourceLocation
|
||||
} from '../ast'
|
||||
import { TransformContext } from '../transform'
|
||||
import { buildProps } from './transformElement'
|
||||
import { TransformContext, NodeTransform } from '../transform'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { isSimpleIdentifier } from '../utils'
|
||||
import { RENDER_SLOT } from '../runtimeConstants'
|
||||
import { isString } from '@vue/shared'
|
||||
|
||||
const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
|
||||
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(
|
||||
{ props, children, loc }: ElementNode,
|
||||
context: TransformContext
|
||||
@ -128,86 +145,3 @@ function buildSlot(
|
||||
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
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user