feat(compiler): support v-for on named slots
This commit is contained in:
parent
f401ac6b88
commit
fc47029ed3
@ -14,9 +14,7 @@ return function render() {
|
|||||||
_toString(world.burn()),
|
_toString(world.burn()),
|
||||||
(_openBlock(), ok
|
(_openBlock(), ok
|
||||||
? _createBlock(\\"div\\", { key: 0 }, \\"yes\\")
|
? _createBlock(\\"div\\", { key: 0 }, \\"yes\\")
|
||||||
: _createBlock(_Fragment, { key: 1 }, [
|
: _createBlock(_Fragment, { key: 1 }, [\\"no\\"])),
|
||||||
\\"no\\"
|
|
||||||
])),
|
|
||||||
(_openBlock(), _createBlock(_Fragment, null, _renderList(list, (value, index) => {
|
(_openBlock(), _createBlock(_Fragment, null, _renderList(list, (value, index) => {
|
||||||
return (_openBlock(), _createBlock(\\"div\\", null, [
|
return (_openBlock(), _createBlock(\\"div\\", null, [
|
||||||
_createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */)
|
_createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */)
|
||||||
@ -39,9 +37,7 @@ return function render() {
|
|||||||
toString(_ctx.world.burn()),
|
toString(_ctx.world.burn()),
|
||||||
(openBlock(), (_ctx.ok)
|
(openBlock(), (_ctx.ok)
|
||||||
? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
|
? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
|
||||||
: createBlock(Fragment, { key: 1 }, [
|
: createBlock(Fragment, { key: 1 }, [\\"no\\"])),
|
||||||
\\"no\\"
|
|
||||||
])),
|
|
||||||
(openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
|
(openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
|
||||||
return (openBlock(), createBlock(\\"div\\", null, [
|
return (openBlock(), createBlock(\\"div\\", null, [
|
||||||
createVNode(\\"span\\", null, toString(value + index), 1 /* TEXT */)
|
createVNode(\\"span\\", null, toString(value + index), 1 /* TEXT */)
|
||||||
@ -63,9 +59,7 @@ export default function render() {
|
|||||||
_toString(_ctx.world.burn()),
|
_toString(_ctx.world.burn()),
|
||||||
(openBlock(), (_ctx.ok)
|
(openBlock(), (_ctx.ok)
|
||||||
? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
|
? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
|
||||||
: createBlock(Fragment, { key: 1 }, [
|
: createBlock(Fragment, { key: 1 }, [\\"no\\"])),
|
||||||
\\"no\\"
|
|
||||||
])),
|
|
||||||
(openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
|
(openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
|
||||||
return (openBlock(), createBlock(\\"div\\", null, [
|
return (openBlock(), createBlock(\\"div\\", null, [
|
||||||
createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */)
|
createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */)
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
ElementTypes
|
ElementTypes
|
||||||
} from '../src'
|
} from '../src'
|
||||||
import { CREATE_VNODE } from '../src/runtimeConstants'
|
import { CREATE_VNODE } from '../src/runtimeConstants'
|
||||||
|
import { isString } from '@vue/shared'
|
||||||
|
|
||||||
const leadingBracketRE = /^\[/
|
const leadingBracketRE = /^\[/
|
||||||
const bracketsRE = /^\[|\]$/g
|
const bracketsRE = /^\[|\]$/g
|
||||||
@ -26,11 +27,13 @@ export function createObjectMatcher(obj: any) {
|
|||||||
content: key.replace(bracketsRE, ''),
|
content: key.replace(bracketsRE, ''),
|
||||||
isStatic: !leadingBracketRE.test(key)
|
isStatic: !leadingBracketRE.test(key)
|
||||||
},
|
},
|
||||||
value: {
|
value: isString(obj[key])
|
||||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
? {
|
||||||
content: obj[key].replace(bracketsRE, ''),
|
type: NodeTypes.SIMPLE_EXPRESSION,
|
||||||
isStatic: !leadingBracketRE.test(obj[key])
|
content: obj[key].replace(bracketsRE, ''),
|
||||||
}
|
isStatic: !leadingBracketRE.test(obj[key])
|
||||||
|
}
|
||||||
|
: obj[key]
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,7 @@ return function render() {
|
|||||||
? _createBlock(\\"div\\", { key: 0 })
|
? _createBlock(\\"div\\", { key: 0 })
|
||||||
: orNot
|
: orNot
|
||||||
? _createBlock(\\"p\\", { key: 1 })
|
? _createBlock(\\"p\\", { key: 1 })
|
||||||
: _createBlock(_Fragment, { key: 2 }, [
|
: _createBlock(_Fragment, { key: 2 }, [\\"fine\\"]))
|
||||||
\\"fine\\"
|
|
||||||
]))
|
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
@ -8,14 +8,8 @@ return function render() {
|
|||||||
const _component_Comp = resolveComponent(\\"Comp\\")
|
const _component_Comp = resolveComponent(\\"Comp\\")
|
||||||
|
|
||||||
return (openBlock(), createBlock(_component_Comp, null, {
|
return (openBlock(), createBlock(_component_Comp, null, {
|
||||||
[_ctx.one]: ({ foo }) => [
|
[_ctx.one]: ({ foo }) => [toString(foo), toString(_ctx.bar)],
|
||||||
toString(foo),
|
[_ctx.two]: ({ bar }) => [toString(_ctx.foo), toString(bar)]
|
||||||
toString(_ctx.bar)
|
|
||||||
],
|
|
||||||
[_ctx.two]: ({ bar }) => [
|
|
||||||
toString(_ctx.foo),
|
|
||||||
toString(bar)
|
|
||||||
]
|
|
||||||
}, 256 /* DYNAMIC_SLOTS */))
|
}, 256 /* DYNAMIC_SLOTS */))
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@ -28,10 +22,7 @@ return function render() {
|
|||||||
const _component_Comp = resolveComponent(\\"Comp\\")
|
const _component_Comp = resolveComponent(\\"Comp\\")
|
||||||
|
|
||||||
return (openBlock(), createBlock(_component_Comp, null, {
|
return (openBlock(), createBlock(_component_Comp, null, {
|
||||||
default: ({ foo }) => [
|
default: ({ foo }) => [toString(foo), toString(_ctx.bar)]
|
||||||
toString(foo),
|
|
||||||
toString(_ctx.bar)
|
|
||||||
]
|
|
||||||
}))
|
}))
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@ -51,6 +42,92 @@ return function render() {
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`compiler: transform component slots named slot with v-for w/ prefixIdentifiers: true 1`] = `
|
||||||
|
"const { toString, resolveComponent, renderList, createSlots, createVNode, openBlock, createBlock } = Vue
|
||||||
|
|
||||||
|
return function render() {
|
||||||
|
const _ctx = this
|
||||||
|
const _component_Comp = resolveComponent(\\"Comp\\")
|
||||||
|
|
||||||
|
return (openBlock(), createBlock(_component_Comp, null, createSlots({}, [
|
||||||
|
renderList(_ctx.list, (name) => {
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
fn: () => [toString(name)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]), 256 /* DYNAMIC_SLOTS */))
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`compiler: transform component slots named slot with v-if + prefixIdentifiers: true 1`] = `
|
||||||
|
"const { toString, resolveComponent, createSlots, createVNode, openBlock, createBlock } = Vue
|
||||||
|
|
||||||
|
return function render() {
|
||||||
|
const _ctx = this
|
||||||
|
const _component_Comp = resolveComponent(\\"Comp\\")
|
||||||
|
|
||||||
|
return (openBlock(), createBlock(_component_Comp, null, createSlots({}, [
|
||||||
|
(_ctx.ok)
|
||||||
|
? {
|
||||||
|
name: \\"one\\",
|
||||||
|
fn: (props) => [toString(props)]
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
]), 256 /* DYNAMIC_SLOTS */))
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`compiler: transform component slots named slot with v-if + v-else-if + v-else 1`] = `
|
||||||
|
"const _Vue = Vue
|
||||||
|
|
||||||
|
return function render() {
|
||||||
|
with (this) {
|
||||||
|
const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
|
||||||
|
|
||||||
|
const _component_Comp = _resolveComponent(\\"Comp\\")
|
||||||
|
|
||||||
|
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({}, [
|
||||||
|
ok
|
||||||
|
? {
|
||||||
|
name: \\"one\\",
|
||||||
|
fn: () => [\\"foo\\"]
|
||||||
|
}
|
||||||
|
: orNot
|
||||||
|
? {
|
||||||
|
name: \\"two\\",
|
||||||
|
fn: (props) => [\\"bar\\"]
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: \\"one\\",
|
||||||
|
fn: () => [\\"baz\\"]
|
||||||
|
}
|
||||||
|
]), 256 /* DYNAMIC_SLOTS */))
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`compiler: transform component slots named slot with v-if 1`] = `
|
||||||
|
"const _Vue = Vue
|
||||||
|
|
||||||
|
return function render() {
|
||||||
|
with (this) {
|
||||||
|
const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
|
||||||
|
|
||||||
|
const _component_Comp = _resolveComponent(\\"Comp\\")
|
||||||
|
|
||||||
|
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({}, [
|
||||||
|
ok
|
||||||
|
? {
|
||||||
|
name: \\"one\\",
|
||||||
|
fn: () => [\\"hello\\"]
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
]), 256 /* DYNAMIC_SLOTS */))
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`compiler: transform component slots named slots 1`] = `
|
exports[`compiler: transform component slots named slots 1`] = `
|
||||||
"const { toString, resolveComponent, createVNode, openBlock, createBlock } = Vue
|
"const { toString, resolveComponent, createVNode, openBlock, createBlock } = Vue
|
||||||
|
|
||||||
@ -59,14 +136,8 @@ return function render() {
|
|||||||
const _component_Comp = resolveComponent(\\"Comp\\")
|
const _component_Comp = resolveComponent(\\"Comp\\")
|
||||||
|
|
||||||
return (openBlock(), createBlock(_component_Comp, null, {
|
return (openBlock(), createBlock(_component_Comp, null, {
|
||||||
one: ({ foo }) => [
|
one: ({ foo }) => [toString(foo), toString(_ctx.bar)],
|
||||||
toString(foo),
|
two: ({ bar }) => [toString(_ctx.foo), toString(bar)]
|
||||||
toString(_ctx.bar)
|
|
||||||
],
|
|
||||||
two: ({ bar }) => [
|
|
||||||
toString(_ctx.foo),
|
|
||||||
toString(bar)
|
|
||||||
]
|
|
||||||
}))
|
}))
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
@ -82,11 +153,7 @@ return function render() {
|
|||||||
return (openBlock(), createBlock(_component_Comp, null, {
|
return (openBlock(), createBlock(_component_Comp, null, {
|
||||||
default: ({ foo }) => [
|
default: ({ foo }) => [
|
||||||
createVNode(_component_Inner, null, {
|
createVNode(_component_Inner, null, {
|
||||||
default: ({ bar }) => [
|
default: ({ bar }) => [toString(foo), toString(bar), toString(_ctx.baz)]
|
||||||
toString(foo),
|
|
||||||
toString(bar),
|
|
||||||
toString(_ctx.baz)
|
|
||||||
]
|
|
||||||
}),
|
}),
|
||||||
toString(foo),
|
toString(foo),
|
||||||
toString(_ctx.bar),
|
toString(_ctx.bar),
|
||||||
|
@ -11,14 +11,20 @@ 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 { trackSlotScopes } from '../../src/transforms/vSlot'
|
import {
|
||||||
|
trackSlotScopes,
|
||||||
|
trackVForSlotScopes
|
||||||
|
} from '../../src/transforms/vSlot'
|
||||||
|
import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeConstants'
|
||||||
|
import { createObjectMatcher } from '../testUtils'
|
||||||
|
import { PatchFlags } from '@vue/shared'
|
||||||
|
|
||||||
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
|
...(options.prefixIdentifiers
|
||||||
? [transformExpression, trackSlotScopes]
|
? [trackVForSlotScopes, transformExpression, trackSlotScopes]
|
||||||
: []),
|
: []),
|
||||||
transformElement
|
transformElement
|
||||||
],
|
],
|
||||||
@ -314,118 +320,311 @@ describe('compiler: transform component slots', () => {
|
|||||||
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
|
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('error on extraneous children w/ named slots', () => {
|
test('named slot with v-if', () => {
|
||||||
const onError = jest.fn()
|
const { root, slots } = parseWithSlots(
|
||||||
const source = `<Comp><template #default>foo</template>bar</Comp>`
|
`<Comp>
|
||||||
parseWithSlots(source, { onError })
|
<template #one v-if="ok">hello</template>
|
||||||
const index = source.indexOf('bar')
|
</Comp>`
|
||||||
expect(onError.mock.calls[0][0]).toMatchObject({
|
)
|
||||||
code: ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
|
expect(slots).toMatchObject({
|
||||||
loc: {
|
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||||
source: `bar`,
|
callee: `_${CREATE_SLOTS}`,
|
||||||
start: {
|
arguments: [
|
||||||
offset: index,
|
createObjectMatcher({}),
|
||||||
line: 1,
|
{
|
||||||
column: index + 1
|
type: NodeTypes.JS_ARRAY_EXPRESSION,
|
||||||
},
|
elements: [
|
||||||
end: {
|
{
|
||||||
offset: index + 3,
|
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
|
||||||
line: 1,
|
test: { content: `ok` },
|
||||||
column: index + 4
|
consequent: createObjectMatcher({
|
||||||
|
name: `one`,
|
||||||
|
fn: {
|
||||||
|
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
||||||
|
returns: [{ type: NodeTypes.TEXT, content: `hello` }]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
alternate: {
|
||||||
|
content: `undefined`,
|
||||||
|
isStatic: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
})
|
})
|
||||||
|
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
|
||||||
|
PatchFlags.DYNAMIC_SLOTS + ''
|
||||||
|
)
|
||||||
|
expect(generate(root).code).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('error on duplicated slot names', () => {
|
test('named slot with v-if + prefixIdentifiers: true', () => {
|
||||||
const onError = jest.fn()
|
const { root, slots } = parseWithSlots(
|
||||||
const source = `<Comp><template #foo></template><template #foo></template></Comp>`
|
`<Comp>
|
||||||
parseWithSlots(source, { onError })
|
<template #one="props" v-if="ok">{{ props }}</template>
|
||||||
const index = source.lastIndexOf('#foo')
|
</Comp>`,
|
||||||
expect(onError.mock.calls[0][0]).toMatchObject({
|
{ prefixIdentifiers: true }
|
||||||
code: ErrorCodes.X_DUPLICATE_SLOT_NAMES,
|
)
|
||||||
loc: {
|
expect(slots).toMatchObject({
|
||||||
source: `#foo`,
|
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||||
start: {
|
callee: CREATE_SLOTS,
|
||||||
offset: index,
|
arguments: [
|
||||||
line: 1,
|
createObjectMatcher({}),
|
||||||
column: index + 1
|
{
|
||||||
},
|
type: NodeTypes.JS_ARRAY_EXPRESSION,
|
||||||
end: {
|
elements: [
|
||||||
offset: index + 4,
|
{
|
||||||
line: 1,
|
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
|
||||||
column: index + 5
|
test: { content: `_ctx.ok` },
|
||||||
|
consequent: createObjectMatcher({
|
||||||
|
name: `one`,
|
||||||
|
fn: {
|
||||||
|
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
||||||
|
params: { content: `props` },
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
type: NodeTypes.INTERPOLATION,
|
||||||
|
content: { content: `props` }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
alternate: {
|
||||||
|
content: `undefined`,
|
||||||
|
isStatic: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
})
|
})
|
||||||
|
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
|
||||||
|
PatchFlags.DYNAMIC_SLOTS + ''
|
||||||
|
)
|
||||||
|
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('error on invalid mixed slot usage', () => {
|
test('named slot with v-if + v-else-if + v-else', () => {
|
||||||
const onError = jest.fn()
|
const { root, slots } = parseWithSlots(
|
||||||
const source = `<Comp v-slot="foo"><template #foo></template></Comp>`
|
`<Comp>
|
||||||
parseWithSlots(source, { onError })
|
<template #one v-if="ok">foo</template>
|
||||||
const index = source.lastIndexOf('#foo')
|
<template #two="props" v-else-if="orNot">bar</template>
|
||||||
expect(onError.mock.calls[0][0]).toMatchObject({
|
<template #one v-else>baz</template>
|
||||||
code: ErrorCodes.X_MIXED_SLOT_USAGE,
|
</Comp>`
|
||||||
loc: {
|
)
|
||||||
source: `#foo`,
|
expect(slots).toMatchObject({
|
||||||
start: {
|
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||||
offset: index,
|
callee: `_${CREATE_SLOTS}`,
|
||||||
line: 1,
|
arguments: [
|
||||||
column: index + 1
|
createObjectMatcher({}),
|
||||||
},
|
{
|
||||||
end: {
|
type: NodeTypes.JS_ARRAY_EXPRESSION,
|
||||||
offset: index + 4,
|
elements: [
|
||||||
line: 1,
|
{
|
||||||
column: index + 5
|
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
|
||||||
|
test: { content: `ok` },
|
||||||
|
consequent: createObjectMatcher({
|
||||||
|
name: `one`,
|
||||||
|
fn: {
|
||||||
|
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
||||||
|
params: undefined,
|
||||||
|
returns: [{ type: NodeTypes.TEXT, content: `foo` }]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
alternate: {
|
||||||
|
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
|
||||||
|
test: { content: `orNot` },
|
||||||
|
consequent: createObjectMatcher({
|
||||||
|
name: `two`,
|
||||||
|
fn: {
|
||||||
|
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
||||||
|
params: { content: `props` },
|
||||||
|
returns: [{ type: NodeTypes.TEXT, content: `bar` }]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
alternate: createObjectMatcher({
|
||||||
|
name: `one`,
|
||||||
|
fn: {
|
||||||
|
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
||||||
|
params: undefined,
|
||||||
|
returns: [{ type: NodeTypes.TEXT, content: `baz` }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
})
|
})
|
||||||
|
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
|
||||||
|
PatchFlags.DYNAMIC_SLOTS + ''
|
||||||
|
)
|
||||||
|
expect(generate(root).code).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('error on v-slot usage on plain elements', () => {
|
test('named slot with v-for w/ prefixIdentifiers: true', () => {
|
||||||
const onError = jest.fn()
|
const { root, slots } = parseWithSlots(
|
||||||
const source = `<div v-slot/>`
|
`<Comp>
|
||||||
parseWithSlots(source, { onError })
|
<template v-for="name in list" #[name]>{{ name }}</template>
|
||||||
const index = source.indexOf('v-slot')
|
</Comp>`,
|
||||||
expect(onError.mock.calls[0][0]).toMatchObject({
|
{ prefixIdentifiers: true }
|
||||||
code: ErrorCodes.X_MISPLACED_V_SLOT,
|
)
|
||||||
loc: {
|
expect(slots).toMatchObject({
|
||||||
source: `v-slot`,
|
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||||
start: {
|
callee: CREATE_SLOTS,
|
||||||
offset: index,
|
arguments: [
|
||||||
line: 1,
|
createObjectMatcher({}),
|
||||||
column: index + 1
|
{
|
||||||
},
|
type: NodeTypes.JS_ARRAY_EXPRESSION,
|
||||||
end: {
|
elements: [
|
||||||
offset: index + 6,
|
{
|
||||||
line: 1,
|
type: NodeTypes.JS_CALL_EXPRESSION,
|
||||||
column: index + 7
|
callee: RENDER_LIST,
|
||||||
|
arguments: [
|
||||||
|
{ content: `_ctx.list` },
|
||||||
|
{
|
||||||
|
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
||||||
|
params: [{ content: `name` }],
|
||||||
|
returns: createObjectMatcher({
|
||||||
|
name: `[name]`,
|
||||||
|
fn: {
|
||||||
|
type: NodeTypes.JS_FUNCTION_EXPRESSION,
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
type: NodeTypes.INTERPOLATION,
|
||||||
|
content: { content: `name`, isStatic: false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
})
|
})
|
||||||
|
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
|
||||||
|
PatchFlags.DYNAMIC_SLOTS + ''
|
||||||
|
)
|
||||||
|
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('error on named slot on component', () => {
|
describe('errors', () => {
|
||||||
const onError = jest.fn()
|
test('error on extraneous children w/ named slots', () => {
|
||||||
const source = `<Comp v-slot:foo>foo</Comp>`
|
const onError = jest.fn()
|
||||||
parseWithSlots(source, { onError })
|
const source = `<Comp><template #default>foo</template>bar</Comp>`
|
||||||
const index = source.indexOf('v-slot')
|
parseWithSlots(source, { onError })
|
||||||
expect(onError.mock.calls[0][0]).toMatchObject({
|
const index = source.indexOf('bar')
|
||||||
code: ErrorCodes.X_NAMED_SLOT_ON_COMPONENT,
|
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||||
loc: {
|
code: ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
|
||||||
source: `v-slot:foo`,
|
loc: {
|
||||||
start: {
|
source: `bar`,
|
||||||
offset: index,
|
start: {
|
||||||
line: 1,
|
offset: index,
|
||||||
column: index + 1
|
line: 1,
|
||||||
},
|
column: index + 1
|
||||||
end: {
|
},
|
||||||
offset: index + 10,
|
end: {
|
||||||
line: 1,
|
offset: index + 3,
|
||||||
column: index + 11
|
line: 1,
|
||||||
|
column: index + 4
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('error on duplicated slot names', () => {
|
||||||
|
const onError = jest.fn()
|
||||||
|
const source = `<Comp><template #foo></template><template #foo></template></Comp>`
|
||||||
|
parseWithSlots(source, { onError })
|
||||||
|
const index = source.lastIndexOf('#foo')
|
||||||
|
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||||
|
code: ErrorCodes.X_DUPLICATE_SLOT_NAMES,
|
||||||
|
loc: {
|
||||||
|
source: `#foo`,
|
||||||
|
start: {
|
||||||
|
offset: index,
|
||||||
|
line: 1,
|
||||||
|
column: index + 1
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
offset: index + 4,
|
||||||
|
line: 1,
|
||||||
|
column: index + 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('error on invalid mixed slot usage', () => {
|
||||||
|
const onError = jest.fn()
|
||||||
|
const source = `<Comp v-slot="foo"><template #foo></template></Comp>`
|
||||||
|
parseWithSlots(source, { onError })
|
||||||
|
const index = source.lastIndexOf('#foo')
|
||||||
|
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||||
|
code: ErrorCodes.X_MIXED_SLOT_USAGE,
|
||||||
|
loc: {
|
||||||
|
source: `#foo`,
|
||||||
|
start: {
|
||||||
|
offset: index,
|
||||||
|
line: 1,
|
||||||
|
column: index + 1
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
offset: index + 4,
|
||||||
|
line: 1,
|
||||||
|
column: index + 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('error on v-slot usage on plain elements', () => {
|
||||||
|
const onError = jest.fn()
|
||||||
|
const source = `<div v-slot/>`
|
||||||
|
parseWithSlots(source, { onError })
|
||||||
|
const index = source.indexOf('v-slot')
|
||||||
|
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||||
|
code: ErrorCodes.X_MISPLACED_V_SLOT,
|
||||||
|
loc: {
|
||||||
|
source: `v-slot`,
|
||||||
|
start: {
|
||||||
|
offset: index,
|
||||||
|
line: 1,
|
||||||
|
column: index + 1
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
offset: index + 6,
|
||||||
|
line: 1,
|
||||||
|
column: index + 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('error on named slot on component', () => {
|
||||||
|
const onError = jest.fn()
|
||||||
|
const source = `<Comp v-slot:foo>foo</Comp>`
|
||||||
|
parseWithSlots(source, { onError })
|
||||||
|
const index = source.indexOf('v-slot')
|
||||||
|
expect(onError.mock.calls[0][0]).toMatchObject({
|
||||||
|
code: ErrorCodes.X_NAMED_SLOT_ON_COMPONENT,
|
||||||
|
loc: {
|
||||||
|
source: `v-slot:foo`,
|
||||||
|
start: {
|
||||||
|
offset: index,
|
||||||
|
line: 1,
|
||||||
|
column: index + 1
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
offset: index + 10,
|
||||||
|
line: 1,
|
||||||
|
column: index + 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { isString } from '@vue/shared'
|
import { isString } from '@vue/shared'
|
||||||
|
import { ForParseResult } from './transforms/vFor'
|
||||||
|
|
||||||
// Vue template is a platform-agnostic superset of HTML (syntax only).
|
// Vue template is a platform-agnostic superset of HTML (syntax only).
|
||||||
// More namespaces like SVG and MathML are declared by platform specific
|
// More namespaces like SVG and MathML are declared by platform specific
|
||||||
@ -115,6 +116,8 @@ export interface DirectiveNode extends Node {
|
|||||||
exp: ExpressionNode | undefined
|
exp: ExpressionNode | undefined
|
||||||
arg: ExpressionNode | undefined
|
arg: ExpressionNode | undefined
|
||||||
modifiers: string[]
|
modifiers: string[]
|
||||||
|
// optional property to cache the expression parse result for v-for
|
||||||
|
parseResult?: ForParseResult
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SimpleExpressionNode extends Node {
|
export interface SimpleExpressionNode extends Node {
|
||||||
@ -249,13 +252,13 @@ export function createObjectExpression(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createObjectProperty(
|
export function createObjectProperty(
|
||||||
key: Property['key'],
|
key: Property['key'] | string,
|
||||||
value: Property['value']
|
value: Property['value']
|
||||||
): Property {
|
): Property {
|
||||||
return {
|
return {
|
||||||
type: NodeTypes.JS_PROPERTY,
|
type: NodeTypes.JS_PROPERTY,
|
||||||
loc: locStub,
|
loc: locStub,
|
||||||
key,
|
key: isString(key) ? createSimpleExpression(key, true) : key,
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,17 +259,23 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
|
|||||||
context.newline()
|
context.newline()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isText(n: string | CodegenNode) {
|
||||||
|
return (
|
||||||
|
isString(n) ||
|
||||||
|
n.type === NodeTypes.SIMPLE_EXPRESSION ||
|
||||||
|
n.type === NodeTypes.TEXT ||
|
||||||
|
n.type === NodeTypes.INTERPOLATION ||
|
||||||
|
n.type === NodeTypes.COMPOUND_EXPRESSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function genNodeListAsArray(
|
function genNodeListAsArray(
|
||||||
nodes: (string | CodegenNode | TemplateChildNode[])[],
|
nodes: (string | CodegenNode | TemplateChildNode[])[],
|
||||||
context: CodegenContext
|
context: CodegenContext
|
||||||
) {
|
) {
|
||||||
const multilines =
|
const multilines =
|
||||||
nodes.length > 3 ||
|
nodes.length > 3 ||
|
||||||
((!__BROWSER__ || __DEV__) &&
|
((!__BROWSER__ || __DEV__) && nodes.some(n => isArray(n) || !isText(n)))
|
||||||
nodes.some(
|
|
||||||
n =>
|
|
||||||
isArray(n) || (!isString(n) && n.type !== NodeTypes.SIMPLE_EXPRESSION)
|
|
||||||
))
|
|
||||||
context.push(`[`)
|
context.push(`[`)
|
||||||
multilines && context.indent()
|
multilines && context.indent()
|
||||||
genNodeList(nodes, context, multilines)
|
genNodeList(nodes, context, multilines)
|
||||||
@ -435,6 +441,10 @@ function genCallExpression(node: CallExpression, context: CodegenContext) {
|
|||||||
function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
|
function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
|
||||||
const { push, indent, deindent, newline, resetMapping } = context
|
const { push, indent, deindent, newline, resetMapping } = context
|
||||||
const { properties } = node
|
const { properties } = node
|
||||||
|
if (!properties.length) {
|
||||||
|
push(`{}`, node)
|
||||||
|
return
|
||||||
|
}
|
||||||
const multilines =
|
const multilines =
|
||||||
properties.length > 1 ||
|
properties.length > 1 ||
|
||||||
((!__BROWSER__ || __DEV__) &&
|
((!__BROWSER__ || __DEV__) &&
|
||||||
|
@ -12,7 +12,7 @@ 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 { defaultOnError, createCompilerError, ErrorCodes } from './errors'
|
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
|
||||||
import { trackSlotScopes } from './transforms/vSlot'
|
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
|
||||||
import { optimizeText } from './transforms/optimizeText'
|
import { optimizeText } from './transforms/optimizeText'
|
||||||
|
|
||||||
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
|
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
|
||||||
@ -45,7 +45,14 @@ export function baseCompile(
|
|||||||
nodeTransforms: [
|
nodeTransforms: [
|
||||||
transformIf,
|
transformIf,
|
||||||
transformFor,
|
transformFor,
|
||||||
...(prefixIdentifiers ? [transformExpression, trackSlotScopes] : []),
|
...(prefixIdentifiers
|
||||||
|
? [
|
||||||
|
// order is important
|
||||||
|
trackVForSlotScopes,
|
||||||
|
transformExpression,
|
||||||
|
trackSlotScopes
|
||||||
|
]
|
||||||
|
: []),
|
||||||
optimizeText,
|
optimizeText,
|
||||||
transformStyle,
|
transformStyle,
|
||||||
transformSlotOutlet,
|
transformSlotOutlet,
|
||||||
|
@ -14,6 +14,7 @@ export const RESOLVE_DIRECTIVE = `resolveDirective`
|
|||||||
export const APPLY_DIRECTIVES = `applyDirectives`
|
export const APPLY_DIRECTIVES = `applyDirectives`
|
||||||
export const RENDER_LIST = `renderList`
|
export const RENDER_LIST = `renderList`
|
||||||
export const RENDER_SLOT = `renderSlot`
|
export const RENDER_SLOT = `renderSlot`
|
||||||
|
export const CREATE_SLOTS = `createSlots`
|
||||||
export const TO_STRING = `toString`
|
export const TO_STRING = `toString`
|
||||||
export const MERGE_PROPS = `mergeProps`
|
export const MERGE_PROPS = `mergeProps`
|
||||||
export const TO_HANDLERS = `toHandlers`
|
export const TO_HANDLERS = `toHandlers`
|
||||||
|
@ -15,8 +15,7 @@ import {
|
|||||||
import { isString, isArray } from '@vue/shared'
|
import { isString, isArray } from '@vue/shared'
|
||||||
import { CompilerError, defaultOnError } from './errors'
|
import { CompilerError, defaultOnError } from './errors'
|
||||||
import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
|
import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
|
||||||
import { createBlockExpression } from './utils'
|
import { isVSlot, createBlockExpression } from './utils'
|
||||||
import { isVSlot } from './transforms/vSlot'
|
|
||||||
|
|
||||||
// There are two types of transforms:
|
// There are two types of transforms:
|
||||||
//
|
//
|
||||||
|
@ -26,8 +26,8 @@ import {
|
|||||||
MERGE_PROPS,
|
MERGE_PROPS,
|
||||||
TO_HANDLERS
|
TO_HANDLERS
|
||||||
} from '../runtimeConstants'
|
} from '../runtimeConstants'
|
||||||
import { getInnerRange } from '../utils'
|
import { getInnerRange, isVSlot } from '../utils'
|
||||||
import { buildSlots, isVSlot } from './vSlot'
|
import { buildSlots } from './vSlot'
|
||||||
|
|
||||||
const toValidId = (str: string): string => str.replace(/[^\w]/g, '')
|
const toValidId = (str: string): string => str.replace(/[^\w]/g, '')
|
||||||
|
|
||||||
@ -418,7 +418,7 @@ function createDirectiveArgs(
|
|||||||
createObjectExpression(
|
createObjectExpression(
|
||||||
dir.modifiers.map(modifier =>
|
dir.modifiers.map(modifier =>
|
||||||
createObjectProperty(
|
createObjectProperty(
|
||||||
createSimpleExpression(modifier, true, loc),
|
modifier,
|
||||||
createSimpleExpression(`true`, false, loc)
|
createSimpleExpression(`true`, false, loc)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -35,11 +35,17 @@ export const transformExpression: NodeTransform = (node, context) => {
|
|||||||
// handle directives on element
|
// handle directives on element
|
||||||
for (let i = 0; i < node.props.length; i++) {
|
for (let i = 0; i < node.props.length; i++) {
|
||||||
const dir = node.props[i]
|
const dir = node.props[i]
|
||||||
if (dir.type === NodeTypes.DIRECTIVE) {
|
// do not process for v-for since it's special handled
|
||||||
|
if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
|
||||||
const exp = dir.exp as SimpleExpressionNode | undefined
|
const exp = dir.exp as SimpleExpressionNode | undefined
|
||||||
const arg = dir.arg as SimpleExpressionNode | undefined
|
const arg = dir.arg as SimpleExpressionNode | undefined
|
||||||
if (exp) {
|
if (exp) {
|
||||||
dir.exp = processExpression(exp, context, dir.name === 'slot')
|
dir.exp = processExpression(
|
||||||
|
exp,
|
||||||
|
context,
|
||||||
|
// slot args must be processed as function params
|
||||||
|
dir.name === 'slot'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (arg && !arg.isStatic) {
|
if (arg && !arg.isStatic) {
|
||||||
dir.arg = processExpression(arg, context)
|
dir.arg = processExpression(arg, context)
|
||||||
|
@ -90,26 +90,6 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// finish the codegen now that all children have been traversed
|
// finish the codegen now that all children have been traversed
|
||||||
const params: ExpressionNode[] = []
|
|
||||||
if (value) {
|
|
||||||
params.push(value)
|
|
||||||
}
|
|
||||||
if (key) {
|
|
||||||
if (!value) {
|
|
||||||
params.push(createSimpleExpression(`_`, false))
|
|
||||||
}
|
|
||||||
params.push(key)
|
|
||||||
}
|
|
||||||
if (index) {
|
|
||||||
if (!key) {
|
|
||||||
if (!value) {
|
|
||||||
params.push(createSimpleExpression(`_`, false))
|
|
||||||
}
|
|
||||||
params.push(createSimpleExpression(`__`, false))
|
|
||||||
}
|
|
||||||
params.push(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
let childBlock
|
let childBlock
|
||||||
if (node.tagType === ElementTypes.TEMPLATE) {
|
if (node.tagType === ElementTypes.TEMPLATE) {
|
||||||
// <template v-for="...">
|
// <template v-for="...">
|
||||||
@ -118,7 +98,7 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
if (keyProp) {
|
if (keyProp) {
|
||||||
childBlockProps = createObjectExpression([
|
childBlockProps = createObjectExpression([
|
||||||
createObjectProperty(
|
createObjectProperty(
|
||||||
createSimpleExpression(`key`, true),
|
`key`,
|
||||||
keyProp.type === NodeTypes.ATTRIBUTE
|
keyProp.type === NodeTypes.ATTRIBUTE
|
||||||
? createSimpleExpression(keyProp.value!.content, true)
|
? createSimpleExpression(keyProp.value!.content, true)
|
||||||
: keyProp.exp!
|
: keyProp.exp!
|
||||||
@ -153,7 +133,7 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
|
|
||||||
renderExp.arguments.push(
|
renderExp.arguments.push(
|
||||||
createFunctionExpression(
|
createFunctionExpression(
|
||||||
params,
|
createForLoopParams(parseResult),
|
||||||
childBlock,
|
childBlock,
|
||||||
true /* force newline */
|
true /* force newline */
|
||||||
)
|
)
|
||||||
@ -178,21 +158,21 @@ const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
|
|||||||
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
|
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
|
||||||
const stripParensRE = /^\(|\)$/g
|
const stripParensRE = /^\(|\)$/g
|
||||||
|
|
||||||
interface ForParseResult {
|
export interface ForParseResult {
|
||||||
source: ExpressionNode
|
source: ExpressionNode
|
||||||
value: ExpressionNode | undefined
|
value: ExpressionNode | undefined
|
||||||
key: ExpressionNode | undefined
|
key: ExpressionNode | undefined
|
||||||
index: ExpressionNode | undefined
|
index: ExpressionNode | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseForExpression(
|
export function parseForExpression(
|
||||||
input: SimpleExpressionNode,
|
input: SimpleExpressionNode,
|
||||||
context: TransformContext
|
context: TransformContext
|
||||||
): ForParseResult | null {
|
): ForParseResult | undefined {
|
||||||
const loc = input.loc
|
const loc = input.loc
|
||||||
const exp = input.content
|
const exp = input.content
|
||||||
const inMatch = exp.match(forAliasRE)
|
const inMatch = exp.match(forAliasRE)
|
||||||
if (!inMatch) return null
|
if (!inMatch) return
|
||||||
|
|
||||||
const [, LHS, RHS] = inMatch
|
const [, LHS, RHS] = inMatch
|
||||||
|
|
||||||
@ -274,3 +254,30 @@ function createAliasExpression(
|
|||||||
getInnerRange(range, offset, content.length)
|
getInnerRange(range, offset, content.length)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createForLoopParams({
|
||||||
|
value,
|
||||||
|
key,
|
||||||
|
index
|
||||||
|
}: ForParseResult): ExpressionNode[] {
|
||||||
|
const params: ExpressionNode[] = []
|
||||||
|
if (value) {
|
||||||
|
params.push(value)
|
||||||
|
}
|
||||||
|
if (key) {
|
||||||
|
if (!value) {
|
||||||
|
params.push(createSimpleExpression(`_`, false))
|
||||||
|
}
|
||||||
|
params.push(key)
|
||||||
|
}
|
||||||
|
if (index) {
|
||||||
|
if (!key) {
|
||||||
|
if (!value) {
|
||||||
|
params.push(createSimpleExpression(`_`, false))
|
||||||
|
}
|
||||||
|
params.push(createSimpleExpression(`__`, false))
|
||||||
|
}
|
||||||
|
params.push(index)
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
@ -229,8 +229,5 @@ function createChildrenCodegenNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createKeyProperty(index: number): Property {
|
function createKeyProperty(index: number): Property {
|
||||||
return createObjectProperty(
|
return createObjectProperty(`key`, createSimpleExpression(index + '', false))
|
||||||
createSimpleExpression(`key`, true),
|
|
||||||
createSimpleExpression(index + '', false)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -15,23 +15,23 @@ import {
|
|||||||
createConditionalExpression,
|
createConditionalExpression,
|
||||||
ConditionalExpression,
|
ConditionalExpression,
|
||||||
JSChildNode,
|
JSChildNode,
|
||||||
SimpleExpressionNode
|
SimpleExpressionNode,
|
||||||
|
FunctionExpression,
|
||||||
|
CallExpression,
|
||||||
|
createCallExpression,
|
||||||
|
createArrayExpression
|
||||||
} from '../ast'
|
} from '../ast'
|
||||||
import { TransformContext, NodeTransform } from '../transform'
|
import { TransformContext, NodeTransform } from '../transform'
|
||||||
import { createCompilerError, ErrorCodes } from '../errors'
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
import { mergeExpressions, findDir } from '../utils'
|
import { findDir, isTemplateNode, assert, isVSlot } from '../utils'
|
||||||
|
import { CREATE_SLOTS, RENDER_LIST } from '../runtimeConstants'
|
||||||
export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
|
import { parseForExpression, createForLoopParams } from './vFor'
|
||||||
p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
|
|
||||||
|
|
||||||
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
|
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
|
||||||
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
|
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
|
||||||
|
|
||||||
const defaultFallback = createSimpleExpression(`undefined`, false)
|
const defaultFallback = createSimpleExpression(`undefined`, false)
|
||||||
|
|
||||||
const hasSameName = (slot: Property, name: string): boolean =>
|
|
||||||
isStaticExp(slot.key) && slot.key.content === name
|
|
||||||
|
|
||||||
// A NodeTransform that tracks scope identifiers for scoped slots so that they
|
// A NodeTransform that tracks scope identifiers for scoped slots so that they
|
||||||
// don't get prefixed by transformExpression. This transform is only applied
|
// don't get prefixed by transformExpression. This transform is only applied
|
||||||
// in non-browser builds with { prefixIdentifiers: true }
|
// in non-browser builds with { prefixIdentifiers: true }
|
||||||
@ -41,11 +41,42 @@ export const trackSlotScopes: NodeTransform = (node, context) => {
|
|||||||
(node.tagType === ElementTypes.COMPONENT ||
|
(node.tagType === ElementTypes.COMPONENT ||
|
||||||
node.tagType === ElementTypes.TEMPLATE)
|
node.tagType === ElementTypes.TEMPLATE)
|
||||||
) {
|
) {
|
||||||
const vSlot = node.props.find(isVSlot)
|
const vSlot = findDir(node, 'slot')
|
||||||
if (vSlot && vSlot.exp) {
|
if (vSlot) {
|
||||||
context.addIdentifiers(vSlot.exp)
|
const { addIdentifiers, removeIdentifiers } = context
|
||||||
|
const slotProps = vSlot.exp
|
||||||
|
slotProps && addIdentifiers(slotProps)
|
||||||
return () => {
|
return () => {
|
||||||
context.removeIdentifiers(vSlot.exp!)
|
slotProps && removeIdentifiers(slotProps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A NodeTransform that tracks scope identifiers for scoped slots with v-for.
|
||||||
|
// This transform is only applied in non-browser builds with { prefixIdentifiers: true }
|
||||||
|
export const trackVForSlotScopes: NodeTransform = (node, context) => {
|
||||||
|
let vFor
|
||||||
|
if (
|
||||||
|
isTemplateNode(node) &&
|
||||||
|
node.props.some(isVSlot) &&
|
||||||
|
(vFor = findDir(node, 'for'))
|
||||||
|
) {
|
||||||
|
const result = (vFor.parseResult = parseForExpression(
|
||||||
|
vFor.exp as SimpleExpressionNode,
|
||||||
|
context
|
||||||
|
))
|
||||||
|
if (result) {
|
||||||
|
const { value, key, index } = result
|
||||||
|
const { addIdentifiers, removeIdentifiers } = context
|
||||||
|
value && addIdentifiers(value)
|
||||||
|
key && addIdentifiers(key)
|
||||||
|
index && addIdentifiers(index)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
value && removeIdentifiers(value)
|
||||||
|
key && removeIdentifiers(key)
|
||||||
|
index && removeIdentifiers(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,18 +85,20 @@ export const trackSlotScopes: NodeTransform = (node, context) => {
|
|||||||
// Instead of being a DirectiveTransform, v-slot processing is called during
|
// Instead of being a DirectiveTransform, v-slot processing is called during
|
||||||
// transformElement to build the slots object for a component.
|
// transformElement to build the slots object for a component.
|
||||||
export function buildSlots(
|
export function buildSlots(
|
||||||
{ props, children, loc }: ElementNode,
|
node: ElementNode,
|
||||||
context: TransformContext
|
context: TransformContext
|
||||||
): {
|
): {
|
||||||
slots: ObjectExpression
|
slots: ObjectExpression | CallExpression
|
||||||
hasDynamicSlots: boolean
|
hasDynamicSlots: boolean
|
||||||
} {
|
} {
|
||||||
const slots: Property[] = []
|
const { children, loc } = node
|
||||||
|
const slotsProperties: Property[] = []
|
||||||
|
const dynamicSlots: (ConditionalExpression | CallExpression)[] = []
|
||||||
let hasDynamicSlots = false
|
let hasDynamicSlots = false
|
||||||
|
|
||||||
// 1. Check for default slot with slotProps on component itself.
|
// 1. Check for default slot with slotProps on component itself.
|
||||||
// <Comp v-slot="{ prop }"/>
|
// <Comp v-slot="{ prop }"/>
|
||||||
const explicitDefaultSlot = props.find(isVSlot)
|
const explicitDefaultSlot = findDir(node, 'slot', true)
|
||||||
if (explicitDefaultSlot) {
|
if (explicitDefaultSlot) {
|
||||||
const { arg, exp, loc } = explicitDefaultSlot
|
const { arg, exp, loc } = explicitDefaultSlot
|
||||||
if (arg) {
|
if (arg) {
|
||||||
@ -73,7 +106,7 @@ export function buildSlots(
|
|||||||
createCompilerError(ErrorCodes.X_NAMED_SLOT_ON_COMPONENT, loc)
|
createCompilerError(ErrorCodes.X_NAMED_SLOT_ON_COMPONENT, loc)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
slots.push(buildDefaultSlot(exp, children, loc))
|
slotsProperties.push(buildDefaultSlot(exp, children, loc))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Iterate through children and check for template slots
|
// 2. Iterate through children and check for template slots
|
||||||
@ -86,12 +119,13 @@ export function buildSlots(
|
|||||||
let slotDir
|
let slotDir
|
||||||
|
|
||||||
if (
|
if (
|
||||||
slotElement.type !== NodeTypes.ELEMENT ||
|
!isTemplateNode(slotElement) ||
|
||||||
slotElement.tagType !== ElementTypes.TEMPLATE ||
|
!(slotDir = findDir(slotElement, 'slot', true))
|
||||||
!(slotDir = slotElement.props.find(isVSlot))
|
|
||||||
) {
|
) {
|
||||||
// not a <template v-slot>, skip.
|
// not a <template v-slot>, skip.
|
||||||
extraneousChild = extraneousChild || slotElement
|
if (slotElement.type !== NodeTypes.COMMENT && !extraneousChild) {
|
||||||
|
extraneousChild = slotElement
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,76 +160,78 @@ export function buildSlots(
|
|||||||
slotChildren.length ? slotChildren[0].loc : slotLoc
|
slotChildren.length ? slotChildren[0].loc : slotLoc
|
||||||
)
|
)
|
||||||
|
|
||||||
// check if this slot is conditional (v-if/else/else-if)
|
// check if this slot is conditional (v-if/v-for)
|
||||||
let vIf: DirectiveNode | undefined
|
let vIf: DirectiveNode | undefined
|
||||||
let vElse: DirectiveNode | undefined
|
let vElse: DirectiveNode | undefined
|
||||||
|
let vFor: DirectiveNode | undefined
|
||||||
if ((vIf = findDir(slotElement, 'if'))) {
|
if ((vIf = findDir(slotElement, 'if'))) {
|
||||||
hasDynamicSlots = true
|
hasDynamicSlots = true
|
||||||
slots.push(
|
dynamicSlots.push(
|
||||||
createObjectProperty(
|
createConditionalExpression(
|
||||||
slotName,
|
vIf.exp!,
|
||||||
createConditionalExpression(vIf.exp!, slotFunction, defaultFallback)
|
buildDynamicSlot(slotName, slotFunction),
|
||||||
|
defaultFallback
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
(vElse = findDir(slotElement, /^else(-if)?$/, true /* allow empty */))
|
(vElse = findDir(slotElement, /^else(-if)?$/, true /* allowEmpty */))
|
||||||
) {
|
) {
|
||||||
hasDynamicSlots = true
|
// find adjacent v-if
|
||||||
|
let j = i
|
||||||
// find adjacent v-if slot
|
let prev
|
||||||
let baseIfSlot: Property | undefined
|
while (j--) {
|
||||||
let baseIfSlotWithSameName: Property | undefined
|
prev = children[j]
|
||||||
let i = slots.length
|
if (prev.type !== NodeTypes.COMMENT) {
|
||||||
while (i--) {
|
break
|
||||||
if (slots[i].value.type === NodeTypes.JS_CONDITIONAL_EXPRESSION) {
|
|
||||||
baseIfSlot = slots[i]
|
|
||||||
if (staticSlotName && hasSameName(baseIfSlot, staticSlotName)) {
|
|
||||||
baseIfSlotWithSameName = baseIfSlot
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!baseIfSlot) {
|
if (prev && isTemplateNode(prev) && findDir(prev, 'if')) {
|
||||||
context.onError(
|
// remove node
|
||||||
createCompilerError(ErrorCodes.X_ELSE_NO_ADJACENT_IF, vElse.loc)
|
children.splice(i, 1)
|
||||||
)
|
i--
|
||||||
continue
|
__DEV__ && assert(dynamicSlots.length > 0)
|
||||||
}
|
// attach this slot to previous conditional
|
||||||
|
let conditional = dynamicSlots[
|
||||||
if (baseIfSlotWithSameName) {
|
dynamicSlots.length - 1
|
||||||
// v-else branch has same slot name with base v-if branch
|
] as ConditionalExpression
|
||||||
let conditional = baseIfSlotWithSameName.value as ConditionalExpression
|
|
||||||
// locate the deepest conditional in case we have nested ones
|
|
||||||
while (
|
while (
|
||||||
conditional.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
|
conditional.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
|
||||||
) {
|
) {
|
||||||
conditional = conditional.alternate
|
conditional = conditional.alternate
|
||||||
}
|
}
|
||||||
// attach the v-else branch to the base v-if's conditional expression
|
|
||||||
conditional.alternate = vElse.exp
|
conditional.alternate = vElse.exp
|
||||||
? createConditionalExpression(
|
? createConditionalExpression(
|
||||||
vElse.exp,
|
vElse.exp,
|
||||||
slotFunction,
|
buildDynamicSlot(slotName, slotFunction),
|
||||||
defaultFallback
|
defaultFallback
|
||||||
)
|
)
|
||||||
: slotFunction
|
: buildDynamicSlot(slotName, slotFunction)
|
||||||
} else {
|
} else {
|
||||||
// not the same slot name. generate a separate property.
|
context.onError(
|
||||||
slots.push(
|
createCompilerError(ErrorCodes.X_ELSE_NO_ADJACENT_IF, vElse.loc)
|
||||||
createObjectProperty(
|
)
|
||||||
slotName,
|
}
|
||||||
createConditionalExpression(
|
} else if ((vFor = findDir(slotElement, 'for'))) {
|
||||||
// negate base branch condition
|
hasDynamicSlots = true
|
||||||
mergeExpressions(
|
const parseResult =
|
||||||
`!(`,
|
vFor.parseResult ||
|
||||||
(baseIfSlot.value as ConditionalExpression).test,
|
parseForExpression(vFor.exp as SimpleExpressionNode, context)
|
||||||
`)`,
|
if (parseResult) {
|
||||||
...(vElse.exp ? [` && (`, vElse.exp, `)`] : [])
|
// Render the dynamic slots as an array and add it to the createSlot()
|
||||||
),
|
// args. The runtime knows how to handle it appropriately.
|
||||||
slotFunction,
|
dynamicSlots.push(
|
||||||
defaultFallback
|
createCallExpression(context.helper(RENDER_LIST), [
|
||||||
|
parseResult.source,
|
||||||
|
createFunctionExpression(
|
||||||
|
createForLoopParams(parseResult),
|
||||||
|
buildDynamicSlot(slotName, slotFunction),
|
||||||
|
true
|
||||||
)
|
)
|
||||||
)
|
])
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.onError(
|
||||||
|
createCompilerError(ErrorCodes.X_FOR_MALFORMED_EXPRESSION, vFor.loc)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -209,7 +245,7 @@ export function buildSlots(
|
|||||||
}
|
}
|
||||||
seenSlotNames.add(staticSlotName)
|
seenSlotNames.add(staticSlotName)
|
||||||
}
|
}
|
||||||
slots.push(createObjectProperty(slotName, slotFunction))
|
slotsProperties.push(createObjectProperty(slotName, slotFunction))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,11 +260,22 @@ export function buildSlots(
|
|||||||
|
|
||||||
if (!explicitDefaultSlot && !hasTemplateSlots) {
|
if (!explicitDefaultSlot && !hasTemplateSlots) {
|
||||||
// implicit default slot.
|
// implicit default slot.
|
||||||
slots.push(buildDefaultSlot(undefined, children, loc))
|
slotsProperties.push(buildDefaultSlot(undefined, children, loc))
|
||||||
|
}
|
||||||
|
|
||||||
|
let slots: ObjectExpression | CallExpression = createObjectExpression(
|
||||||
|
slotsProperties,
|
||||||
|
loc
|
||||||
|
)
|
||||||
|
if (dynamicSlots.length) {
|
||||||
|
slots = createCallExpression(context.helper(CREATE_SLOTS), [
|
||||||
|
slots,
|
||||||
|
createArrayExpression(dynamicSlots)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slots: createObjectExpression(slots, loc),
|
slots,
|
||||||
hasDynamicSlots
|
hasDynamicSlots
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,7 +286,7 @@ function buildDefaultSlot(
|
|||||||
loc: SourceLocation
|
loc: SourceLocation
|
||||||
): Property {
|
): Property {
|
||||||
return createObjectProperty(
|
return createObjectProperty(
|
||||||
createSimpleExpression(`default`, true),
|
`default`,
|
||||||
createFunctionExpression(
|
createFunctionExpression(
|
||||||
slotProps,
|
slotProps,
|
||||||
children,
|
children,
|
||||||
@ -248,3 +295,13 @@ function buildDefaultSlot(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDynamicSlot(
|
||||||
|
name: ExpressionNode,
|
||||||
|
fn: FunctionExpression
|
||||||
|
): ObjectExpression {
|
||||||
|
return createObjectExpression([
|
||||||
|
createObjectProperty(`name`, name),
|
||||||
|
createObjectProperty(`fn`, fn)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
@ -7,10 +7,10 @@ import {
|
|||||||
SequenceExpression,
|
SequenceExpression,
|
||||||
createSequenceExpression,
|
createSequenceExpression,
|
||||||
createCallExpression,
|
createCallExpression,
|
||||||
ExpressionNode,
|
DirectiveNode,
|
||||||
CompoundExpressionNode,
|
ElementTypes,
|
||||||
createCompoundExpression,
|
TemplateChildNode,
|
||||||
DirectiveNode
|
RootNode
|
||||||
} from './ast'
|
} from './ast'
|
||||||
import { parse } from 'acorn'
|
import { parse } from 'acorn'
|
||||||
import { walk } from 'estree-walker'
|
import { walk } from 'estree-walker'
|
||||||
@ -121,7 +121,7 @@ export function findDir(
|
|||||||
if (
|
if (
|
||||||
p.type === NodeTypes.DIRECTIVE &&
|
p.type === NodeTypes.DIRECTIVE &&
|
||||||
(allowEmpty || p.exp) &&
|
(allowEmpty || p.exp) &&
|
||||||
p.name.match(name)
|
(isString(name) ? p.name === name : name.test(p.name))
|
||||||
) {
|
) {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
@ -160,17 +160,10 @@ export function createBlockExpression(
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeExpressions(
|
export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
|
||||||
...args: (string | ExpressionNode)[]
|
p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
|
||||||
): CompoundExpressionNode {
|
|
||||||
const children: CompoundExpressionNode['children'] = []
|
export const isTemplateNode = (
|
||||||
for (let i = 0; i < args.length; i++) {
|
node: RootNode | TemplateChildNode
|
||||||
const exp = args[i]
|
): node is ElementNode =>
|
||||||
if (isString(exp) || exp.type === NodeTypes.SIMPLE_EXPRESSION) {
|
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
|
||||||
children.push(exp)
|
|
||||||
} else {
|
|
||||||
children.push(...exp.children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return createCompoundExpression(children)
|
|
||||||
}
|
|
||||||
|
26
packages/runtime-core/src/helpers/createSlots.ts
Normal file
26
packages/runtime-core/src/helpers/createSlots.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Slot } from '../componentSlots'
|
||||||
|
import { isArray } from '@vue/shared'
|
||||||
|
|
||||||
|
interface CompiledSlotDescriptor {
|
||||||
|
name: string
|
||||||
|
fn: Slot
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSlots(
|
||||||
|
slots: Record<string, Slot>,
|
||||||
|
dynamicSlots: (CompiledSlotDescriptor | CompiledSlotDescriptor[])[]
|
||||||
|
): Record<string, Slot> {
|
||||||
|
for (let i = 0; i < dynamicSlots.length; i++) {
|
||||||
|
const slot = dynamicSlots[i]
|
||||||
|
// array of dynamic slot generated by <template v-for="..." #[...]>
|
||||||
|
if (isArray(slot)) {
|
||||||
|
for (let j = 0; j < slot.length; j++) {
|
||||||
|
slots[slot[i].name] = slot[i].fn
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// conditional single slot generated by <template v-if="..." #foo>
|
||||||
|
slots[slot.name] = slot.fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slots
|
||||||
|
}
|
@ -43,6 +43,7 @@ export { renderList } from './helpers/renderList'
|
|||||||
export { toString } from './helpers/toString'
|
export { toString } from './helpers/toString'
|
||||||
export { toHandlers } from './helpers/toHandlers'
|
export { toHandlers } from './helpers/toHandlers'
|
||||||
export { renderSlot } from './helpers/renderSlot'
|
export { renderSlot } from './helpers/renderSlot'
|
||||||
|
export { createSlots } from './helpers/createSlots'
|
||||||
export { capitalize, camelize } from '@vue/shared'
|
export { capitalize, camelize } from '@vue/shared'
|
||||||
|
|
||||||
// Internal, for integration with runtime compiler
|
// Internal, for integration with runtime compiler
|
||||||
|
Loading…
x
Reference in New Issue
Block a user