feat(compiler): support v-for on named slots

This commit is contained in:
Evan You 2019-10-02 23:10:41 -04:00
parent f401ac6b88
commit fc47029ed3
18 changed files with 645 additions and 277 deletions

View File

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

View File

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

View File

@ -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\\"
]))
} }
}" }"
`; `;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
}

View File

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