Merge remote-tracking branch 'github/master' into changing_unwrap_ref
# Conflicts: # packages/reactivity/src/ref.ts # packages/runtime-core/__tests__/apiTemplateRef.spec.ts # packages/runtime-core/src/apiWatch.ts
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import { isString } from '@vue/shared'
|
||||
import { ForParseResult } from './transforms/vFor'
|
||||
import {
|
||||
CREATE_VNODE,
|
||||
WITH_DIRECTIVES,
|
||||
RENDER_SLOT,
|
||||
CREATE_SLOTS,
|
||||
RENDER_LIST,
|
||||
OPEN_BLOCK,
|
||||
CREATE_BLOCK,
|
||||
FRAGMENT
|
||||
FRAGMENT,
|
||||
CREATE_VNODE,
|
||||
WITH_DIRECTIVES
|
||||
} from './runtimeHelpers'
|
||||
import { PropsExpression } from './transforms/transformElement'
|
||||
import { ImportItem } from './transform'
|
||||
import { ImportItem, TransformContext } from './transform'
|
||||
|
||||
// Vue template is a platform-agnostic superset of HTML (syntax only).
|
||||
// More namespaces like SVG and MathML are declared by platform specific
|
||||
@@ -38,14 +38,22 @@ export const enum NodeTypes {
|
||||
FOR,
|
||||
TEXT_CALL,
|
||||
// codegen
|
||||
VNODE_CALL,
|
||||
JS_CALL_EXPRESSION,
|
||||
JS_OBJECT_EXPRESSION,
|
||||
JS_PROPERTY,
|
||||
JS_ARRAY_EXPRESSION,
|
||||
JS_FUNCTION_EXPRESSION,
|
||||
JS_SEQUENCE_EXPRESSION,
|
||||
JS_CONDITIONAL_EXPRESSION,
|
||||
JS_CACHE_EXPRESSION
|
||||
JS_CACHE_EXPRESSION,
|
||||
|
||||
// ssr codegen
|
||||
JS_BLOCK_STATEMENT,
|
||||
JS_TEMPLATE_LITERAL,
|
||||
JS_IF_STATEMENT,
|
||||
JS_ASSIGNMENT_EXPRESSION,
|
||||
JS_SEQUENCE_EXPRESSION,
|
||||
JS_RETURN_STATEMENT
|
||||
}
|
||||
|
||||
export const enum ElementTypes {
|
||||
@@ -85,6 +93,7 @@ export type TemplateChildNode =
|
||||
| TextNode
|
||||
| CommentNode
|
||||
| IfNode
|
||||
| IfBranchNode
|
||||
| ForNode
|
||||
| TextCallNode
|
||||
|
||||
@@ -97,7 +106,9 @@ export interface RootNode extends Node {
|
||||
hoists: JSChildNode[]
|
||||
imports: ImportItem[]
|
||||
cached: number
|
||||
codegenNode: TemplateChildNode | JSChildNode | undefined
|
||||
temps: number
|
||||
ssrHelpers?: symbol[]
|
||||
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
|
||||
}
|
||||
|
||||
export type ElementNode =
|
||||
@@ -114,35 +125,40 @@ export interface BaseElementNode extends Node {
|
||||
isSelfClosing: boolean
|
||||
props: Array<AttributeNode | DirectiveNode>
|
||||
children: TemplateChildNode[]
|
||||
codegenNode:
|
||||
| CallExpression
|
||||
| SimpleExpressionNode
|
||||
| CacheExpression
|
||||
| undefined
|
||||
}
|
||||
|
||||
export interface PlainElementNode extends BaseElementNode {
|
||||
tagType: ElementTypes.ELEMENT
|
||||
codegenNode:
|
||||
| ElementCodegenNode
|
||||
| undefined
|
||||
| VNodeCall
|
||||
| SimpleExpressionNode // when hoisted
|
||||
| CacheExpression // when cached by v-once
|
||||
| undefined
|
||||
ssrCodegenNode?: TemplateLiteral
|
||||
}
|
||||
|
||||
export interface ComponentNode extends BaseElementNode {
|
||||
tagType: ElementTypes.COMPONENT
|
||||
codegenNode: ComponentCodegenNode | undefined | CacheExpression // when cached by v-once
|
||||
codegenNode:
|
||||
| VNodeCall
|
||||
| CacheExpression // when cached by v-once
|
||||
| undefined
|
||||
ssrCodegenNode?: CallExpression
|
||||
}
|
||||
|
||||
export interface SlotOutletNode extends BaseElementNode {
|
||||
tagType: ElementTypes.SLOT
|
||||
codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once
|
||||
codegenNode:
|
||||
| RenderSlotCall
|
||||
| CacheExpression // when cached by v-once
|
||||
| undefined
|
||||
ssrCodegenNode?: CallExpression
|
||||
}
|
||||
|
||||
export interface TemplateNode extends BaseElementNode {
|
||||
tagType: ElementTypes.TEMPLATE
|
||||
codegenNode: ElementCodegenNode | undefined | CacheExpression
|
||||
// TemplateNode is a container type that always gets compiled away
|
||||
codegenNode: undefined
|
||||
}
|
||||
|
||||
export interface TextNode extends Node {
|
||||
@@ -190,6 +206,7 @@ export interface CompoundExpressionNode extends Node {
|
||||
type: NodeTypes.COMPOUND_EXPRESSION
|
||||
children: (
|
||||
| SimpleExpressionNode
|
||||
| CompoundExpressionNode
|
||||
| InterpolationNode
|
||||
| TextNode
|
||||
| string
|
||||
@@ -202,7 +219,7 @@ export interface CompoundExpressionNode extends Node {
|
||||
export interface IfNode extends Node {
|
||||
type: NodeTypes.IF
|
||||
branches: IfBranchNode[]
|
||||
codegenNode: IfCodegenNode
|
||||
codegenNode?: IfConditionalExpression
|
||||
}
|
||||
|
||||
export interface IfBranchNode extends Node {
|
||||
@@ -217,28 +234,56 @@ export interface ForNode extends Node {
|
||||
valueAlias: ExpressionNode | undefined
|
||||
keyAlias: ExpressionNode | undefined
|
||||
objectIndexAlias: ExpressionNode | undefined
|
||||
parseResult: ForParseResult
|
||||
children: TemplateChildNode[]
|
||||
codegenNode: ForCodegenNode
|
||||
codegenNode?: ForCodegenNode
|
||||
}
|
||||
|
||||
export interface TextCallNode extends Node {
|
||||
type: NodeTypes.TEXT_CALL
|
||||
content: TextNode | InterpolationNode | CompoundExpressionNode
|
||||
codegenNode: CallExpression
|
||||
codegenNode: CallExpression | SimpleExpressionNode // when hoisted
|
||||
}
|
||||
|
||||
export type TemplateTextChildNode =
|
||||
| TextNode
|
||||
| InterpolationNode
|
||||
| CompoundExpressionNode
|
||||
|
||||
export interface VNodeCall extends Node {
|
||||
type: NodeTypes.VNODE_CALL
|
||||
tag: string | symbol | CallExpression
|
||||
props: PropsExpression | undefined
|
||||
children:
|
||||
| TemplateChildNode[] // multiple children
|
||||
| TemplateTextChildNode // single text child
|
||||
| SlotsExpression // component slots
|
||||
| ForRenderListExpression // v-for fragment call
|
||||
| undefined
|
||||
patchFlag: string | undefined
|
||||
dynamicProps: string | undefined
|
||||
directives: DirectiveArguments | undefined
|
||||
isBlock: boolean
|
||||
isForBlock: boolean
|
||||
}
|
||||
|
||||
// JS Node Types ---------------------------------------------------------------
|
||||
|
||||
// We also include a number of JavaScript AST nodes for code generation.
|
||||
// The AST is an intentionally minimal subset just to meet the exact needs of
|
||||
// Vue render function generation.
|
||||
|
||||
export type JSChildNode =
|
||||
| VNodeCall
|
||||
| CallExpression
|
||||
| ObjectExpression
|
||||
| ArrayExpression
|
||||
| ExpressionNode
|
||||
| FunctionExpression
|
||||
| ConditionalExpression
|
||||
| SequenceExpression
|
||||
| CacheExpression
|
||||
| AssignmentExpression
|
||||
| SequenceExpression
|
||||
|
||||
export interface CallExpression extends Node {
|
||||
type: NodeTypes.JS_CALL_EXPRESSION
|
||||
@@ -247,6 +292,7 @@ export interface CallExpression extends Node {
|
||||
| string
|
||||
| symbol
|
||||
| JSChildNode
|
||||
| SSRCodegenNode
|
||||
| TemplateChildNode
|
||||
| TemplateChildNode[])[]
|
||||
}
|
||||
@@ -269,21 +315,20 @@ export interface ArrayExpression extends Node {
|
||||
|
||||
export interface FunctionExpression extends Node {
|
||||
type: NodeTypes.JS_FUNCTION_EXPRESSION
|
||||
params: ExpressionNode | ExpressionNode[] | undefined
|
||||
returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
|
||||
params: ExpressionNode | string | (ExpressionNode | string)[] | undefined
|
||||
returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode
|
||||
body?: BlockStatement | IfStatement
|
||||
newline: boolean
|
||||
}
|
||||
|
||||
export interface SequenceExpression extends Node {
|
||||
type: NodeTypes.JS_SEQUENCE_EXPRESSION
|
||||
expressions: JSChildNode[]
|
||||
// so that codegen knows it needs to generate ScopeId wrapper
|
||||
isSlot: boolean
|
||||
}
|
||||
|
||||
export interface ConditionalExpression extends Node {
|
||||
type: NodeTypes.JS_CONDITIONAL_EXPRESSION
|
||||
test: ExpressionNode
|
||||
test: JSChildNode
|
||||
consequent: JSChildNode
|
||||
alternate: JSChildNode
|
||||
newline: boolean
|
||||
}
|
||||
|
||||
export interface CacheExpression extends Node {
|
||||
@@ -293,59 +338,76 @@ export interface CacheExpression extends Node {
|
||||
isVNode: boolean
|
||||
}
|
||||
|
||||
// SSR-specific Node Types -----------------------------------------------------
|
||||
|
||||
export type SSRCodegenNode =
|
||||
| BlockStatement
|
||||
| TemplateLiteral
|
||||
| IfStatement
|
||||
| AssignmentExpression
|
||||
| ReturnStatement
|
||||
| SequenceExpression
|
||||
|
||||
export interface BlockStatement extends Node {
|
||||
type: NodeTypes.JS_BLOCK_STATEMENT
|
||||
body: (JSChildNode | IfStatement)[]
|
||||
}
|
||||
|
||||
export interface TemplateLiteral extends Node {
|
||||
type: NodeTypes.JS_TEMPLATE_LITERAL
|
||||
elements: (string | JSChildNode)[]
|
||||
}
|
||||
|
||||
export interface IfStatement extends Node {
|
||||
type: NodeTypes.JS_IF_STATEMENT
|
||||
test: ExpressionNode
|
||||
consequent: BlockStatement
|
||||
alternate: IfStatement | BlockStatement | ReturnStatement | undefined
|
||||
}
|
||||
|
||||
export interface AssignmentExpression extends Node {
|
||||
type: NodeTypes.JS_ASSIGNMENT_EXPRESSION
|
||||
left: SimpleExpressionNode
|
||||
right: JSChildNode
|
||||
}
|
||||
|
||||
export interface SequenceExpression extends Node {
|
||||
type: NodeTypes.JS_SEQUENCE_EXPRESSION
|
||||
expressions: JSChildNode[]
|
||||
}
|
||||
|
||||
export interface ReturnStatement extends Node {
|
||||
type: NodeTypes.JS_RETURN_STATEMENT
|
||||
returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
|
||||
}
|
||||
|
||||
// Codegen Node Types ----------------------------------------------------------
|
||||
|
||||
// createVNode(...)
|
||||
export interface PlainElementCodegenNode extends CallExpression {
|
||||
callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
|
||||
arguments: // tag, props, children, patchFlag, dynamicProps
|
||||
| [string | symbol]
|
||||
| [string | symbol, PropsExpression]
|
||||
| [string | symbol, 'null' | PropsExpression, TemplateChildNode[]]
|
||||
| [
|
||||
string | symbol,
|
||||
'null' | PropsExpression,
|
||||
'null' | TemplateChildNode[],
|
||||
string
|
||||
]
|
||||
| [
|
||||
string | symbol,
|
||||
'null' | PropsExpression,
|
||||
'null' | TemplateChildNode[],
|
||||
string,
|
||||
string
|
||||
]
|
||||
export interface DirectiveArguments extends ArrayExpression {
|
||||
elements: DirectiveArgumentNode[]
|
||||
}
|
||||
|
||||
export type ElementCodegenNode =
|
||||
| PlainElementCodegenNode
|
||||
| CodegenNodeWithDirective<PlainElementCodegenNode>
|
||||
|
||||
// createVNode(...)
|
||||
export interface PlainComponentCodegenNode extends CallExpression {
|
||||
callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
|
||||
arguments: // Comp, props, slots, patchFlag, dynamicProps
|
||||
| [string | symbol]
|
||||
| [string | symbol, PropsExpression]
|
||||
| [string | symbol, 'null' | PropsExpression, SlotsExpression]
|
||||
| [
|
||||
string | symbol,
|
||||
'null' | PropsExpression,
|
||||
'null' | SlotsExpression,
|
||||
string
|
||||
]
|
||||
| [
|
||||
string | symbol,
|
||||
'null' | PropsExpression,
|
||||
'null' | SlotsExpression,
|
||||
string,
|
||||
string
|
||||
]
|
||||
export interface DirectiveArgumentNode extends ArrayExpression {
|
||||
elements: // dir, exp, arg, modifiers
|
||||
| [string]
|
||||
| [string, ExpressionNode]
|
||||
| [string, ExpressionNode, ExpressionNode]
|
||||
| [string, ExpressionNode, ExpressionNode, ObjectExpression]
|
||||
}
|
||||
|
||||
export type ComponentCodegenNode =
|
||||
| PlainComponentCodegenNode
|
||||
| CodegenNodeWithDirective<PlainComponentCodegenNode>
|
||||
// renderSlot(...)
|
||||
export interface RenderSlotCall extends CallExpression {
|
||||
callee: typeof RENDER_SLOT
|
||||
arguments: // $slots, name, props, fallback
|
||||
| [string, string | ExpressionNode]
|
||||
| [string, string | ExpressionNode, PropsExpression]
|
||||
| [
|
||||
string,
|
||||
string | ExpressionNode,
|
||||
PropsExpression | '{}',
|
||||
TemplateChildNode[]
|
||||
]
|
||||
}
|
||||
|
||||
export type SlotsExpression = SlotsObjectExpression | DynamicSlotsExpression
|
||||
|
||||
@@ -397,63 +459,20 @@ export interface DynamicSlotFnProperty extends Property {
|
||||
value: SlotFunctionExpression
|
||||
}
|
||||
|
||||
// withDirectives(createVNode(...), [
|
||||
// [_directive_foo, someValue],
|
||||
// [_directive_bar, someValue, "arg", { mod: true }]
|
||||
// ])
|
||||
export interface CodegenNodeWithDirective<T extends CallExpression>
|
||||
extends CallExpression {
|
||||
callee: typeof WITH_DIRECTIVES
|
||||
arguments: [T, DirectiveArguments]
|
||||
}
|
||||
|
||||
export interface DirectiveArguments extends ArrayExpression {
|
||||
elements: DirectiveArgumentNode[]
|
||||
}
|
||||
|
||||
export interface DirectiveArgumentNode extends ArrayExpression {
|
||||
elements: // dir, exp, arg, modifiers
|
||||
| [string]
|
||||
| [string, ExpressionNode]
|
||||
| [string, ExpressionNode, ExpressionNode]
|
||||
| [string, ExpressionNode, ExpressionNode, ObjectExpression]
|
||||
}
|
||||
|
||||
// renderSlot(...)
|
||||
export interface SlotOutletCodegenNode extends CallExpression {
|
||||
callee: typeof RENDER_SLOT
|
||||
arguments: // $slots, name, props, fallback
|
||||
| [string, string | ExpressionNode]
|
||||
| [string, string | ExpressionNode, PropsExpression]
|
||||
| [
|
||||
string,
|
||||
string | ExpressionNode,
|
||||
PropsExpression | '{}',
|
||||
TemplateChildNode[]
|
||||
]
|
||||
}
|
||||
|
||||
export type BlockCodegenNode =
|
||||
| ElementCodegenNode
|
||||
| ComponentCodegenNode
|
||||
| SlotOutletCodegenNode
|
||||
|
||||
export interface IfCodegenNode extends SequenceExpression {
|
||||
expressions: [OpenBlockExpression, IfConditionalExpression]
|
||||
}
|
||||
export type BlockCodegenNode = VNodeCall | RenderSlotCall
|
||||
|
||||
export interface IfConditionalExpression extends ConditionalExpression {
|
||||
consequent: BlockCodegenNode
|
||||
alternate: BlockCodegenNode | IfConditionalExpression
|
||||
}
|
||||
|
||||
export interface ForCodegenNode extends SequenceExpression {
|
||||
expressions: [OpenBlockExpression, ForBlockCodegenNode]
|
||||
}
|
||||
|
||||
export interface ForBlockCodegenNode extends CallExpression {
|
||||
callee: typeof CREATE_BLOCK
|
||||
arguments: [typeof FRAGMENT, 'null', ForRenderListExpression, string]
|
||||
export interface ForCodegenNode extends VNodeCall {
|
||||
isBlock: true
|
||||
tag: typeof FRAGMENT
|
||||
props: undefined
|
||||
children: ForRenderListExpression
|
||||
patchFlag: string
|
||||
isForBlock: true
|
||||
}
|
||||
|
||||
export interface ForRenderListExpression extends CallExpression {
|
||||
@@ -465,11 +484,6 @@ export interface ForIteratorExpression extends FunctionExpression {
|
||||
returns: BlockCodegenNode
|
||||
}
|
||||
|
||||
export interface OpenBlockExpression extends CallExpression {
|
||||
callee: typeof OPEN_BLOCK
|
||||
arguments: []
|
||||
}
|
||||
|
||||
// AST Utilities ---------------------------------------------------------------
|
||||
|
||||
// Some expressions, e.g. sequence and conditional expressions, are never
|
||||
@@ -481,6 +495,63 @@ export const locStub: SourceLocation = {
|
||||
end: { line: 1, column: 1, offset: 0 }
|
||||
}
|
||||
|
||||
export function createRoot(
|
||||
children: TemplateChildNode[],
|
||||
loc = locStub
|
||||
): RootNode {
|
||||
return {
|
||||
type: NodeTypes.ROOT,
|
||||
children,
|
||||
helpers: [],
|
||||
components: [],
|
||||
directives: [],
|
||||
hoists: [],
|
||||
imports: [],
|
||||
cached: 0,
|
||||
temps: 0,
|
||||
codegenNode: undefined,
|
||||
loc
|
||||
}
|
||||
}
|
||||
|
||||
export function createVNodeCall(
|
||||
context: TransformContext | null,
|
||||
tag: VNodeCall['tag'],
|
||||
props?: VNodeCall['props'],
|
||||
children?: VNodeCall['children'],
|
||||
patchFlag?: VNodeCall['patchFlag'],
|
||||
dynamicProps?: VNodeCall['dynamicProps'],
|
||||
directives?: VNodeCall['directives'],
|
||||
isBlock: VNodeCall['isBlock'] = false,
|
||||
isForBlock: VNodeCall['isForBlock'] = false,
|
||||
loc = locStub
|
||||
): VNodeCall {
|
||||
if (context) {
|
||||
if (isBlock) {
|
||||
context.helper(OPEN_BLOCK)
|
||||
context.helper(CREATE_BLOCK)
|
||||
} else {
|
||||
context.helper(CREATE_VNODE)
|
||||
}
|
||||
if (directives) {
|
||||
context.helper(WITH_DIRECTIVES)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: NodeTypes.VNODE_CALL,
|
||||
tag,
|
||||
props,
|
||||
children,
|
||||
patchFlag,
|
||||
dynamicProps,
|
||||
directives,
|
||||
isBlock,
|
||||
isForBlock,
|
||||
loc
|
||||
}
|
||||
}
|
||||
|
||||
export function createArrayExpression(
|
||||
elements: ArrayExpression['elements'],
|
||||
loc: SourceLocation = locStub
|
||||
@@ -554,15 +625,9 @@ export function createCompoundExpression(
|
||||
}
|
||||
}
|
||||
|
||||
type InferCodegenNodeType<T> = T extends
|
||||
| typeof CREATE_VNODE
|
||||
| typeof CREATE_BLOCK
|
||||
? PlainElementCodegenNode | PlainComponentCodegenNode
|
||||
: T extends typeof WITH_DIRECTIVES
|
||||
?
|
||||
| CodegenNodeWithDirective<PlainElementCodegenNode>
|
||||
| CodegenNodeWithDirective<PlainComponentCodegenNode>
|
||||
: T extends typeof RENDER_SLOT ? SlotOutletCodegenNode : CallExpression
|
||||
type InferCodegenNodeType<T> = T extends typeof RENDER_SLOT
|
||||
? RenderSlotCall
|
||||
: CallExpression
|
||||
|
||||
export function createCallExpression<T extends CallExpression['callee']>(
|
||||
callee: T,
|
||||
@@ -579,8 +644,9 @@ export function createCallExpression<T extends CallExpression['callee']>(
|
||||
|
||||
export function createFunctionExpression(
|
||||
params: FunctionExpression['params'],
|
||||
returns: FunctionExpression['returns'],
|
||||
returns: FunctionExpression['returns'] = undefined,
|
||||
newline: boolean = false,
|
||||
isSlot: boolean = false,
|
||||
loc: SourceLocation = locStub
|
||||
): FunctionExpression {
|
||||
return {
|
||||
@@ -588,30 +654,23 @@ export function createFunctionExpression(
|
||||
params,
|
||||
returns,
|
||||
newline,
|
||||
isSlot,
|
||||
loc
|
||||
}
|
||||
}
|
||||
|
||||
export function createSequenceExpression(
|
||||
expressions: SequenceExpression['expressions']
|
||||
): SequenceExpression {
|
||||
return {
|
||||
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
|
||||
expressions,
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
|
||||
export function createConditionalExpression(
|
||||
test: ConditionalExpression['test'],
|
||||
consequent: ConditionalExpression['consequent'],
|
||||
alternate: ConditionalExpression['alternate']
|
||||
alternate: ConditionalExpression['alternate'],
|
||||
newline = true
|
||||
): ConditionalExpression {
|
||||
return {
|
||||
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
|
||||
test,
|
||||
consequent,
|
||||
alternate,
|
||||
newline,
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
@@ -629,3 +688,69 @@ export function createCacheExpression(
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlockStatement(
|
||||
body: BlockStatement['body']
|
||||
): BlockStatement {
|
||||
return {
|
||||
type: NodeTypes.JS_BLOCK_STATEMENT,
|
||||
body,
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
|
||||
export function createTemplateLiteral(
|
||||
elements: TemplateLiteral['elements']
|
||||
): TemplateLiteral {
|
||||
return {
|
||||
type: NodeTypes.JS_TEMPLATE_LITERAL,
|
||||
elements,
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
|
||||
export function createIfStatement(
|
||||
test: IfStatement['test'],
|
||||
consequent: IfStatement['consequent'],
|
||||
alternate?: IfStatement['alternate']
|
||||
): IfStatement {
|
||||
return {
|
||||
type: NodeTypes.JS_IF_STATEMENT,
|
||||
test,
|
||||
consequent,
|
||||
alternate,
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
|
||||
export function createAssignmentExpression(
|
||||
left: AssignmentExpression['left'],
|
||||
right: AssignmentExpression['right']
|
||||
): AssignmentExpression {
|
||||
return {
|
||||
type: NodeTypes.JS_ASSIGNMENT_EXPRESSION,
|
||||
left,
|
||||
right,
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
|
||||
export function createSequenceExpression(
|
||||
expressions: SequenceExpression['expressions']
|
||||
): SequenceExpression {
|
||||
return {
|
||||
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
|
||||
expressions,
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
|
||||
export function createReturnStatement(
|
||||
returns: ReturnStatement['returns']
|
||||
): ReturnStatement {
|
||||
return {
|
||||
type: NodeTypes.JS_RETURN_STATEMENT,
|
||||
returns,
|
||||
loc: locStub
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,21 @@ import {
|
||||
CallExpression,
|
||||
ArrayExpression,
|
||||
ObjectExpression,
|
||||
SourceLocation,
|
||||
Position,
|
||||
InterpolationNode,
|
||||
CompoundExpressionNode,
|
||||
SimpleExpressionNode,
|
||||
FunctionExpression,
|
||||
SequenceExpression,
|
||||
ConditionalExpression,
|
||||
CacheExpression
|
||||
CacheExpression,
|
||||
locStub,
|
||||
SSRCodegenNode,
|
||||
TemplateLiteral,
|
||||
IfStatement,
|
||||
AssignmentExpression,
|
||||
ReturnStatement,
|
||||
VNodeCall,
|
||||
SequenceExpression
|
||||
} from './ast'
|
||||
import { SourceMapGenerator, RawSourceMap } from 'source-map'
|
||||
import {
|
||||
@@ -31,17 +37,25 @@ import {
|
||||
import { isString, isArray, isSymbol } from '@vue/shared'
|
||||
import {
|
||||
helperNameMap,
|
||||
TO_STRING,
|
||||
TO_DISPLAY_STRING,
|
||||
CREATE_VNODE,
|
||||
RESOLVE_COMPONENT,
|
||||
RESOLVE_DIRECTIVE,
|
||||
SET_BLOCK_TRACKING,
|
||||
CREATE_COMMENT,
|
||||
CREATE_TEXT
|
||||
CREATE_TEXT,
|
||||
PUSH_SCOPE_ID,
|
||||
POP_SCOPE_ID,
|
||||
WITH_SCOPE_ID,
|
||||
WITH_DIRECTIVES,
|
||||
CREATE_BLOCK,
|
||||
OPEN_BLOCK,
|
||||
CREATE_STATIC,
|
||||
WITH_CTX
|
||||
} from './runtimeHelpers'
|
||||
import { ImportItem } from './transform'
|
||||
|
||||
type CodegenNode = TemplateChildNode | JSChildNode
|
||||
type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
|
||||
|
||||
export interface CodegenResult {
|
||||
code: string
|
||||
@@ -58,8 +72,7 @@ export interface CodegenContext extends Required<CodegenOptions> {
|
||||
indentLevel: number
|
||||
map?: SourceMapGenerator
|
||||
helper(key: symbol): string
|
||||
push(code: string, node?: CodegenNode, openOnly?: boolean): void
|
||||
resetMapping(loc: SourceLocation): void
|
||||
push(code: string, node?: CodegenNode): void
|
||||
indent(): void
|
||||
deindent(withoutNewLine?: boolean): void
|
||||
newline(): void
|
||||
@@ -71,7 +84,12 @@ function createCodegenContext(
|
||||
mode = 'function',
|
||||
prefixIdentifiers = mode === 'module',
|
||||
sourceMap = false,
|
||||
filename = `template.vue.html`
|
||||
filename = `template.vue.html`,
|
||||
scopeId = null,
|
||||
optimizeBindings = false,
|
||||
runtimeGlobalName = `Vue`,
|
||||
runtimeModuleName = `vue`,
|
||||
ssr = false
|
||||
}: CodegenOptions
|
||||
): CodegenContext {
|
||||
const context: CodegenContext = {
|
||||
@@ -79,24 +97,22 @@ function createCodegenContext(
|
||||
prefixIdentifiers,
|
||||
sourceMap,
|
||||
filename,
|
||||
scopeId,
|
||||
optimizeBindings,
|
||||
runtimeGlobalName,
|
||||
runtimeModuleName,
|
||||
ssr,
|
||||
source: ast.loc.source,
|
||||
code: ``,
|
||||
column: 1,
|
||||
line: 1,
|
||||
offset: 0,
|
||||
indentLevel: 0,
|
||||
|
||||
// lazy require source-map implementation, only in non-browser builds!
|
||||
map:
|
||||
__BROWSER__ || !sourceMap
|
||||
? undefined
|
||||
: new (loadDep('source-map')).SourceMapGenerator(),
|
||||
|
||||
map: undefined,
|
||||
helper(key) {
|
||||
const name = helperNameMap[key]
|
||||
return prefixIdentifiers ? name : `_${name}`
|
||||
return `_${helperNameMap[key]}`
|
||||
},
|
||||
push(code, node, openOnly) {
|
||||
push(code, node) {
|
||||
context.code += code
|
||||
if (!__BROWSER__ && context.map) {
|
||||
if (node) {
|
||||
@@ -110,16 +126,11 @@ function createCodegenContext(
|
||||
addMapping(node.loc.start, name)
|
||||
}
|
||||
advancePositionWithMutation(context, code)
|
||||
if (node && !openOnly) {
|
||||
if (node && node.loc !== locStub) {
|
||||
addMapping(node.loc.end)
|
||||
}
|
||||
}
|
||||
},
|
||||
resetMapping(loc: SourceLocation) {
|
||||
if (!__BROWSER__ && context.map) {
|
||||
addMapping(loc.start)
|
||||
}
|
||||
},
|
||||
indent() {
|
||||
newline(++context.indentLevel)
|
||||
},
|
||||
@@ -154,9 +165,12 @@ function createCodegenContext(
|
||||
})
|
||||
}
|
||||
|
||||
if (!__BROWSER__ && context.map) {
|
||||
context.map.setSourceContent(filename, context.source)
|
||||
if (!__BROWSER__ && sourceMap) {
|
||||
// lazy require source-map implementation, only in non-browser builds
|
||||
context.map = new (loadDep('source-map')).SourceMapGenerator()
|
||||
context.map!.setSourceContent(filename, context.source)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -168,63 +182,37 @@ export function generate(
|
||||
const {
|
||||
mode,
|
||||
push,
|
||||
helper,
|
||||
prefixIdentifiers,
|
||||
indent,
|
||||
deindent,
|
||||
newline
|
||||
newline,
|
||||
scopeId,
|
||||
ssr
|
||||
} = context
|
||||
const hasHelpers = ast.helpers.length > 0
|
||||
const useWithBlock = !prefixIdentifiers && mode !== 'module'
|
||||
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
|
||||
|
||||
// preambles
|
||||
if (mode === 'function') {
|
||||
// Generate const declaration for helpers
|
||||
// In prefix mode, we place the const declaration at top so it's done
|
||||
// only once; But if we not prefixing, we place the declaration inside the
|
||||
// with block so it doesn't incur the `in` check cost for every helper access.
|
||||
if (hasHelpers) {
|
||||
if (prefixIdentifiers) {
|
||||
push(`const { ${ast.helpers.map(helper).join(', ')} } = Vue\n`)
|
||||
} else {
|
||||
// "with" mode.
|
||||
// save Vue in a separate variable to avoid collision
|
||||
push(`const _Vue = Vue\n`)
|
||||
// in "with" mode, helpers are declared inside the with block to avoid
|
||||
// has check cost, but hoists are lifted out of the function - we need
|
||||
// to provide the helper here.
|
||||
if (ast.hoists.length) {
|
||||
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
|
||||
.filter(helper => ast.helpers.includes(helper))
|
||||
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
|
||||
.join(', ')
|
||||
push(`const { ${staticHelpers} } = Vue\n`)
|
||||
}
|
||||
}
|
||||
}
|
||||
genHoists(ast.hoists, context)
|
||||
newline()
|
||||
push(`return `)
|
||||
if (!__BROWSER__ && mode === 'module') {
|
||||
genModulePreamble(ast, context, genScopeId)
|
||||
} else {
|
||||
// generate import statements for helpers
|
||||
if (hasHelpers) {
|
||||
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
|
||||
}
|
||||
if (ast.imports.length) {
|
||||
genImports(ast.imports, context)
|
||||
newline()
|
||||
}
|
||||
genHoists(ast.hoists, context)
|
||||
newline()
|
||||
push(`export default `)
|
||||
genFunctionPreamble(ast, context)
|
||||
}
|
||||
|
||||
// enter render function
|
||||
push(`function render() {`)
|
||||
if (genScopeId && !ssr) {
|
||||
push(`const render = _withId(`)
|
||||
}
|
||||
if (!ssr) {
|
||||
push(`function render(_ctx, _cache) {`)
|
||||
} else {
|
||||
push(`function ssrRender(_ctx, _push, _parent) {`)
|
||||
}
|
||||
indent()
|
||||
|
||||
if (useWithBlock) {
|
||||
push(`with (this) {`)
|
||||
push(`with (_ctx) {`)
|
||||
indent()
|
||||
// function mode const declarations should be inside with block
|
||||
// also they should be renamed to avoid collision with user properties
|
||||
@@ -234,35 +222,39 @@ export function generate(
|
||||
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
|
||||
.join(', ')} } = _Vue`
|
||||
)
|
||||
newline()
|
||||
if (ast.cached > 0) {
|
||||
push(`const _cache = $cache`)
|
||||
newline()
|
||||
}
|
||||
push(`\n`)
|
||||
newline()
|
||||
}
|
||||
} else {
|
||||
push(`const _ctx = this`)
|
||||
if (ast.cached > 0) {
|
||||
newline()
|
||||
push(`const _cache = _ctx.$cache`)
|
||||
}
|
||||
newline()
|
||||
}
|
||||
|
||||
// generate asset resolution statements
|
||||
if (ast.components.length) {
|
||||
genAssets(ast.components, 'component', context)
|
||||
if (ast.directives.length || ast.temps > 0) {
|
||||
newline()
|
||||
}
|
||||
}
|
||||
if (ast.directives.length) {
|
||||
genAssets(ast.directives, 'directive', context)
|
||||
if (ast.temps > 0) {
|
||||
newline()
|
||||
}
|
||||
}
|
||||
if (ast.components.length || ast.directives.length) {
|
||||
if (ast.temps > 0) {
|
||||
push(`let `)
|
||||
for (let i = 0; i < ast.temps; i++) {
|
||||
push(`${i > 0 ? `, ` : ``}_temp${i}`)
|
||||
}
|
||||
}
|
||||
if (ast.components.length || ast.directives.length || ast.temps) {
|
||||
push(`\n`)
|
||||
newline()
|
||||
}
|
||||
|
||||
// generate the VNode tree expression
|
||||
push(`return `)
|
||||
if (!ssr) {
|
||||
push(`return `)
|
||||
}
|
||||
if (ast.codegenNode) {
|
||||
genNode(ast.codegenNode, context)
|
||||
} else {
|
||||
@@ -276,27 +268,164 @@ export function generate(
|
||||
|
||||
deindent()
|
||||
push(`}`)
|
||||
|
||||
if (genScopeId && !ssr) {
|
||||
push(`)`)
|
||||
}
|
||||
|
||||
return {
|
||||
ast,
|
||||
code: context.code,
|
||||
map: context.map ? context.map.toJSON() : undefined
|
||||
// SourceMapGenerator does have toJSON() method but it's not in the types
|
||||
map: context.map ? (context.map as any).toJSON() : undefined
|
||||
}
|
||||
}
|
||||
|
||||
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
|
||||
const {
|
||||
ssr,
|
||||
prefixIdentifiers,
|
||||
push,
|
||||
newline,
|
||||
runtimeModuleName,
|
||||
runtimeGlobalName
|
||||
} = context
|
||||
const VueBinding =
|
||||
!__BROWSER__ && ssr
|
||||
? `require(${JSON.stringify(runtimeModuleName)})`
|
||||
: runtimeGlobalName
|
||||
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
|
||||
// Generate const declaration for helpers
|
||||
// In prefix mode, we place the const declaration at top so it's done
|
||||
// only once; But if we not prefixing, we place the declaration inside the
|
||||
// with block so it doesn't incur the `in` check cost for every helper access.
|
||||
if (ast.helpers.length > 0) {
|
||||
if (!__BROWSER__ && prefixIdentifiers) {
|
||||
push(
|
||||
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`
|
||||
)
|
||||
} else {
|
||||
// "with" mode.
|
||||
// save Vue in a separate variable to avoid collision
|
||||
push(`const _Vue = ${VueBinding}\n`)
|
||||
// in "with" mode, helpers are declared inside the with block to avoid
|
||||
// has check cost, but hoists are lifted out of the function - we need
|
||||
// to provide the helper here.
|
||||
if (ast.hoists.length) {
|
||||
const staticHelpers = [
|
||||
CREATE_VNODE,
|
||||
CREATE_COMMENT,
|
||||
CREATE_TEXT,
|
||||
CREATE_STATIC
|
||||
]
|
||||
.filter(helper => ast.helpers.includes(helper))
|
||||
.map(aliasHelper)
|
||||
.join(', ')
|
||||
push(`const { ${staticHelpers} } = _Vue\n`)
|
||||
}
|
||||
}
|
||||
}
|
||||
// generate variables for ssr helpers
|
||||
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
|
||||
// ssr guaruntees prefixIdentifier: true
|
||||
push(
|
||||
`const { ${ast.ssrHelpers
|
||||
.map(aliasHelper)
|
||||
.join(', ')} } = require("@vue/server-renderer")\n`
|
||||
)
|
||||
}
|
||||
genHoists(ast.hoists, context)
|
||||
newline()
|
||||
push(`return `)
|
||||
}
|
||||
|
||||
function genModulePreamble(
|
||||
ast: RootNode,
|
||||
context: CodegenContext,
|
||||
genScopeId: boolean
|
||||
) {
|
||||
const {
|
||||
push,
|
||||
helper,
|
||||
newline,
|
||||
scopeId,
|
||||
optimizeBindings,
|
||||
runtimeModuleName
|
||||
} = context
|
||||
|
||||
if (genScopeId) {
|
||||
ast.helpers.push(WITH_SCOPE_ID)
|
||||
if (ast.hoists.length) {
|
||||
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
// generate import statements for helpers
|
||||
if (ast.helpers.length) {
|
||||
if (optimizeBindings) {
|
||||
// when bundled with webpack with code-split, calling an import binding
|
||||
// as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,
|
||||
// incurring both payload size increase and potential perf overhead.
|
||||
// therefore we assign the imports to vairables (which is a constant ~50b
|
||||
// cost per-component instead of scaling with template size)
|
||||
push(
|
||||
`import { ${ast.helpers
|
||||
.map(s => helperNameMap[s])
|
||||
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
|
||||
)
|
||||
push(
|
||||
`\n// Binding optimization for webpack code-split\nconst ${ast.helpers
|
||||
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
|
||||
.join(', ')}\n`
|
||||
)
|
||||
} else {
|
||||
push(
|
||||
`import { ${ast.helpers
|
||||
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
|
||||
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (ast.ssrHelpers && ast.ssrHelpers.length) {
|
||||
push(
|
||||
`import { ${ast.ssrHelpers
|
||||
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
|
||||
.join(', ')} } from "@vue/server-renderer"\n`
|
||||
)
|
||||
}
|
||||
|
||||
if (ast.imports.length) {
|
||||
genImports(ast.imports, context)
|
||||
newline()
|
||||
}
|
||||
|
||||
if (genScopeId) {
|
||||
push(`const _withId = ${helper(WITH_SCOPE_ID)}("${scopeId}")`)
|
||||
newline()
|
||||
}
|
||||
|
||||
genHoists(ast.hoists, context)
|
||||
newline()
|
||||
push(`export `)
|
||||
}
|
||||
|
||||
function genAssets(
|
||||
assets: string[],
|
||||
type: 'component' | 'directive',
|
||||
context: CodegenContext
|
||||
{ helper, push, newline }: CodegenContext
|
||||
) {
|
||||
const resolver = context.helper(
|
||||
const resolver = helper(
|
||||
type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE
|
||||
)
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
const id = assets[i]
|
||||
context.push(
|
||||
push(
|
||||
`const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})`
|
||||
)
|
||||
context.newline()
|
||||
if (i < assets.length - 1) {
|
||||
newline()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,12 +433,27 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
|
||||
if (!hoists.length) {
|
||||
return
|
||||
}
|
||||
context.newline()
|
||||
const { push, newline, helper, scopeId, mode } = context
|
||||
const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
|
||||
newline()
|
||||
|
||||
// push scope Id before initilaizing hoisted vnodes so that these vnodes
|
||||
// get the proper scopeId as well.
|
||||
if (genScopeId) {
|
||||
push(`${helper(PUSH_SCOPE_ID)}("${scopeId}")`)
|
||||
newline()
|
||||
}
|
||||
|
||||
hoists.forEach((exp, i) => {
|
||||
context.push(`const _hoisted_${i + 1} = `)
|
||||
push(`const _hoisted_${i + 1} = `)
|
||||
genNode(exp, context)
|
||||
context.newline()
|
||||
newline()
|
||||
})
|
||||
|
||||
if (genScopeId) {
|
||||
push(`${helper(POP_SCOPE_ID)}()`)
|
||||
newline()
|
||||
}
|
||||
}
|
||||
|
||||
function genImports(importsOptions: ImportItem[], context: CodegenContext) {
|
||||
@@ -351,7 +495,8 @@ function genNodeListAsArray(
|
||||
function genNodeList(
|
||||
nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
|
||||
context: CodegenContext,
|
||||
multilines: boolean = false
|
||||
multilines: boolean = false,
|
||||
comma: boolean = true
|
||||
) {
|
||||
const { push, newline } = context
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
@@ -365,10 +510,10 @@ function genNodeList(
|
||||
}
|
||||
if (i < nodes.length - 1) {
|
||||
if (multilines) {
|
||||
push(',')
|
||||
comma && push(',')
|
||||
newline()
|
||||
} else {
|
||||
push(', ')
|
||||
comma && push(', ')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,6 +558,10 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
|
||||
case NodeTypes.COMMENT:
|
||||
genComment(node, context)
|
||||
break
|
||||
case NodeTypes.VNODE_CALL:
|
||||
genVNodeCall(node, context)
|
||||
break
|
||||
|
||||
case NodeTypes.JS_CALL_EXPRESSION:
|
||||
genCallExpression(node, context)
|
||||
break
|
||||
@@ -425,16 +574,37 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
|
||||
case NodeTypes.JS_FUNCTION_EXPRESSION:
|
||||
genFunctionExpression(node, context)
|
||||
break
|
||||
case NodeTypes.JS_SEQUENCE_EXPRESSION:
|
||||
genSequenceExpression(node, context)
|
||||
break
|
||||
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
|
||||
genConditionalExpression(node, context)
|
||||
break
|
||||
case NodeTypes.JS_CACHE_EXPRESSION:
|
||||
genCacheExpression(node, context)
|
||||
break
|
||||
|
||||
// SSR only types
|
||||
case NodeTypes.JS_BLOCK_STATEMENT:
|
||||
!__BROWSER__ && genNodeList(node.body, context, true, false)
|
||||
break
|
||||
case NodeTypes.JS_TEMPLATE_LITERAL:
|
||||
!__BROWSER__ && genTemplateLiteral(node, context)
|
||||
break
|
||||
case NodeTypes.JS_IF_STATEMENT:
|
||||
!__BROWSER__ && genIfStatement(node, context)
|
||||
break
|
||||
case NodeTypes.JS_ASSIGNMENT_EXPRESSION:
|
||||
!__BROWSER__ && genAssignmentExpression(node, context)
|
||||
break
|
||||
case NodeTypes.JS_SEQUENCE_EXPRESSION:
|
||||
!__BROWSER__ && genSequenceExpression(node, context)
|
||||
break
|
||||
case NodeTypes.JS_RETURN_STATEMENT:
|
||||
!__BROWSER__ && genReturnStatement(node, context)
|
||||
break
|
||||
|
||||
/* istanbul ignore next */
|
||||
case NodeTypes.IF_BRANCH:
|
||||
// noop
|
||||
break
|
||||
default:
|
||||
if (__DEV__) {
|
||||
assert(false, `unhandled codegen node type: ${(node as any).type}`)
|
||||
@@ -459,7 +629,7 @@ function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
|
||||
|
||||
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
|
||||
const { push, helper } = context
|
||||
push(`${helper(TO_STRING)}(`)
|
||||
push(`${helper(TO_DISPLAY_STRING)}(`)
|
||||
genNode(node.content, context)
|
||||
push(`)`)
|
||||
}
|
||||
@@ -505,18 +675,60 @@ function genComment(node: CommentNode, context: CodegenContext) {
|
||||
}
|
||||
}
|
||||
|
||||
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
|
||||
const { push, helper } = context
|
||||
const {
|
||||
tag,
|
||||
props,
|
||||
children,
|
||||
patchFlag,
|
||||
dynamicProps,
|
||||
directives,
|
||||
isBlock,
|
||||
isForBlock
|
||||
} = node
|
||||
if (directives) {
|
||||
push(helper(WITH_DIRECTIVES) + `(`)
|
||||
}
|
||||
if (isBlock) {
|
||||
push(`(${helper(OPEN_BLOCK)}(${isForBlock ? `true` : ``}), `)
|
||||
}
|
||||
push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + `(`, node)
|
||||
genNodeList(
|
||||
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
|
||||
context
|
||||
)
|
||||
push(`)`)
|
||||
if (isBlock) {
|
||||
push(`)`)
|
||||
}
|
||||
if (directives) {
|
||||
push(`, `)
|
||||
genNode(directives, context)
|
||||
push(`)`)
|
||||
}
|
||||
}
|
||||
|
||||
function genNullableArgs(args: any[]): CallExpression['arguments'] {
|
||||
let i = args.length
|
||||
while (i--) {
|
||||
if (args[i] != null) break
|
||||
}
|
||||
return args.slice(0, i + 1).map(arg => arg || `null`)
|
||||
}
|
||||
|
||||
// JavaScript
|
||||
function genCallExpression(node: CallExpression, context: CodegenContext) {
|
||||
const callee = isString(node.callee)
|
||||
? node.callee
|
||||
: context.helper(node.callee)
|
||||
context.push(callee + `(`, node, true)
|
||||
context.push(callee + `(`, node)
|
||||
genNodeList(node.arguments, context)
|
||||
context.push(`)`)
|
||||
}
|
||||
|
||||
function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
|
||||
const { push, indent, deindent, newline, resetMapping } = context
|
||||
const { push, indent, deindent, newline } = context
|
||||
const { properties } = node
|
||||
if (!properties.length) {
|
||||
push(`{}`, node)
|
||||
@@ -529,8 +741,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
|
||||
push(multilines ? `{` : `{ `)
|
||||
multilines && indent()
|
||||
for (let i = 0; i < properties.length; i++) {
|
||||
const { key, value, loc } = properties[i]
|
||||
resetMapping(loc) // reset source mapping for every property.
|
||||
const { key, value } = properties[i]
|
||||
// key
|
||||
genExpressionAsPropertyKey(key, context)
|
||||
push(`: `)
|
||||
@@ -554,8 +765,17 @@ function genFunctionExpression(
|
||||
node: FunctionExpression,
|
||||
context: CodegenContext
|
||||
) {
|
||||
const { push, indent, deindent } = context
|
||||
const { params, returns, newline } = node
|
||||
const { push, indent, deindent, scopeId, mode } = context
|
||||
const { params, returns, body, newline, isSlot } = node
|
||||
// slot functions also need to push scopeId before rendering its content
|
||||
const genScopeId =
|
||||
!__BROWSER__ && isSlot && scopeId != null && mode !== 'function'
|
||||
|
||||
if (genScopeId) {
|
||||
push(`_withId(`)
|
||||
} else if (isSlot) {
|
||||
push(`_${helperNameMap[WITH_CTX]}(`)
|
||||
}
|
||||
push(`(`, node)
|
||||
if (isArray(params)) {
|
||||
genNodeList(params, context)
|
||||
@@ -563,27 +783,36 @@ function genFunctionExpression(
|
||||
genNode(params, context)
|
||||
}
|
||||
push(`) => `)
|
||||
if (newline) {
|
||||
if (newline || body) {
|
||||
push(`{`)
|
||||
indent()
|
||||
push(`return `)
|
||||
}
|
||||
if (isArray(returns)) {
|
||||
genNodeListAsArray(returns, context)
|
||||
} else {
|
||||
genNode(returns, context)
|
||||
if (returns) {
|
||||
if (newline) {
|
||||
push(`return `)
|
||||
}
|
||||
if (isArray(returns)) {
|
||||
genNodeListAsArray(returns, context)
|
||||
} else {
|
||||
genNode(returns, context)
|
||||
}
|
||||
} else if (body) {
|
||||
genNode(body, context)
|
||||
}
|
||||
if (newline) {
|
||||
if (newline || body) {
|
||||
deindent()
|
||||
push(`}`)
|
||||
}
|
||||
if (genScopeId || isSlot) {
|
||||
push(`)`)
|
||||
}
|
||||
}
|
||||
|
||||
function genConditionalExpression(
|
||||
node: ConditionalExpression,
|
||||
context: CodegenContext
|
||||
) {
|
||||
const { test, consequent, alternate } = node
|
||||
const { test, consequent, alternate, newline: needNewline } = node
|
||||
const { push, indent, deindent, newline } = context
|
||||
if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
|
||||
const needsParens = !isSimpleIdentifier(test.content)
|
||||
@@ -592,15 +821,17 @@ function genConditionalExpression(
|
||||
needsParens && push(`)`)
|
||||
} else {
|
||||
push(`(`)
|
||||
genCompoundExpression(test, context)
|
||||
genNode(test, context)
|
||||
push(`)`)
|
||||
}
|
||||
indent()
|
||||
needNewline && indent()
|
||||
context.indentLevel++
|
||||
needNewline || push(` `)
|
||||
push(`? `)
|
||||
genNode(consequent, context)
|
||||
context.indentLevel--
|
||||
newline()
|
||||
needNewline && newline()
|
||||
needNewline || push(` `)
|
||||
push(`: `)
|
||||
const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
|
||||
if (!isNested) {
|
||||
@@ -610,16 +841,7 @@ function genConditionalExpression(
|
||||
if (!isNested) {
|
||||
context.indentLevel--
|
||||
}
|
||||
deindent(true /* without newline */)
|
||||
}
|
||||
|
||||
function genSequenceExpression(
|
||||
node: SequenceExpression,
|
||||
context: CodegenContext
|
||||
) {
|
||||
context.push(`(`)
|
||||
genNodeList(node.expressions, context)
|
||||
context.push(`)`)
|
||||
needNewline && deindent(true /* without newline */)
|
||||
}
|
||||
|
||||
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
|
||||
@@ -642,3 +864,77 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
|
||||
}
|
||||
push(`)`)
|
||||
}
|
||||
|
||||
function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
|
||||
const { push, indent, deindent } = context
|
||||
push('`')
|
||||
const l = node.elements.length
|
||||
const multilines = l > 3
|
||||
for (let i = 0; i < l; i++) {
|
||||
const e = node.elements[i]
|
||||
if (isString(e)) {
|
||||
push(e.replace(/`/g, '\\`'))
|
||||
} else {
|
||||
push('${')
|
||||
if (multilines) indent()
|
||||
genNode(e, context)
|
||||
if (multilines) deindent()
|
||||
push('}')
|
||||
}
|
||||
}
|
||||
push('`')
|
||||
}
|
||||
|
||||
function genIfStatement(node: IfStatement, context: CodegenContext) {
|
||||
const { push, indent, deindent } = context
|
||||
const { test, consequent, alternate } = node
|
||||
push(`if (`)
|
||||
genNode(test, context)
|
||||
push(`) {`)
|
||||
indent()
|
||||
genNode(consequent, context)
|
||||
deindent()
|
||||
push(`}`)
|
||||
if (alternate) {
|
||||
push(` else `)
|
||||
if (alternate.type === NodeTypes.JS_IF_STATEMENT) {
|
||||
genIfStatement(alternate, context)
|
||||
} else {
|
||||
push(`{`)
|
||||
indent()
|
||||
genNode(alternate, context)
|
||||
deindent()
|
||||
push(`}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function genAssignmentExpression(
|
||||
node: AssignmentExpression,
|
||||
context: CodegenContext
|
||||
) {
|
||||
genNode(node.left, context)
|
||||
context.push(` = `)
|
||||
genNode(node.right, context)
|
||||
}
|
||||
|
||||
function genSequenceExpression(
|
||||
node: SequenceExpression,
|
||||
context: CodegenContext
|
||||
) {
|
||||
context.push(`(`)
|
||||
genNodeList(node.expressions, context)
|
||||
context.push(`)`)
|
||||
}
|
||||
|
||||
function genReturnStatement(
|
||||
{ returns }: ReturnStatement,
|
||||
context: CodegenContext
|
||||
) {
|
||||
context.push(`return `)
|
||||
if (isArray(returns)) {
|
||||
genNodeListAsArray(returns, context)
|
||||
} else {
|
||||
genNode(returns, context)
|
||||
}
|
||||
}
|
||||
|
||||
100
packages/compiler-core/src/compile.ts
Normal file
100
packages/compiler-core/src/compile.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { CompilerOptions } from './options'
|
||||
import { baseParse } from './parse'
|
||||
import { transform, NodeTransform, DirectiveTransform } from './transform'
|
||||
import { generate, CodegenResult } from './codegen'
|
||||
import { RootNode } from './ast'
|
||||
import { isString } from '@vue/shared'
|
||||
import { transformIf } from './transforms/vIf'
|
||||
import { transformFor } from './transforms/vFor'
|
||||
import { transformExpression } from './transforms/transformExpression'
|
||||
import { transformSlotOutlet } from './transforms/transformSlotOutlet'
|
||||
import { transformElement } from './transforms/transformElement'
|
||||
import { transformOn } from './transforms/vOn'
|
||||
import { transformBind } from './transforms/vBind'
|
||||
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
|
||||
import { transformText } from './transforms/transformText'
|
||||
import { transformOnce } from './transforms/vOnce'
|
||||
import { transformModel } from './transforms/vModel'
|
||||
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
|
||||
|
||||
export type TransformPreset = [
|
||||
NodeTransform[],
|
||||
Record<string, DirectiveTransform>
|
||||
]
|
||||
|
||||
export function getBaseTransformPreset(
|
||||
prefixIdentifiers?: boolean
|
||||
): TransformPreset {
|
||||
return [
|
||||
[
|
||||
transformOnce,
|
||||
transformIf,
|
||||
transformFor,
|
||||
...(!__BROWSER__ && prefixIdentifiers
|
||||
? [
|
||||
// order is important
|
||||
trackVForSlotScopes,
|
||||
transformExpression
|
||||
]
|
||||
: []),
|
||||
transformSlotOutlet,
|
||||
transformElement,
|
||||
trackSlotScopes,
|
||||
transformText
|
||||
],
|
||||
{
|
||||
on: transformOn,
|
||||
bind: transformBind,
|
||||
model: transformModel
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// we name it `baseCompile` so that higher order compilers like
|
||||
// @vue/compiler-dom can export `compile` while re-exporting everything else.
|
||||
export function baseCompile(
|
||||
template: string | RootNode,
|
||||
options: CompilerOptions = {}
|
||||
): CodegenResult {
|
||||
const onError = options.onError || defaultOnError
|
||||
const isModuleMode = options.mode === 'module'
|
||||
/* istanbul ignore if */
|
||||
if (__BROWSER__) {
|
||||
if (options.prefixIdentifiers === true) {
|
||||
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
|
||||
} else if (isModuleMode) {
|
||||
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
|
||||
}
|
||||
}
|
||||
|
||||
const prefixIdentifiers =
|
||||
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
|
||||
if (!prefixIdentifiers && options.cacheHandlers) {
|
||||
onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
|
||||
}
|
||||
if (options.scopeId && !isModuleMode) {
|
||||
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
|
||||
}
|
||||
|
||||
const ast = isString(template) ? baseParse(template, options) : template
|
||||
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
|
||||
prefixIdentifiers
|
||||
)
|
||||
transform(ast, {
|
||||
...options,
|
||||
prefixIdentifiers,
|
||||
nodeTransforms: [
|
||||
...nodeTransforms,
|
||||
...(options.nodeTransforms || []) // user transforms
|
||||
],
|
||||
directiveTransforms: {
|
||||
...directiveTransforms,
|
||||
...(options.directiveTransforms || {}) // user transforms
|
||||
}
|
||||
})
|
||||
|
||||
return generate(ast, {
|
||||
...options,
|
||||
prefixIdentifiers
|
||||
})
|
||||
}
|
||||
@@ -16,11 +16,14 @@ export function defaultOnError(error: CompilerError) {
|
||||
export function createCompilerError<T extends number>(
|
||||
code: T,
|
||||
loc?: SourceLocation,
|
||||
messages?: { [code: number]: string }
|
||||
messages?: { [code: number]: string },
|
||||
additionalMessage?: string
|
||||
): T extends ErrorCodes ? CoreCompilerError : CompilerError {
|
||||
const msg = __DEV__ || !__BROWSER__ ? (messages || errorMessages)[code] : code
|
||||
const locInfo = loc ? ` (${loc.start.line}:${loc.start.column})` : ``
|
||||
const error = new SyntaxError(msg + locInfo) as CompilerError
|
||||
const msg =
|
||||
__DEV__ || !__BROWSER__
|
||||
? (messages || errorMessages)[code] + (additionalMessage || ``)
|
||||
: code
|
||||
const error = new SyntaxError(String(msg)) as CompilerError
|
||||
error.code = code
|
||||
error.loc = loc
|
||||
return error as any
|
||||
@@ -58,7 +61,6 @@ export const enum ErrorCodes {
|
||||
UNEXPECTED_NULL_CHARACTER,
|
||||
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
|
||||
UNEXPECTED_SOLIDUS_IN_TAG,
|
||||
UNKNOWN_NAMED_CHARACTER_REFERENCE,
|
||||
|
||||
// Vue-specific parse errors
|
||||
X_INVALID_END_TAG,
|
||||
@@ -74,19 +76,21 @@ export const enum ErrorCodes {
|
||||
X_V_BIND_NO_EXPRESSION,
|
||||
X_V_ON_NO_EXPRESSION,
|
||||
X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
|
||||
X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
|
||||
X_V_SLOT_MIXED_SLOT_USAGE,
|
||||
X_V_SLOT_DUPLICATE_SLOT_NAMES,
|
||||
X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
|
||||
X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
|
||||
X_V_SLOT_MISPLACED,
|
||||
X_V_MODEL_NO_EXPRESSION,
|
||||
X_V_MODEL_MALFORMED_EXPRESSION,
|
||||
X_V_MODEL_ON_SCOPE_VARIABLE,
|
||||
X_INVALID_EXPRESSION,
|
||||
X_KEEP_ALIVE_INVALID_CHILDREN,
|
||||
|
||||
// generic errors
|
||||
X_PREFIX_ID_NOT_SUPPORTED,
|
||||
X_MODULE_MODE_NOT_SUPPORTED,
|
||||
X_CACHE_HANDLER_NOT_SUPPORTED,
|
||||
X_SCOPE_ID_NOT_SUPPORTED,
|
||||
|
||||
// Special value for higher-order compilers to pick up the last code
|
||||
// to avoid collision of error codes. This should always be kept as the last
|
||||
@@ -140,11 +144,10 @@ export const errorMessages: { [code: number]: string } = {
|
||||
[ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME]:
|
||||
"'<?' is allowed only in XML context.",
|
||||
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
|
||||
[ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
|
||||
|
||||
// Vue-specific parse errors
|
||||
[ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
|
||||
[ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.',
|
||||
[ErrorCodes.X_MISSING_END_TAG]: 'Element is missing end tag.',
|
||||
[ErrorCodes.X_MISSING_INTERPOLATION_END]:
|
||||
'Interpolation end sign was not found.',
|
||||
[ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]:
|
||||
@@ -159,24 +162,24 @@ export const errorMessages: { [code: number]: string } = {
|
||||
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
|
||||
[ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
|
||||
[ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
|
||||
[ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT]:
|
||||
`Named v-slot on component. ` +
|
||||
`Named slots should use <template v-slot> syntax nested inside the component.`,
|
||||
[ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE]:
|
||||
`Mixed v-slot usage on both the component and nested <template>.` +
|
||||
`The default slot should also use <template> syntax when there are other ` +
|
||||
`named slots to avoid scope ambiguity.`,
|
||||
`When there are multiple named slots, all slots should use <template> ` +
|
||||
`syntax to avoid scope ambiguity.`,
|
||||
[ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
|
||||
[ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN]:
|
||||
`Extraneous children found when component has explicit slots. ` +
|
||||
`These children will be ignored.`,
|
||||
[ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN]:
|
||||
`Extraneous children found when component already has explicitly named ` +
|
||||
`default slot. These children will be ignored.`,
|
||||
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
|
||||
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
|
||||
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
|
||||
[ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`,
|
||||
[ErrorCodes.X_INVALID_EXPRESSION]: `Invalid JavaScript expression.`,
|
||||
[ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
|
||||
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> expects exactly one child component.`,
|
||||
|
||||
// generic errors
|
||||
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
|
||||
[ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`
|
||||
[ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`,
|
||||
[ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED]: `"cacheHandlers" option is only supported when the "prefixIdentifiers" option is enabled.`,
|
||||
[ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED]: `"scopeId" option is only supported in module mode.`
|
||||
}
|
||||
|
||||
@@ -1,90 +1,20 @@
|
||||
import { CompilerOptions } from './options'
|
||||
import { parse } from './parse'
|
||||
import { transform } from './transform'
|
||||
import { generate, CodegenResult } from './codegen'
|
||||
import { RootNode } from './ast'
|
||||
import { isString } from '@vue/shared'
|
||||
import { transformIf } from './transforms/vIf'
|
||||
import { transformFor } from './transforms/vFor'
|
||||
import { transformExpression } from './transforms/transformExpression'
|
||||
import { transformSlotOutlet } from './transforms/transformSlotOutlet'
|
||||
import { transformElement } from './transforms/transformElement'
|
||||
import { transformOn } from './transforms/vOn'
|
||||
import { transformBind } from './transforms/vBind'
|
||||
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
|
||||
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
|
||||
import { transformText } from './transforms/transformText'
|
||||
import { transformOnce } from './transforms/vOnce'
|
||||
import { transformModel } from './transforms/vModel'
|
||||
|
||||
// we name it `baseCompile` so that higher order compilers like @vue/compiler-dom
|
||||
// can export `compile` while re-exporting everything else.
|
||||
export function baseCompile(
|
||||
template: string | RootNode,
|
||||
options: CompilerOptions = {}
|
||||
): CodegenResult {
|
||||
/* istanbul ignore if */
|
||||
if (__BROWSER__) {
|
||||
const onError = options.onError || defaultOnError
|
||||
if (options.prefixIdentifiers === true) {
|
||||
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
|
||||
} else if (options.mode === 'module') {
|
||||
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
|
||||
}
|
||||
}
|
||||
|
||||
const ast = isString(template) ? parse(template, options) : template
|
||||
|
||||
const prefixIdentifiers =
|
||||
!__BROWSER__ &&
|
||||
(options.prefixIdentifiers === true || options.mode === 'module')
|
||||
|
||||
transform(ast, {
|
||||
...options,
|
||||
prefixIdentifiers,
|
||||
nodeTransforms: [
|
||||
transformOnce,
|
||||
transformIf,
|
||||
transformFor,
|
||||
...(prefixIdentifiers
|
||||
? [
|
||||
// order is important
|
||||
trackVForSlotScopes,
|
||||
transformExpression
|
||||
]
|
||||
: []),
|
||||
transformSlotOutlet,
|
||||
transformElement,
|
||||
trackSlotScopes,
|
||||
transformText,
|
||||
...(options.nodeTransforms || []) // user transforms
|
||||
],
|
||||
directiveTransforms: {
|
||||
on: transformOn,
|
||||
bind: transformBind,
|
||||
model: transformModel,
|
||||
...(options.directiveTransforms || {}) // user transforms
|
||||
}
|
||||
})
|
||||
|
||||
return generate(ast, {
|
||||
...options,
|
||||
prefixIdentifiers
|
||||
})
|
||||
}
|
||||
export { baseCompile } from './compile'
|
||||
|
||||
// Also expose lower level APIs & types
|
||||
export {
|
||||
CompilerOptions,
|
||||
ParserOptions,
|
||||
TransformOptions,
|
||||
CodegenOptions
|
||||
CodegenOptions,
|
||||
HoistTransform
|
||||
} from './options'
|
||||
export { parse, TextModes } from './parse'
|
||||
export { baseParse, TextModes } from './parse'
|
||||
export {
|
||||
transform,
|
||||
createStructuralDirectiveTransform,
|
||||
TransformContext,
|
||||
createTransformContext,
|
||||
traverseNode,
|
||||
createStructuralDirectiveTransform,
|
||||
NodeTransform,
|
||||
StructuralDirectiveTransform,
|
||||
DirectiveTransform
|
||||
@@ -96,19 +26,32 @@ export {
|
||||
CompilerError,
|
||||
createCompilerError
|
||||
} from './errors'
|
||||
|
||||
export * from './ast'
|
||||
export * from './utils'
|
||||
export { registerRuntimeHelpers } from './runtimeHelpers'
|
||||
export * from './runtimeHelpers'
|
||||
|
||||
// expose transforms so higher-order compilers can import and extend them
|
||||
export { getBaseTransformPreset, TransformPreset } from './compile'
|
||||
export { transformModel } from './transforms/vModel'
|
||||
export { transformOn } from './transforms/vOn'
|
||||
|
||||
// utility, but need to rewrite typing to avoid dts relying on @vue/shared
|
||||
import { generateCodeFrame as _genCodeFrame } from '@vue/shared'
|
||||
const generateCodeFrame = _genCodeFrame as (
|
||||
source: string,
|
||||
start?: number,
|
||||
end?: number
|
||||
) => string
|
||||
export { generateCodeFrame }
|
||||
export { transformBind } from './transforms/vBind'
|
||||
export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
|
||||
export { processIf } from './transforms/vIf'
|
||||
export { processFor, createForLoopParams } from './transforms/vFor'
|
||||
export {
|
||||
transformExpression,
|
||||
processExpression
|
||||
} from './transforms/transformExpression'
|
||||
export {
|
||||
buildSlots,
|
||||
SlotFnBuilder,
|
||||
trackVForSlotScopes,
|
||||
trackSlotScopes
|
||||
} from './transforms/vSlot'
|
||||
export {
|
||||
transformElement,
|
||||
resolveComponentType,
|
||||
buildProps
|
||||
} from './transforms/transformElement'
|
||||
export { processSlotOutlet } from './transforms/transformSlotOutlet'
|
||||
export { generateCodeFrame } from '@vue/shared'
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import { ElementNode, Namespace } from './ast'
|
||||
import { ElementNode, Namespace, JSChildNode, PlainElementNode } from './ast'
|
||||
import { TextModes } from './parse'
|
||||
import { CompilerError } from './errors'
|
||||
import { NodeTransform, DirectiveTransform } from './transform'
|
||||
import {
|
||||
NodeTransform,
|
||||
DirectiveTransform,
|
||||
TransformContext
|
||||
} from './transform'
|
||||
import { ParserPlugin } from '@babel/parser'
|
||||
|
||||
export interface ParserOptions {
|
||||
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
|
||||
isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex
|
||||
isPreTag?: (tag: string) => boolean // e.g. <pre> where whitespace is intact
|
||||
isCustomElement?: (tag: string) => boolean
|
||||
// e.g. platform native elements, e.g. <div> for browsers
|
||||
isNativeTag?: (tag: string) => boolean
|
||||
// e.g. native elements that can self-close, e.g. <img>, <br>, <hr>
|
||||
isVoidTag?: (tag: string) => boolean
|
||||
// e.g. elements that should preserve whitespace inside, e.g. <pre>
|
||||
isPreTag?: (tag: string) => boolean
|
||||
// platform-specific built-in components e.g. <Transition>
|
||||
isBuiltInComponent?: (tag: string) => symbol | void
|
||||
// separate option for end users to extend the native elements list
|
||||
isCustomElement?: (tag: string) => boolean
|
||||
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
|
||||
getTextMode?: (tag: string, ns: Namespace) => TextModes
|
||||
getTextMode?: (
|
||||
tag: string,
|
||||
ns: Namespace,
|
||||
parent: ElementNode | undefined
|
||||
) => TextModes
|
||||
delimiters?: [string, string] // ['{{', '}}']
|
||||
|
||||
// Map to HTML entities. E.g., `{ "amp;": "&" }`
|
||||
@@ -19,27 +33,46 @@ export interface ParserOptions {
|
||||
// this number is based on the map above, but it should be pre-computed
|
||||
// to avoid the cost on every parse() call.
|
||||
maxCRNameLength?: number
|
||||
|
||||
onError?: (error: CompilerError) => void
|
||||
}
|
||||
|
||||
export type HoistTransform = (
|
||||
node: PlainElementNode,
|
||||
context: TransformContext
|
||||
) => JSChildNode
|
||||
|
||||
export interface TransformOptions {
|
||||
nodeTransforms?: NodeTransform[]
|
||||
directiveTransforms?: { [name: string]: DirectiveTransform }
|
||||
directiveTransforms?: Record<string, DirectiveTransform | undefined>
|
||||
// an optional hook to transform a node being hoisted.
|
||||
// used by compiler-dom to turn hoisted nodes into stringified HTML vnodes.
|
||||
transformHoist?: HoistTransform | null
|
||||
isBuiltInComponent?: (tag: string) => symbol | void
|
||||
// Transform expressions like {{ foo }} to `_ctx.foo`.
|
||||
// Default: mode === 'module'
|
||||
// If this option is false, the generated code will be wrapped in a
|
||||
// `with (this) { ... }` block.
|
||||
// - This is force-enabled in module mode, since modules are by default strict
|
||||
// and cannot use `with`
|
||||
// - Default: mode === 'module'
|
||||
prefixIdentifiers?: boolean
|
||||
// Hoist static VNodes and props objects to `_hoisted_x` constants
|
||||
// Default: false
|
||||
// - Default: false
|
||||
hoistStatic?: boolean
|
||||
// Cache v-on handlers to avoid creating new inline functions on each render,
|
||||
// also avoids the need for dynamically patching the handlers by wrapping it.
|
||||
// e.g `@click="foo"` by default is compiled to `{ onClick: foo }`. With this
|
||||
// option it's compiled to:
|
||||
// `{ onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }`
|
||||
// Default: false
|
||||
// - Requires "prefixIdentifiers" to be enabled because it relies on scope
|
||||
// analysis to determine if a handler is safe to cache.
|
||||
// - Default: false
|
||||
cacheHandlers?: boolean
|
||||
// a list of parser plugins to enable for @babel/parser
|
||||
// https://babeljs.io/docs/en/next/babel-parser#plugins
|
||||
expressionPlugins?: ParserPlugin[]
|
||||
// SFC scoped styles ID
|
||||
scopeId?: string | null
|
||||
ssr?: boolean
|
||||
onError?: (error: CompilerError) => void
|
||||
}
|
||||
|
||||
@@ -49,19 +82,26 @@ export interface CodegenOptions {
|
||||
// - Function mode will generate a single `const { helpers... } = Vue`
|
||||
// statement and return the render function. It is meant to be used with
|
||||
// `new Function(code)()` to generate a render function at runtime.
|
||||
// Default: 'function'
|
||||
// - Default: 'function'
|
||||
mode?: 'module' | 'function'
|
||||
// Prefix suitable identifiers with _ctx.
|
||||
// If this option is false, the generated code will be wrapped in a
|
||||
// `with (this) { ... }` block.
|
||||
// Default: false
|
||||
prefixIdentifiers?: boolean
|
||||
// Generate source map?
|
||||
// Default: false
|
||||
// - Default: false
|
||||
sourceMap?: boolean
|
||||
// Filename for source map generation.
|
||||
// Default: `template.vue.html`
|
||||
// - Default: `template.vue.html`
|
||||
filename?: string
|
||||
// SFC scoped styles ID
|
||||
scopeId?: string | null
|
||||
// we need to know about this to generate proper preambles
|
||||
prefixIdentifiers?: boolean
|
||||
// option to optimize helper import bindings via variable assignment
|
||||
// (only used for webpack code-split)
|
||||
optimizeBindings?: boolean
|
||||
// for specifying where to import helpers
|
||||
runtimeModuleName?: string
|
||||
runtimeGlobalName?: string
|
||||
// generate ssr-specific code?
|
||||
ssr?: boolean
|
||||
}
|
||||
|
||||
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ParserOptions } from './options'
|
||||
import { NO, isArray } from '@vue/shared'
|
||||
import { NO, isArray, makeMap } from '@vue/shared'
|
||||
import { ErrorCodes, createCompilerError, defaultOnError } from './errors'
|
||||
import {
|
||||
assert,
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
SourceLocation,
|
||||
TextNode,
|
||||
TemplateChildNode,
|
||||
InterpolationNode
|
||||
InterpolationNode,
|
||||
createRoot
|
||||
} from './ast'
|
||||
import { extend } from '@vue/shared'
|
||||
|
||||
// `isNativeTag` is optional, others are required
|
||||
type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent'
|
||||
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
|
||||
Pick<ParserOptions, OptionalOptions>
|
||||
@@ -64,25 +64,20 @@ interface ParserContext {
|
||||
offset: number
|
||||
line: number
|
||||
column: number
|
||||
inPre: boolean
|
||||
inPre: boolean // HTML <pre> tag, preserve whitespaces
|
||||
inVPre: boolean // v-pre, do not process directives and interpolations
|
||||
}
|
||||
|
||||
export function parse(content: string, options: ParserOptions = {}): RootNode {
|
||||
export function baseParse(
|
||||
content: string,
|
||||
options: ParserOptions = {}
|
||||
): RootNode {
|
||||
const context = createParserContext(content, options)
|
||||
const start = getCursor(context)
|
||||
|
||||
return {
|
||||
type: NodeTypes.ROOT,
|
||||
children: parseChildren(context, TextModes.DATA, []),
|
||||
helpers: [],
|
||||
components: [],
|
||||
directives: [],
|
||||
hoists: [],
|
||||
imports: [],
|
||||
cached: 0,
|
||||
codegenNode: undefined,
|
||||
loc: getSelection(context, start)
|
||||
}
|
||||
return createRoot(
|
||||
parseChildren(context, TextModes.DATA, []),
|
||||
getSelection(context, start)
|
||||
)
|
||||
}
|
||||
|
||||
function createParserContext(
|
||||
@@ -99,7 +94,8 @@ function createParserContext(
|
||||
offset: 0,
|
||||
originalSource: content,
|
||||
source: content,
|
||||
inPre: false
|
||||
inPre: false,
|
||||
inVPre: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,11 +113,11 @@ function parseChildren(
|
||||
const s = context.source
|
||||
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
|
||||
|
||||
if (mode === TextModes.DATA) {
|
||||
if (!context.inPre && startsWith(s, context.options.delimiters[0])) {
|
||||
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
|
||||
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
|
||||
// '{{'
|
||||
node = parseInterpolation(context, mode)
|
||||
} else if (s[0] === '<') {
|
||||
} else if (mode === TextModes.DATA && s[0] === '<') {
|
||||
// https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
|
||||
if (s.length === 1) {
|
||||
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
|
||||
@@ -193,45 +189,51 @@ function parseChildren(
|
||||
// Whitespace management for more efficient output
|
||||
// (same as v2 whitespace: 'condense')
|
||||
let removedWhitespace = false
|
||||
if (
|
||||
mode !== TextModes.RAWTEXT &&
|
||||
(!parent || !context.options.isPreTag(parent.tag))
|
||||
) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (node.type === NodeTypes.TEXT) {
|
||||
if (!node.content.trim()) {
|
||||
const prev = nodes[i - 1]
|
||||
const next = nodes[i + 1]
|
||||
// If:
|
||||
// - the whitespace is the first or last node, or:
|
||||
// - the whitespace is adjacent to a comment, or:
|
||||
// - the whitespace is between two elements AND contains newline
|
||||
// Then the whitespace is ignored.
|
||||
if (
|
||||
!prev ||
|
||||
!next ||
|
||||
prev.type === NodeTypes.COMMENT ||
|
||||
next.type === NodeTypes.COMMENT ||
|
||||
(prev.type === NodeTypes.ELEMENT &&
|
||||
next.type === NodeTypes.ELEMENT &&
|
||||
/[\r\n]/.test(node.content))
|
||||
) {
|
||||
removedWhitespace = true
|
||||
nodes[i] = null as any
|
||||
if (mode !== TextModes.RAWTEXT) {
|
||||
if (!context.inPre) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (node.type === NodeTypes.TEXT) {
|
||||
if (!node.content.trim()) {
|
||||
const prev = nodes[i - 1]
|
||||
const next = nodes[i + 1]
|
||||
// If:
|
||||
// - the whitespace is the first or last node, or:
|
||||
// - the whitespace is adjacent to a comment, or:
|
||||
// - the whitespace is between two elements AND contains newline
|
||||
// Then the whitespace is ignored.
|
||||
if (
|
||||
!prev ||
|
||||
!next ||
|
||||
prev.type === NodeTypes.COMMENT ||
|
||||
next.type === NodeTypes.COMMENT ||
|
||||
(prev.type === NodeTypes.ELEMENT &&
|
||||
next.type === NodeTypes.ELEMENT &&
|
||||
/[\r\n]/.test(node.content))
|
||||
) {
|
||||
removedWhitespace = true
|
||||
nodes[i] = null as any
|
||||
} else {
|
||||
// Otherwise, condensed consecutive whitespace inside the text down to
|
||||
// a single space
|
||||
node.content = ' '
|
||||
}
|
||||
} else {
|
||||
// Otherwise, condensed consecutive whitespace inside the text down to
|
||||
// a single space
|
||||
node.content = ' '
|
||||
node.content = node.content.replace(/\s+/g, ' ')
|
||||
}
|
||||
} else {
|
||||
node.content = node.content.replace(/\s+/g, ' ')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// remove leading newline per html spec
|
||||
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
|
||||
const first = nodes[0]
|
||||
if (first && first.type === NodeTypes.TEXT) {
|
||||
first.content = first.content.replace(/^\r?\n/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removedWhitespace ? nodes.filter(node => node !== null) : nodes
|
||||
return removedWhitespace ? nodes.filter(Boolean) : nodes
|
||||
}
|
||||
|
||||
function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void {
|
||||
@@ -353,9 +355,11 @@ function parseElement(
|
||||
|
||||
// Start tag.
|
||||
const wasInPre = context.inPre
|
||||
const wasInVPre = context.inVPre
|
||||
const parent = last(ancestors)
|
||||
const element = parseTag(context, TagType.Start, parent)
|
||||
const isPreBoundary = context.inPre && !wasInPre
|
||||
const isVPreBoundary = context.inVPre && !wasInVPre
|
||||
|
||||
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
|
||||
return element
|
||||
@@ -363,7 +367,7 @@ function parseElement(
|
||||
|
||||
// Children.
|
||||
ancestors.push(element)
|
||||
const mode = context.options.getTextMode(element.tag, element.ns)
|
||||
const mode = context.options.getTextMode(element.tag, element.ns, parent)
|
||||
const children = parseChildren(context, mode, ancestors)
|
||||
ancestors.pop()
|
||||
|
||||
@@ -373,7 +377,7 @@ function parseElement(
|
||||
if (startsWithEndTagOpen(context.source, element.tag)) {
|
||||
parseTag(context, TagType.End, parent)
|
||||
} else {
|
||||
emitError(context, ErrorCodes.X_MISSING_END_TAG)
|
||||
emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
|
||||
if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
|
||||
const first = children[0]
|
||||
if (first && startsWith(first.loc.source, '<!--')) {
|
||||
@@ -387,6 +391,9 @@ function parseElement(
|
||||
if (isPreBoundary) {
|
||||
context.inPre = false
|
||||
}
|
||||
if (isVPreBoundary) {
|
||||
context.inVPre = false
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
@@ -395,6 +402,10 @@ const enum TagType {
|
||||
End
|
||||
}
|
||||
|
||||
const isSpecialTemplateDirective = /*#__PURE__*/ makeMap(
|
||||
`if,else,else-if,for,slot`
|
||||
)
|
||||
|
||||
/**
|
||||
* Parse a tag (E.g. `<div id=a>`) with that type (start tag or end tag).
|
||||
*/
|
||||
@@ -425,12 +436,17 @@ function parseTag(
|
||||
// Attributes.
|
||||
let props = parseAttributes(context, type)
|
||||
|
||||
// check <pre> tag
|
||||
if (context.options.isPreTag(tag)) {
|
||||
context.inPre = true
|
||||
}
|
||||
|
||||
// check v-pre
|
||||
if (
|
||||
!context.inPre &&
|
||||
!context.inVPre &&
|
||||
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
|
||||
) {
|
||||
context.inPre = true
|
||||
context.inVPre = true
|
||||
// reset context
|
||||
extend(context, cursor)
|
||||
context.source = currentSource
|
||||
@@ -452,20 +468,32 @@ function parseTag(
|
||||
|
||||
let tagType = ElementTypes.ELEMENT
|
||||
const options = context.options
|
||||
if (!context.inPre && !options.isCustomElement(tag)) {
|
||||
if (options.isNativeTag) {
|
||||
if (!context.inVPre && !options.isCustomElement(tag)) {
|
||||
const hasVIs = props.some(
|
||||
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
|
||||
)
|
||||
if (options.isNativeTag && !hasVIs) {
|
||||
if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
|
||||
} else if (
|
||||
hasVIs ||
|
||||
isCoreComponent(tag) ||
|
||||
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
|
||||
/^[A-Z]/.test(tag)
|
||||
/^[A-Z]/.test(tag) ||
|
||||
tag === 'component'
|
||||
) {
|
||||
tagType = ElementTypes.COMPONENT
|
||||
}
|
||||
|
||||
if (tag === 'slot') {
|
||||
tagType = ElementTypes.SLOT
|
||||
} else if (tag === 'template') {
|
||||
} else if (
|
||||
tag === 'template' &&
|
||||
props.some(p => {
|
||||
return (
|
||||
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
|
||||
)
|
||||
})
|
||||
) {
|
||||
tagType = ElementTypes.TEMPLATE
|
||||
}
|
||||
}
|
||||
@@ -539,7 +567,7 @@ function parseAttribute(
|
||||
{
|
||||
const pattern = /["'<]/g
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = pattern.exec(name)) !== null) {
|
||||
while ((m = pattern.exec(name))) {
|
||||
emitError(
|
||||
context,
|
||||
ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
|
||||
@@ -570,7 +598,7 @@ function parseAttribute(
|
||||
}
|
||||
const loc = getSelection(context, start)
|
||||
|
||||
if (!context.inPre && /^(v-|:|@|#)/.test(name)) {
|
||||
if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
|
||||
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
|
||||
name
|
||||
)!
|
||||
@@ -688,9 +716,9 @@ function parseAttributeValue(
|
||||
if (!match) {
|
||||
return undefined
|
||||
}
|
||||
let unexpectedChars = /["'<=`]/g
|
||||
const unexpectedChars = /["'<=`]/g
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = unexpectedChars.exec(match[0])) !== null) {
|
||||
while ((m = unexpectedChars.exec(match[0]))) {
|
||||
emitError(
|
||||
context,
|
||||
ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
|
||||
@@ -818,8 +846,8 @@ function parseTextData(
|
||||
|
||||
if (head[0] === '&') {
|
||||
// Named character reference.
|
||||
let name = '',
|
||||
value: string | undefined = undefined
|
||||
let name = ''
|
||||
let value: string | undefined = undefined
|
||||
if (/[0-9a-z]/i.test(rawText[1])) {
|
||||
for (
|
||||
let length = context.options.maxCRNameLength;
|
||||
@@ -834,7 +862,7 @@ function parseTextData(
|
||||
if (
|
||||
mode === TextModes.ATTRIBUTE_VALUE &&
|
||||
!semi &&
|
||||
/[=a-z0-9]/i.test(rawText[1 + name.length] || '')
|
||||
/[=a-z0-9]/i.test(rawText[name.length + 1] || '')
|
||||
) {
|
||||
decodedText += '&' + name
|
||||
advance(1 + name.length)
|
||||
@@ -849,7 +877,6 @@ function parseTextData(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emitError(context, ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE)
|
||||
decodedText += '&' + name
|
||||
advance(1 + name.length)
|
||||
}
|
||||
@@ -964,9 +991,9 @@ function getNewPosition(
|
||||
function emitError(
|
||||
context: ParserContext,
|
||||
code: ErrorCodes,
|
||||
offset?: number
|
||||
offset?: number,
|
||||
loc: Position = getCursor(context)
|
||||
): void {
|
||||
const loc = getCursor(context)
|
||||
if (offset) {
|
||||
loc.offset += offset
|
||||
loc.column += offset
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``)
|
||||
export const PORTAL = Symbol(__DEV__ ? `Portal` : ``)
|
||||
export const TELEPORT = Symbol(__DEV__ ? `Teleport` : ``)
|
||||
export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``)
|
||||
export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``)
|
||||
export const BASE_TRANSITION = Symbol(__DEV__ ? `BaseTransition` : ``)
|
||||
@@ -8,6 +8,7 @@ export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
|
||||
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
|
||||
export const CREATE_COMMENT = Symbol(__DEV__ ? `createCommentVNode` : ``)
|
||||
export const CREATE_TEXT = Symbol(__DEV__ ? `createTextVNode` : ``)
|
||||
export const CREATE_STATIC = Symbol(__DEV__ ? `createStaticVNode` : ``)
|
||||
export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
|
||||
export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
|
||||
__DEV__ ? `resolveDynamicComponent` : ``
|
||||
@@ -17,18 +18,22 @@ export const WITH_DIRECTIVES = Symbol(__DEV__ ? `withDirectives` : ``)
|
||||
export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``)
|
||||
export const RENDER_SLOT = Symbol(__DEV__ ? `renderSlot` : ``)
|
||||
export const CREATE_SLOTS = Symbol(__DEV__ ? `createSlots` : ``)
|
||||
export const TO_STRING = Symbol(__DEV__ ? `toString` : ``)
|
||||
export const TO_DISPLAY_STRING = Symbol(__DEV__ ? `toDisplayString` : ``)
|
||||
export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``)
|
||||
export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``)
|
||||
export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``)
|
||||
export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``)
|
||||
export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``)
|
||||
export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``)
|
||||
export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``)
|
||||
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
|
||||
|
||||
// Name mapping for runtime helpers that need to be imported from 'vue' in
|
||||
// generated code. Make sure these are correctly exported in the runtime!
|
||||
// Using `any` here because TS doesn't allow symbols as index type.
|
||||
export const helperNameMap: any = {
|
||||
[FRAGMENT]: `Fragment`,
|
||||
[PORTAL]: `Portal`,
|
||||
[TELEPORT]: `Teleport`,
|
||||
[SUSPENSE]: `Suspense`,
|
||||
[KEEP_ALIVE]: `KeepAlive`,
|
||||
[BASE_TRANSITION]: `BaseTransition`,
|
||||
@@ -37,6 +42,7 @@ export const helperNameMap: any = {
|
||||
[CREATE_VNODE]: `createVNode`,
|
||||
[CREATE_COMMENT]: `createCommentVNode`,
|
||||
[CREATE_TEXT]: `createTextVNode`,
|
||||
[CREATE_STATIC]: `createStaticVNode`,
|
||||
[RESOLVE_COMPONENT]: `resolveComponent`,
|
||||
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
|
||||
[RESOLVE_DIRECTIVE]: `resolveDirective`,
|
||||
@@ -44,11 +50,15 @@ export const helperNameMap: any = {
|
||||
[RENDER_LIST]: `renderList`,
|
||||
[RENDER_SLOT]: `renderSlot`,
|
||||
[CREATE_SLOTS]: `createSlots`,
|
||||
[TO_STRING]: `toString`,
|
||||
[TO_DISPLAY_STRING]: `toDisplayString`,
|
||||
[MERGE_PROPS]: `mergeProps`,
|
||||
[TO_HANDLERS]: `toHandlers`,
|
||||
[CAMELIZE]: `camelize`,
|
||||
[SET_BLOCK_TRACKING]: `setBlockTracking`
|
||||
[SET_BLOCK_TRACKING]: `setBlockTracking`,
|
||||
[PUSH_SCOPE_ID]: `pushScopeId`,
|
||||
[POP_SCOPE_ID]: `popScopeId`,
|
||||
[WITH_SCOPE_ID]: `withScopeId`,
|
||||
[WITH_CTX]: `withCtx`
|
||||
}
|
||||
|
||||
export function registerRuntimeHelpers(helpers: any) {
|
||||
|
||||
@@ -12,11 +12,10 @@ import {
|
||||
JSChildNode,
|
||||
SimpleExpressionNode,
|
||||
ElementTypes,
|
||||
ElementCodegenNode,
|
||||
ComponentCodegenNode,
|
||||
createCallExpression,
|
||||
CacheExpression,
|
||||
createCacheExpression
|
||||
createCacheExpression,
|
||||
TemplateLiteral,
|
||||
createVNodeCall
|
||||
} from './ast'
|
||||
import {
|
||||
isString,
|
||||
@@ -27,14 +26,14 @@ import {
|
||||
} from '@vue/shared'
|
||||
import { defaultOnError } from './errors'
|
||||
import {
|
||||
TO_STRING,
|
||||
TO_DISPLAY_STRING,
|
||||
FRAGMENT,
|
||||
helperNameMap,
|
||||
WITH_DIRECTIVES,
|
||||
CREATE_BLOCK,
|
||||
CREATE_COMMENT
|
||||
CREATE_COMMENT,
|
||||
OPEN_BLOCK
|
||||
} from './runtimeHelpers'
|
||||
import { isVSlot, createBlockExpression } from './utils'
|
||||
import { isVSlot } from './utils'
|
||||
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
|
||||
|
||||
// There are two types of transforms:
|
||||
@@ -61,7 +60,8 @@ export type DirectiveTransform = (
|
||||
|
||||
export interface DirectiveTransformResult {
|
||||
props: Property[]
|
||||
needRuntime: boolean | symbol
|
||||
needRuntime?: boolean | symbol
|
||||
ssrTagParts?: TemplateLiteral['elements']
|
||||
}
|
||||
|
||||
// A structural directive transform is a technically a NodeTransform;
|
||||
@@ -84,6 +84,7 @@ export interface TransformContext extends Required<TransformOptions> {
|
||||
directives: Set<string>
|
||||
hoists: JSChildNode[]
|
||||
imports: Set<ImportItem>
|
||||
temps: number
|
||||
cached: number
|
||||
identifiers: { [name: string]: number | undefined }
|
||||
scopes: {
|
||||
@@ -106,7 +107,7 @@ export interface TransformContext extends Required<TransformOptions> {
|
||||
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
|
||||
}
|
||||
|
||||
function createTransformContext(
|
||||
export function createTransformContext(
|
||||
root: RootNode,
|
||||
{
|
||||
prefixIdentifiers = false,
|
||||
@@ -114,17 +115,36 @@ function createTransformContext(
|
||||
cacheHandlers = false,
|
||||
nodeTransforms = [],
|
||||
directiveTransforms = {},
|
||||
transformHoist = null,
|
||||
isBuiltInComponent = NOOP,
|
||||
expressionPlugins = [],
|
||||
scopeId = null,
|
||||
ssr = false,
|
||||
onError = defaultOnError
|
||||
}: TransformOptions
|
||||
): TransformContext {
|
||||
const context: TransformContext = {
|
||||
// options
|
||||
prefixIdentifiers,
|
||||
hoistStatic,
|
||||
cacheHandlers,
|
||||
nodeTransforms,
|
||||
directiveTransforms,
|
||||
transformHoist,
|
||||
isBuiltInComponent,
|
||||
expressionPlugins,
|
||||
scopeId,
|
||||
ssr,
|
||||
onError,
|
||||
|
||||
// state
|
||||
root,
|
||||
helpers: new Set(),
|
||||
components: new Set(),
|
||||
directives: new Set(),
|
||||
hoists: [],
|
||||
imports: new Set(),
|
||||
temps: 0,
|
||||
cached: 0,
|
||||
identifiers: {},
|
||||
scopes: {
|
||||
@@ -133,25 +153,17 @@ function createTransformContext(
|
||||
vPre: 0,
|
||||
vOnce: 0
|
||||
},
|
||||
prefixIdentifiers,
|
||||
hoistStatic,
|
||||
cacheHandlers,
|
||||
nodeTransforms,
|
||||
directiveTransforms,
|
||||
isBuiltInComponent,
|
||||
onError,
|
||||
parent: null,
|
||||
currentNode: root,
|
||||
childIndex: 0,
|
||||
|
||||
// methods
|
||||
helper(name) {
|
||||
context.helpers.add(name)
|
||||
return name
|
||||
},
|
||||
helperString(name) {
|
||||
return (
|
||||
(context.prefixIdentifiers ? `` : `_`) +
|
||||
helperNameMap[context.helper(name)]
|
||||
)
|
||||
return `_${helperNameMap[context.helper(name)]}`
|
||||
},
|
||||
replaceNode(node) {
|
||||
/* istanbul ignore if */
|
||||
@@ -251,10 +263,20 @@ export function transform(root: RootNode, options: TransformOptions) {
|
||||
if (options.hoistStatic) {
|
||||
hoistStatic(root, context)
|
||||
}
|
||||
finalizeRoot(root, context)
|
||||
if (!options.ssr) {
|
||||
createRootCodegen(root, context)
|
||||
}
|
||||
// finalize meta information
|
||||
root.helpers = [...context.helpers]
|
||||
root.components = [...context.components]
|
||||
root.directives = [...context.directives]
|
||||
root.imports = [...context.imports]
|
||||
root.hoists = context.hoists
|
||||
root.temps = context.temps
|
||||
root.cached = context.cached
|
||||
}
|
||||
|
||||
function finalizeRoot(root: RootNode, context: TransformContext) {
|
||||
function createRootCodegen(root: RootNode, context: TransformContext) {
|
||||
const { helper } = context
|
||||
const { children } = root
|
||||
const child = children[0]
|
||||
@@ -263,20 +285,13 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
|
||||
if (isSingleElementRoot(root, child) && child.codegenNode) {
|
||||
// single element root is never hoisted so codegenNode will never be
|
||||
// SimpleExpressionNode
|
||||
const codegenNode = child.codegenNode as
|
||||
| ElementCodegenNode
|
||||
| ComponentCodegenNode
|
||||
| CacheExpression
|
||||
if (codegenNode.type !== NodeTypes.JS_CACHE_EXPRESSION) {
|
||||
if (codegenNode.callee === WITH_DIRECTIVES) {
|
||||
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
|
||||
} else {
|
||||
codegenNode.callee = helper(CREATE_BLOCK)
|
||||
}
|
||||
root.codegenNode = createBlockExpression(codegenNode, context)
|
||||
} else {
|
||||
root.codegenNode = codegenNode
|
||||
const codegenNode = child.codegenNode
|
||||
if (codegenNode.type === NodeTypes.VNODE_CALL) {
|
||||
codegenNode.isBlock = true
|
||||
helper(OPEN_BLOCK)
|
||||
helper(CREATE_BLOCK)
|
||||
}
|
||||
root.codegenNode = codegenNode
|
||||
} else {
|
||||
// - single <slot/>, IfNode, ForNode: already blocks.
|
||||
// - single text node: always patched.
|
||||
@@ -285,27 +300,21 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
|
||||
}
|
||||
} else if (children.length > 1) {
|
||||
// root has multiple nodes - return a fragment block.
|
||||
root.codegenNode = createBlockExpression(
|
||||
createCallExpression(helper(CREATE_BLOCK), [
|
||||
helper(FRAGMENT),
|
||||
`null`,
|
||||
root.children,
|
||||
`${PatchFlags.STABLE_FRAGMENT} /* ${
|
||||
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
|
||||
} */`
|
||||
]),
|
||||
context
|
||||
root.codegenNode = createVNodeCall(
|
||||
context,
|
||||
helper(FRAGMENT),
|
||||
undefined,
|
||||
root.children,
|
||||
`${PatchFlags.STABLE_FRAGMENT} /* ${
|
||||
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
|
||||
} */`,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
// no children = noop. codegen will return null.
|
||||
}
|
||||
// finalize meta information
|
||||
root.helpers = [...context.helpers]
|
||||
root.components = [...context.components]
|
||||
root.directives = [...context.directives]
|
||||
root.imports = [...context.imports]
|
||||
root.hoists = context.hoists
|
||||
root.cached = context.cached
|
||||
}
|
||||
|
||||
export function traverseChildren(
|
||||
@@ -319,7 +328,6 @@ export function traverseChildren(
|
||||
for (; i < parent.children.length; i++) {
|
||||
const child = parent.children[i]
|
||||
if (isString(child)) continue
|
||||
context.currentNode = child
|
||||
context.parent = parent
|
||||
context.childIndex = i
|
||||
context.onNodeRemoved = nodeRemoved
|
||||
@@ -331,6 +339,7 @@ export function traverseNode(
|
||||
node: RootNode | TemplateChildNode,
|
||||
context: TransformContext
|
||||
) {
|
||||
context.currentNode = node
|
||||
// apply transform plugins
|
||||
const { nodeTransforms } = context
|
||||
const exitFns = []
|
||||
@@ -354,21 +363,26 @@ export function traverseNode(
|
||||
|
||||
switch (node.type) {
|
||||
case NodeTypes.COMMENT:
|
||||
// inject import for the Comment symbol, which is needed for creating
|
||||
// comment nodes with `createVNode`
|
||||
context.helper(CREATE_COMMENT)
|
||||
if (!context.ssr) {
|
||||
// inject import for the Comment symbol, which is needed for creating
|
||||
// comment nodes with `createVNode`
|
||||
context.helper(CREATE_COMMENT)
|
||||
}
|
||||
break
|
||||
case NodeTypes.INTERPOLATION:
|
||||
// no need to traverse, but we need to inject toString helper
|
||||
context.helper(TO_STRING)
|
||||
if (!context.ssr) {
|
||||
context.helper(TO_DISPLAY_STRING)
|
||||
}
|
||||
break
|
||||
|
||||
// for container types, further traverse downwards
|
||||
case NodeTypes.IF:
|
||||
for (let i = 0; i < node.branches.length; i++) {
|
||||
traverseChildren(node.branches[i], context)
|
||||
traverseNode(node.branches[i], context)
|
||||
}
|
||||
break
|
||||
case NodeTypes.IF_BRANCH:
|
||||
case NodeTypes.FOR:
|
||||
case NodeTypes.ELEMENT:
|
||||
case NodeTypes.ROOT:
|
||||
|
||||
@@ -8,11 +8,9 @@ import {
|
||||
ComponentNode,
|
||||
TemplateNode,
|
||||
ElementNode,
|
||||
PlainElementCodegenNode,
|
||||
CodegenNodeWithDirective
|
||||
VNodeCall
|
||||
} from '../ast'
|
||||
import { TransformContext } from '../transform'
|
||||
import { WITH_DIRECTIVES } from '../runtimeHelpers'
|
||||
import { PatchFlags, isString, isSymbol } from '@vue/shared'
|
||||
import { isSlotOutlet, findProp } from '../utils'
|
||||
|
||||
@@ -21,6 +19,8 @@ export function hoistStatic(root: RootNode, context: TransformContext) {
|
||||
root.children,
|
||||
context,
|
||||
new Map(),
|
||||
// Root node is unfortuantely non-hoistable due to potential parent
|
||||
// fallthrough attributes.
|
||||
isSingleElementRoot(root, root.children[0])
|
||||
)
|
||||
}
|
||||
@@ -52,13 +52,18 @@ function walk(
|
||||
) {
|
||||
if (!doNotHoistNode && isStaticNode(child, resultCache)) {
|
||||
// whole tree is static
|
||||
child.codegenNode = context.hoist(child.codegenNode!)
|
||||
;(child.codegenNode as VNodeCall).patchFlag =
|
||||
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
|
||||
const hoisted = context.transformHoist
|
||||
? context.transformHoist(child, context)
|
||||
: child.codegenNode!
|
||||
child.codegenNode = context.hoist(hoisted)
|
||||
continue
|
||||
} else {
|
||||
// node may contain dynamic children, but its props may be eligible for
|
||||
// hoisting.
|
||||
const codegenNode = child.codegenNode!
|
||||
if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) {
|
||||
if (codegenNode.type === NodeTypes.VNODE_CALL) {
|
||||
const flag = getPatchFlag(codegenNode)
|
||||
if (
|
||||
(!flag ||
|
||||
@@ -68,8 +73,8 @@ function walk(
|
||||
!hasCachedProps(child)
|
||||
) {
|
||||
const props = getNodeProps(child)
|
||||
if (props && props !== `null`) {
|
||||
getVNodeCall(codegenNode).arguments[1] = context.hoist(props)
|
||||
if (props) {
|
||||
codegenNode.props = context.hoist(props)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,6 +91,11 @@ function walk(
|
||||
// Do not hoist v-if single child because it has to be a block
|
||||
walk(branchChildren, context, resultCache, branchChildren.length === 1)
|
||||
}
|
||||
} else if (
|
||||
child.type === NodeTypes.TEXT_CALL &&
|
||||
isStaticNode(child.content, resultCache)
|
||||
) {
|
||||
child.codegenNode = context.hoist(child.codegenNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +114,7 @@ export function isStaticNode(
|
||||
return cached
|
||||
}
|
||||
const codegenNode = node.codegenNode!
|
||||
if (codegenNode.type !== NodeTypes.JS_CALL_EXPRESSION) {
|
||||
if (codegenNode.type !== NodeTypes.VNODE_CALL) {
|
||||
return false
|
||||
}
|
||||
const flag = getPatchFlag(codegenNode)
|
||||
@@ -116,6 +126,12 @@ export function isStaticNode(
|
||||
return false
|
||||
}
|
||||
}
|
||||
// only svg/foreignObject could be block here, however if they are static
|
||||
// then they don't need to be blocks since there will be no nested
|
||||
// updates.
|
||||
if (codegenNode.isBlock) {
|
||||
codegenNode.isBlock = false
|
||||
}
|
||||
resultCache.set(node, true)
|
||||
return true
|
||||
} else {
|
||||
@@ -127,6 +143,7 @@ export function isStaticNode(
|
||||
return true
|
||||
case NodeTypes.IF:
|
||||
case NodeTypes.FOR:
|
||||
case NodeTypes.IF_BRANCH:
|
||||
return false
|
||||
case NodeTypes.INTERPOLATION:
|
||||
case NodeTypes.TEXT_CALL:
|
||||
@@ -157,14 +174,20 @@ function hasCachedProps(node: PlainElementNode): boolean {
|
||||
return false
|
||||
}
|
||||
const props = getNodeProps(node)
|
||||
if (
|
||||
props &&
|
||||
props !== 'null' &&
|
||||
props.type === NodeTypes.JS_OBJECT_EXPRESSION
|
||||
) {
|
||||
if (props && props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
|
||||
const { properties } = props
|
||||
for (let i = 0; i < properties.length; i++) {
|
||||
if (properties[i].value.type === NodeTypes.JS_CACHE_EXPRESSION) {
|
||||
const val = properties[i].value
|
||||
if (val.type === NodeTypes.JS_CACHE_EXPRESSION) {
|
||||
return true
|
||||
}
|
||||
// merged event handlers
|
||||
if (
|
||||
val.type === NodeTypes.JS_ARRAY_EXPRESSION &&
|
||||
val.elements.some(
|
||||
e => !isString(e) && e.type === NodeTypes.JS_CACHE_EXPRESSION
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -174,30 +197,12 @@ function hasCachedProps(node: PlainElementNode): boolean {
|
||||
|
||||
function getNodeProps(node: PlainElementNode) {
|
||||
const codegenNode = node.codegenNode!
|
||||
if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) {
|
||||
return getVNodeArgAt(
|
||||
codegenNode,
|
||||
1
|
||||
) as PlainElementCodegenNode['arguments'][1]
|
||||
if (codegenNode.type === NodeTypes.VNODE_CALL) {
|
||||
return codegenNode.props
|
||||
}
|
||||
}
|
||||
|
||||
type NonCachedCodegenNode =
|
||||
| PlainElementCodegenNode
|
||||
| CodegenNodeWithDirective<PlainElementCodegenNode>
|
||||
|
||||
function getVNodeArgAt(
|
||||
node: NonCachedCodegenNode,
|
||||
index: number
|
||||
): PlainElementCodegenNode['arguments'][number] {
|
||||
return getVNodeCall(node).arguments[index]
|
||||
}
|
||||
|
||||
function getVNodeCall(node: NonCachedCodegenNode) {
|
||||
return node.callee === WITH_DIRECTIVES ? node.arguments[0] : node
|
||||
}
|
||||
|
||||
function getPatchFlag(node: NonCachedCodegenNode): number | undefined {
|
||||
const flag = getVNodeArgAt(node, 3) as string
|
||||
function getPatchFlag(node: VNodeCall): number | undefined {
|
||||
const flag = node.patchFlag
|
||||
return flag ? parseInt(flag, 10) : undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DirectiveTransform } from '../transform'
|
||||
|
||||
export const noopDirectiveTransform: DirectiveTransform = () => ({ props: [] })
|
||||
@@ -13,27 +13,31 @@ import {
|
||||
createObjectProperty,
|
||||
createSimpleExpression,
|
||||
createObjectExpression,
|
||||
Property
|
||||
Property,
|
||||
ComponentNode,
|
||||
VNodeCall,
|
||||
TemplateTextChildNode,
|
||||
DirectiveArguments,
|
||||
createVNodeCall
|
||||
} from '../ast'
|
||||
import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared'
|
||||
import { PatchFlags, PatchFlagNames, isSymbol, isOn } from '@vue/shared'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import {
|
||||
CREATE_VNODE,
|
||||
WITH_DIRECTIVES,
|
||||
RESOLVE_DIRECTIVE,
|
||||
RESOLVE_COMPONENT,
|
||||
RESOLVE_DYNAMIC_COMPONENT,
|
||||
MERGE_PROPS,
|
||||
TO_HANDLERS,
|
||||
PORTAL,
|
||||
TELEPORT,
|
||||
KEEP_ALIVE
|
||||
} from '../runtimeHelpers'
|
||||
import {
|
||||
getInnerRange,
|
||||
isVSlot,
|
||||
toValidAssetId,
|
||||
findProp,
|
||||
isCoreComponent
|
||||
isCoreComponent,
|
||||
isBindKey,
|
||||
findDir
|
||||
} from '../utils'
|
||||
import { buildSlots } from './vSlot'
|
||||
import { isStaticNode } from './hoistStatic'
|
||||
@@ -45,107 +49,97 @@ const directiveImportMap = new WeakMap<DirectiveNode, symbol>()
|
||||
// generate a JavaScript AST for this element's codegen
|
||||
export const transformElement: NodeTransform = (node, context) => {
|
||||
if (
|
||||
node.type !== NodeTypes.ELEMENT ||
|
||||
// handled by transformSlotOutlet
|
||||
node.tagType === ElementTypes.SLOT ||
|
||||
// <template v-if/v-for> should have already been replaced
|
||||
// <template v-slot> is handled by buildSlots
|
||||
(node.tagType === ElementTypes.TEMPLATE && node.props.some(isVSlot))
|
||||
!(
|
||||
node.type === NodeTypes.ELEMENT &&
|
||||
(node.tagType === ElementTypes.ELEMENT ||
|
||||
node.tagType === ElementTypes.COMPONENT)
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// perform the work on exit, after all child expressions have been
|
||||
// processed and merged.
|
||||
return function postTransformElement() {
|
||||
const { tag, tagType, props } = node
|
||||
const builtInComponentSymbol =
|
||||
isCoreComponent(tag) || context.isBuiltInComponent(tag)
|
||||
const isComponent = tagType === ElementTypes.COMPONENT
|
||||
const { tag, props } = node
|
||||
const isComponent = node.tagType === ElementTypes.COMPONENT
|
||||
|
||||
let hasProps = props.length > 0
|
||||
// The goal of the transform is to create a codegenNode implementing the
|
||||
// VNodeCall interface.
|
||||
const vnodeTag = isComponent
|
||||
? resolveComponentType(node as ComponentNode, context)
|
||||
: `"${tag}"`
|
||||
|
||||
let vnodeProps: VNodeCall['props']
|
||||
let vnodeChildren: VNodeCall['children']
|
||||
let vnodePatchFlag: VNodeCall['patchFlag']
|
||||
let patchFlag: number = 0
|
||||
let runtimeDirectives: DirectiveNode[] | undefined
|
||||
let vnodeDynamicProps: VNodeCall['dynamicProps']
|
||||
let dynamicPropNames: string[] | undefined
|
||||
let dynamicComponent: string | CallExpression | undefined
|
||||
let vnodeDirectives: VNodeCall['directives']
|
||||
|
||||
// handle dynamic component
|
||||
const isProp = findProp(node, 'is')
|
||||
if (tag === 'component') {
|
||||
if (isProp) {
|
||||
// static <component is="foo" />
|
||||
if (isProp.type === NodeTypes.ATTRIBUTE) {
|
||||
const tag = isProp.value && isProp.value.content
|
||||
if (tag) {
|
||||
context.helper(RESOLVE_COMPONENT)
|
||||
context.components.add(tag)
|
||||
dynamicComponent = toValidAssetId(tag, `component`)
|
||||
}
|
||||
}
|
||||
// dynamic <component :is="asdf" />
|
||||
else if (isProp.exp) {
|
||||
dynamicComponent = createCallExpression(
|
||||
context.helper(RESOLVE_DYNAMIC_COMPONENT),
|
||||
// _ctx.$ exposes the owner instance of current render function
|
||||
[isProp.exp, context.prefixIdentifiers ? `_ctx.$` : `$`]
|
||||
let shouldUseBlock =
|
||||
!isComponent &&
|
||||
// <svg> and <foreignObject> must be forced into blocks so that block
|
||||
// updates inside get proper isSVG flag at runtime. (#639, #643)
|
||||
// This is technically web-specific, but splitting the logic out of core
|
||||
// leads to too much unnecessary complexity.
|
||||
(tag === 'svg' ||
|
||||
tag === 'foreignObject' ||
|
||||
// #938: elements with dynamic keys should be forced into blocks
|
||||
findProp(node, 'key', true))
|
||||
|
||||
// props
|
||||
if (props.length > 0) {
|
||||
const propsBuildResult = buildProps(node, context)
|
||||
vnodeProps = propsBuildResult.props
|
||||
patchFlag = propsBuildResult.patchFlag
|
||||
dynamicPropNames = propsBuildResult.dynamicPropNames
|
||||
const directives = propsBuildResult.directives
|
||||
vnodeDirectives =
|
||||
directives && directives.length
|
||||
? (createArrayExpression(
|
||||
directives.map(dir => buildDirectiveArgs(dir, context))
|
||||
) as DirectiveArguments)
|
||||
: undefined
|
||||
}
|
||||
|
||||
// children
|
||||
if (node.children.length > 0) {
|
||||
if (vnodeTag === KEEP_ALIVE) {
|
||||
// Although a built-in component, we compile KeepAlive with raw children
|
||||
// instead of slot functions so that it can be used inside Transition
|
||||
// or other Transition-wrapping HOCs.
|
||||
// To ensure correct updates with block optimizations, we need to:
|
||||
// 1. Force keep-alive into a block. This avoids its children being
|
||||
// collected by a parent block.
|
||||
shouldUseBlock = true
|
||||
// 2. Force keep-alive to always be updated, since it uses raw children.
|
||||
patchFlag |= PatchFlags.DYNAMIC_SLOTS
|
||||
if (__DEV__ && node.children.length > 1) {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN, {
|
||||
start: node.children[0].loc.start,
|
||||
end: node.children[node.children.length - 1].loc.end,
|
||||
source: ''
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nodeType
|
||||
if (dynamicComponent) {
|
||||
nodeType = dynamicComponent
|
||||
} else if (builtInComponentSymbol) {
|
||||
nodeType = context.helper(builtInComponentSymbol)
|
||||
} else if (isComponent) {
|
||||
// user component w/ resolve
|
||||
context.helper(RESOLVE_COMPONENT)
|
||||
context.components.add(tag)
|
||||
nodeType = toValidAssetId(tag, `component`)
|
||||
} else {
|
||||
// plain element
|
||||
nodeType = `"${node.tag}"`
|
||||
}
|
||||
|
||||
const args: CallExpression['arguments'] = [nodeType]
|
||||
// props
|
||||
if (hasProps) {
|
||||
const propsBuildResult = buildProps(
|
||||
node,
|
||||
context,
|
||||
// skip reserved "is" prop <component is>
|
||||
isProp ? node.props.filter(p => p !== isProp) : node.props
|
||||
)
|
||||
patchFlag = propsBuildResult.patchFlag
|
||||
dynamicPropNames = propsBuildResult.dynamicPropNames
|
||||
runtimeDirectives = propsBuildResult.directives
|
||||
if (!propsBuildResult.props) {
|
||||
hasProps = false
|
||||
} else {
|
||||
args.push(propsBuildResult.props)
|
||||
}
|
||||
}
|
||||
// children
|
||||
const hasChildren = node.children.length > 0
|
||||
if (hasChildren) {
|
||||
if (!hasProps) {
|
||||
args.push(`null`)
|
||||
}
|
||||
// Portal & KeepAlive should have normal children instead of slots
|
||||
// Portal is not a real component has dedicated handling in the renderer
|
||||
// KeepAlive should not track its own deps so that it can be used inside
|
||||
// Transition
|
||||
if (
|
||||
const shouldBuildAsSlots =
|
||||
isComponent &&
|
||||
builtInComponentSymbol !== PORTAL &&
|
||||
builtInComponentSymbol !== KEEP_ALIVE
|
||||
) {
|
||||
// Teleport is not a real component and has dedicated runtime handling
|
||||
vnodeTag !== TELEPORT &&
|
||||
// explained above.
|
||||
vnodeTag !== KEEP_ALIVE
|
||||
|
||||
if (shouldBuildAsSlots) {
|
||||
const { slots, hasDynamicSlots } = buildSlots(node, context)
|
||||
args.push(slots)
|
||||
vnodeChildren = slots
|
||||
if (hasDynamicSlots) {
|
||||
patchFlag |= PatchFlags.DYNAMIC_SLOTS
|
||||
}
|
||||
} else if (node.children.length === 1) {
|
||||
} else if (node.children.length === 1 && vnodeTag !== TELEPORT) {
|
||||
const child = node.children[0]
|
||||
const type = child.type
|
||||
// check for dynamic text children
|
||||
@@ -158,65 +152,88 @@ export const transformElement: NodeTransform = (node, context) => {
|
||||
// pass directly if the only child is a text node
|
||||
// (plain / interpolation / expression)
|
||||
if (hasDynamicTextChild || type === NodeTypes.TEXT) {
|
||||
args.push(child)
|
||||
vnodeChildren = child as TemplateTextChildNode
|
||||
} else {
|
||||
args.push(node.children)
|
||||
vnodeChildren = node.children
|
||||
}
|
||||
} else {
|
||||
args.push(node.children)
|
||||
vnodeChildren = node.children
|
||||
}
|
||||
}
|
||||
|
||||
// patchFlag & dynamicPropNames
|
||||
if (patchFlag !== 0) {
|
||||
if (!hasChildren) {
|
||||
if (!hasProps) {
|
||||
args.push(`null`)
|
||||
}
|
||||
args.push(`null`)
|
||||
}
|
||||
if (__DEV__) {
|
||||
const flagNames = Object.keys(PatchFlagNames)
|
||||
.map(Number)
|
||||
.filter(n => n > 0 && patchFlag & n)
|
||||
.map(n => PatchFlagNames[n])
|
||||
.join(`, `)
|
||||
args.push(patchFlag + ` /* ${flagNames} */`)
|
||||
if (patchFlag < 0) {
|
||||
// special flags (negative and mutually exclusive)
|
||||
vnodePatchFlag = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */`
|
||||
} else {
|
||||
// bitwise flags
|
||||
const flagNames = Object.keys(PatchFlagNames)
|
||||
.map(Number)
|
||||
.filter(n => n > 0 && patchFlag & n)
|
||||
.map(n => PatchFlagNames[n])
|
||||
.join(`, `)
|
||||
vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
|
||||
}
|
||||
} else {
|
||||
args.push(patchFlag + '')
|
||||
vnodePatchFlag = String(patchFlag)
|
||||
}
|
||||
if (dynamicPropNames && dynamicPropNames.length) {
|
||||
args.push(stringifyDynamicPropNames(dynamicPropNames))
|
||||
vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
|
||||
}
|
||||
}
|
||||
|
||||
const { loc } = node
|
||||
const vnode = createCallExpression(context.helper(CREATE_VNODE), args, loc)
|
||||
|
||||
if (runtimeDirectives && runtimeDirectives.length) {
|
||||
node.codegenNode = createCallExpression(
|
||||
context.helper(WITH_DIRECTIVES),
|
||||
[
|
||||
vnode,
|
||||
createArrayExpression(
|
||||
runtimeDirectives.map(dir => buildDirectiveArgs(dir, context)),
|
||||
loc
|
||||
)
|
||||
],
|
||||
loc
|
||||
)
|
||||
} else {
|
||||
node.codegenNode = vnode
|
||||
}
|
||||
node.codegenNode = createVNodeCall(
|
||||
context,
|
||||
vnodeTag,
|
||||
vnodeProps,
|
||||
vnodeChildren,
|
||||
vnodePatchFlag,
|
||||
vnodeDynamicProps,
|
||||
vnodeDirectives,
|
||||
!!shouldUseBlock,
|
||||
false /* isForBlock */,
|
||||
node.loc
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyDynamicPropNames(props: string[]): string {
|
||||
let propsNamesString = `[`
|
||||
for (let i = 0, l = props.length; i < l; i++) {
|
||||
propsNamesString += JSON.stringify(props[i])
|
||||
if (i < l - 1) propsNamesString += ', '
|
||||
export function resolveComponentType(
|
||||
node: ComponentNode,
|
||||
context: TransformContext,
|
||||
ssr = false
|
||||
) {
|
||||
const { tag } = node
|
||||
|
||||
// 1. dynamic component
|
||||
const isProp =
|
||||
node.tag === 'component' ? findProp(node, 'is') : findDir(node, 'is')
|
||||
if (isProp) {
|
||||
const exp =
|
||||
isProp.type === NodeTypes.ATTRIBUTE
|
||||
? isProp.value && createSimpleExpression(isProp.value.content, true)
|
||||
: isProp.exp
|
||||
if (exp) {
|
||||
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
|
||||
exp
|
||||
])
|
||||
}
|
||||
}
|
||||
return propsNamesString + `]`
|
||||
|
||||
// 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
|
||||
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
|
||||
if (builtIn) {
|
||||
// built-ins are simply fallthroughs / have special handling during ssr
|
||||
// no we don't need to import their runtime equivalents
|
||||
if (!ssr) context.helper(builtIn)
|
||||
return builtIn
|
||||
}
|
||||
|
||||
// 3. user component (resolve)
|
||||
context.helper(RESOLVE_COMPONENT)
|
||||
context.components.add(tag)
|
||||
return toValidAssetId(tag, `component`)
|
||||
}
|
||||
|
||||
export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
|
||||
@@ -224,14 +241,15 @@ export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
|
||||
export function buildProps(
|
||||
node: ElementNode,
|
||||
context: TransformContext,
|
||||
props: ElementNode['props'] = node.props
|
||||
props: ElementNode['props'] = node.props,
|
||||
ssr = false
|
||||
): {
|
||||
props: PropsExpression | undefined
|
||||
directives: DirectiveNode[]
|
||||
patchFlag: number
|
||||
dynamicPropNames: string[]
|
||||
} {
|
||||
const elementLoc = node.loc
|
||||
const { tag, loc: elementLoc } = node
|
||||
const isComponent = node.tagType === ElementTypes.COMPONENT
|
||||
let properties: ObjectExpression['properties'] = []
|
||||
const mergeArgs: PropsExpression[] = []
|
||||
@@ -242,27 +260,40 @@ export function buildProps(
|
||||
let hasRef = false
|
||||
let hasClassBinding = false
|
||||
let hasStyleBinding = false
|
||||
let hasHydrationEventBinding = false
|
||||
let hasDynamicKeys = false
|
||||
const dynamicPropNames: string[] = []
|
||||
|
||||
const analyzePatchFlag = ({ key, value }: Property) => {
|
||||
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
|
||||
const name = key.content
|
||||
if (
|
||||
!isComponent &&
|
||||
isOn(name) &&
|
||||
// omit the flag for click handlers becaues hydration gives click
|
||||
// dedicated fast path.
|
||||
name.toLowerCase() !== 'onclick' &&
|
||||
// omit v-model handlers
|
||||
name !== 'onUpdate:modelValue'
|
||||
) {
|
||||
hasHydrationEventBinding = true
|
||||
}
|
||||
if (
|
||||
value.type === NodeTypes.JS_CACHE_EXPRESSION ||
|
||||
((value.type === NodeTypes.SIMPLE_EXPRESSION ||
|
||||
value.type === NodeTypes.COMPOUND_EXPRESSION) &&
|
||||
isStaticNode(value))
|
||||
) {
|
||||
// skip if the prop is a cached handler or has constant value
|
||||
return
|
||||
}
|
||||
const name = key.content
|
||||
if (name === 'ref') {
|
||||
hasRef = true
|
||||
} else if (name === 'class') {
|
||||
hasClassBinding = true
|
||||
} else if (name === 'style') {
|
||||
hasStyleBinding = true
|
||||
} else if (name !== 'key') {
|
||||
} else if (name !== 'key' && !dynamicPropNames.includes(name)) {
|
||||
dynamicPropNames.push(name)
|
||||
}
|
||||
} else {
|
||||
@@ -278,6 +309,10 @@ export function buildProps(
|
||||
if (name === 'ref') {
|
||||
hasRef = true
|
||||
}
|
||||
// skip :is on <component>
|
||||
if (name === 'is' && tag === 'component') {
|
||||
continue
|
||||
}
|
||||
properties.push(
|
||||
createObjectProperty(
|
||||
createSimpleExpression(
|
||||
@@ -295,6 +330,8 @@ export function buildProps(
|
||||
} else {
|
||||
// directives
|
||||
const { name, arg, exp, loc } = prop
|
||||
const isBind = name === 'bind'
|
||||
const isOn = name === 'on'
|
||||
|
||||
// skip v-slot - it is handled by its dedicated transform.
|
||||
if (name === 'slot') {
|
||||
@@ -305,15 +342,23 @@ export function buildProps(
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// skip v-once - it is handled by its dedicated transform.
|
||||
if (name === 'once') {
|
||||
continue
|
||||
}
|
||||
// skip v-is and :is on <component>
|
||||
if (
|
||||
name === 'is' ||
|
||||
(isBind && tag === 'component' && isBindKey(arg, 'is'))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
// skip v-on in SSR compilation
|
||||
if (isOn && ssr) {
|
||||
continue
|
||||
}
|
||||
|
||||
// special case for v-bind and v-on with no argument
|
||||
const isBind = name === 'bind'
|
||||
const isOn = name === 'on'
|
||||
if (!arg && (isBind || isOn)) {
|
||||
hasDynamicKeys = true
|
||||
if (exp) {
|
||||
@@ -351,7 +396,7 @@ export function buildProps(
|
||||
if (directiveTransform) {
|
||||
// has built-in directive transform.
|
||||
const { props, needRuntime } = directiveTransform(prop, node, context)
|
||||
props.forEach(analyzePatchFlag)
|
||||
!ssr && props.forEach(analyzePatchFlag)
|
||||
properties.push(...props)
|
||||
if (needRuntime) {
|
||||
runtimeDirectives.push(prop)
|
||||
@@ -405,8 +450,14 @@ export function buildProps(
|
||||
if (dynamicPropNames.length) {
|
||||
patchFlag |= PatchFlags.PROPS
|
||||
}
|
||||
if (hasHydrationEventBinding) {
|
||||
patchFlag |= PatchFlags.HYDRATE_EVENTS
|
||||
}
|
||||
}
|
||||
if (patchFlag === 0 && (hasRef || runtimeDirectives.length > 0)) {
|
||||
if (
|
||||
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
|
||||
(hasRef || runtimeDirectives.length > 0)
|
||||
) {
|
||||
patchFlag |= PatchFlags.NEED_PATCH
|
||||
}
|
||||
|
||||
@@ -437,12 +488,7 @@ function dedupeProperties(properties: Property[]): Property[] {
|
||||
const name = prop.key.content
|
||||
const existing = knownProps.get(name)
|
||||
if (existing) {
|
||||
if (
|
||||
name === 'style' ||
|
||||
name === 'class' ||
|
||||
name.startsWith('on') ||
|
||||
name.startsWith('vnode')
|
||||
) {
|
||||
if (name === 'style' || name === 'class' || name.startsWith('on')) {
|
||||
mergeAsArray(existing, prop)
|
||||
}
|
||||
// unexpected duplicate, should have emitted error during parse
|
||||
@@ -472,7 +518,6 @@ function buildDirectiveArgs(
|
||||
const dirArgs: ArrayExpression['elements'] = []
|
||||
const runtime = directiveImportMap.get(dir)
|
||||
if (runtime) {
|
||||
context.helper(runtime)
|
||||
dirArgs.push(context.helperString(runtime))
|
||||
} else {
|
||||
// inject statement for resolving directive
|
||||
@@ -507,3 +552,12 @@ function buildDirectiveArgs(
|
||||
}
|
||||
return createArrayExpression(dirArgs, dir.loc)
|
||||
}
|
||||
|
||||
function stringifyDynamicPropNames(props: string[]): string {
|
||||
let propsNamesString = `[`
|
||||
for (let i = 0, l = props.length; i < l; i++) {
|
||||
propsNamesString += JSON.stringify(props[i])
|
||||
if (i < l - 1) propsNamesString += ', '
|
||||
}
|
||||
return propsNamesString + `]`
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
CompoundExpressionNode,
|
||||
createCompoundExpression
|
||||
} from '../ast'
|
||||
import { Node, Function, Identifier, Property } from 'estree'
|
||||
import {
|
||||
advancePositionWithClone,
|
||||
isSimpleIdentifier,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
} from '../utils'
|
||||
import { isGloballyWhitelisted, makeMap } from '@vue/shared'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
|
||||
|
||||
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
|
||||
|
||||
@@ -40,11 +40,15 @@ export const transformExpression: NodeTransform = (node, context) => {
|
||||
const dir = node.props[i]
|
||||
// do not process for v-on & v-for since they are special handled
|
||||
if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
|
||||
const exp = dir.exp as SimpleExpressionNode | undefined
|
||||
const arg = dir.arg as SimpleExpressionNode | undefined
|
||||
const exp = dir.exp
|
||||
const arg = dir.arg
|
||||
// do not process exp if this is v-on:arg - we need special handling
|
||||
// for wrapping inline statements.
|
||||
if (exp && !(dir.name === 'on' && arg)) {
|
||||
if (
|
||||
exp &&
|
||||
exp.type === NodeTypes.SIMPLE_EXPRESSION &&
|
||||
!(dir.name === 'on' && arg)
|
||||
) {
|
||||
dir.exp = processExpression(
|
||||
exp,
|
||||
context,
|
||||
@@ -52,7 +56,7 @@ export const transformExpression: NodeTransform = (node, context) => {
|
||||
dir.name === 'slot'
|
||||
)
|
||||
}
|
||||
if (arg && !arg.isStatic) {
|
||||
if (arg && arg.type === NodeTypes.SIMPLE_EXPRESSION && !arg.isStatic) {
|
||||
dir.arg = processExpression(arg, context)
|
||||
}
|
||||
}
|
||||
@@ -76,7 +80,9 @@ export function processExpression(
|
||||
context: TransformContext,
|
||||
// some expressions like v-slot props & v-for aliases should be parsed as
|
||||
// function params
|
||||
asParams: boolean = false
|
||||
asParams = false,
|
||||
// v-on handler values may contain multiple statements
|
||||
asRawStatements = false
|
||||
): ExpressionNode {
|
||||
if (!context.prefixIdentifiers || !node.content.trim()) {
|
||||
return node
|
||||
@@ -84,6 +90,8 @@ export function processExpression(
|
||||
|
||||
// fast path if expression is a simple identifier.
|
||||
const rawExp = node.content
|
||||
// bail on parens to prevent any possible function invocations.
|
||||
const bailConstant = rawExp.indexOf(`(`) > -1
|
||||
if (isSimpleIdentifier(rawExp)) {
|
||||
if (
|
||||
!asParams &&
|
||||
@@ -92,7 +100,7 @@ export function processExpression(
|
||||
!isLiteralWhitelisted(rawExp)
|
||||
) {
|
||||
node.content = `_ctx.${rawExp}`
|
||||
} else if (!context.identifiers[rawExp]) {
|
||||
} else if (!context.identifiers[rawExp] && !bailConstant) {
|
||||
// mark node constant for hoisting unless it's referring a scope variable
|
||||
node.isConstant = true
|
||||
}
|
||||
@@ -100,26 +108,48 @@ export function processExpression(
|
||||
}
|
||||
|
||||
let ast: any
|
||||
// if the expression is supposed to be used in a function params position
|
||||
// we need to parse it differently.
|
||||
const source = `(${rawExp})${asParams ? `=>{}` : ``}`
|
||||
// exp needs to be parsed differently:
|
||||
// 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
|
||||
// exp, but make sure to pad with spaces for consistent ranges
|
||||
// 2. Expressions: wrap with parens (for e.g. object expressions)
|
||||
// 3. Function arguments (v-for, v-slot): place in a function argument position
|
||||
const source = asRawStatements
|
||||
? ` ${rawExp} `
|
||||
: `(${rawExp})${asParams ? `=>{}` : ``}`
|
||||
try {
|
||||
ast = parseJS(source, { ranges: true })
|
||||
ast = parseJS(source, {
|
||||
plugins: [
|
||||
...context.expressionPlugins,
|
||||
// by default we enable proposals slated for ES2020.
|
||||
// full list at https://babeljs.io/docs/en/next/babel-parser#plugins
|
||||
// this will need to be updated as the spec moves forward.
|
||||
'bigInt',
|
||||
'optionalChaining',
|
||||
'nullishCoalescingOperator'
|
||||
]
|
||||
}).program
|
||||
} catch (e) {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_INVALID_EXPRESSION, node.loc)
|
||||
createCompilerError(
|
||||
ErrorCodes.X_INVALID_EXPRESSION,
|
||||
node.loc,
|
||||
undefined,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
return node
|
||||
}
|
||||
|
||||
const ids: (Identifier & PrefixMeta)[] = []
|
||||
const knownIds = Object.create(context.identifiers)
|
||||
const isDuplicate = (node: Node & PrefixMeta): boolean =>
|
||||
ids.some(id => id.start === node.start)
|
||||
|
||||
// walk the AST and look for identifiers that need to be prefixed with `_ctx.`.
|
||||
walkJS(ast, {
|
||||
enter(node: Node & PrefixMeta, parent) {
|
||||
if (node.type === 'Identifier') {
|
||||
if (!ids.includes(node)) {
|
||||
if (!isDuplicate(node)) {
|
||||
const needPrefix = shouldPrefix(node, parent)
|
||||
if (!knownIds[node.name] && needPrefix) {
|
||||
if (isPropertyShorthand(node, parent)) {
|
||||
@@ -128,12 +158,13 @@ export function processExpression(
|
||||
node.prefix = `${node.name}: `
|
||||
}
|
||||
node.name = `_ctx.${node.name}`
|
||||
node.isConstant = false
|
||||
ids.push(node)
|
||||
} else if (!isStaticPropertyKey(node, parent)) {
|
||||
// The identifier is considered constant unless it's pointing to a
|
||||
// scope variable (a v-for alias, or a v-slot prop)
|
||||
node.isConstant = !(needPrefix && knownIds[node.name])
|
||||
if (!(needPrefix && knownIds[node.name]) && !bailConstant) {
|
||||
node.isConstant = true
|
||||
}
|
||||
// also generate sub-expressions for other identifiers for better
|
||||
// source map support. (except for property keys which are static)
|
||||
ids.push(node)
|
||||
@@ -223,7 +254,7 @@ export function processExpression(
|
||||
ret = createCompoundExpression(children, node.loc)
|
||||
} else {
|
||||
ret = node
|
||||
ret.isConstant = true
|
||||
ret.isConstant = !bailConstant
|
||||
}
|
||||
ret.identifiers = Object.keys(knownIds)
|
||||
return ret
|
||||
@@ -232,17 +263,21 @@ export function processExpression(
|
||||
const isFunction = (node: Node): node is Function =>
|
||||
/Function(Expression|Declaration)$/.test(node.type)
|
||||
|
||||
const isPropertyKey = (node: Node, parent: Node) =>
|
||||
parent &&
|
||||
parent.type === 'Property' &&
|
||||
parent.key === node &&
|
||||
!parent.computed
|
||||
const isStaticProperty = (node: Node): node is ObjectProperty =>
|
||||
node && node.type === 'ObjectProperty' && !node.computed
|
||||
|
||||
const isPropertyShorthand = (node: Node, parent: Node) =>
|
||||
isPropertyKey(node, parent) && (parent as Property).value === node
|
||||
const isPropertyShorthand = (node: Node, parent: Node) => {
|
||||
return (
|
||||
isStaticProperty(parent) &&
|
||||
parent.value === node &&
|
||||
parent.key.type === 'Identifier' &&
|
||||
parent.key.name === (node as Identifier).name &&
|
||||
parent.key.start === node.start
|
||||
)
|
||||
}
|
||||
|
||||
const isStaticPropertyKey = (node: Node, parent: Node) =>
|
||||
isPropertyKey(node, parent) && (parent as Property).value !== node
|
||||
isStaticProperty(parent) && parent.key === node
|
||||
|
||||
function shouldPrefix(identifier: Identifier, parent: Node) {
|
||||
if (
|
||||
@@ -257,7 +292,8 @@ function shouldPrefix(identifier: Identifier, parent: Node) {
|
||||
!isStaticPropertyKey(identifier, parent) &&
|
||||
// not a property of a MemberExpression
|
||||
!(
|
||||
parent.type === 'MemberExpression' &&
|
||||
(parent.type === 'MemberExpression' ||
|
||||
parent.type === 'OptionalMemberExpression') &&
|
||||
parent.property === identifier &&
|
||||
!parent.computed
|
||||
) &&
|
||||
|
||||
@@ -1,78 +1,32 @@
|
||||
import { NodeTransform } from '../transform'
|
||||
import { NodeTransform, TransformContext } from '../transform'
|
||||
import {
|
||||
NodeTypes,
|
||||
CallExpression,
|
||||
createCallExpression,
|
||||
ExpressionNode
|
||||
ExpressionNode,
|
||||
SlotOutletNode
|
||||
} from '../ast'
|
||||
import { isSlotOutlet } from '../utils'
|
||||
import { buildProps } from './transformElement'
|
||||
import { isSlotOutlet, findProp } from '../utils'
|
||||
import { buildProps, PropsExpression } from './transformElement'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { RENDER_SLOT } from '../runtimeHelpers'
|
||||
|
||||
export const transformSlotOutlet: NodeTransform = (node, context) => {
|
||||
if (isSlotOutlet(node)) {
|
||||
const { props, children, loc } = node
|
||||
const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
|
||||
let slotName: string | ExpressionNode = `"default"`
|
||||
const { children, loc } = node
|
||||
const { slotName, slotProps } = processSlotOutlet(node, context)
|
||||
|
||||
// check for <slot name="xxx" OR :name="xxx" />
|
||||
let nameIndex: number = -1
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
const prop = props[i]
|
||||
if (prop.type === NodeTypes.ATTRIBUTE) {
|
||||
if (prop.name === `name` && prop.value) {
|
||||
// static name="xxx"
|
||||
slotName = JSON.stringify(prop.value.content)
|
||||
nameIndex = i
|
||||
break
|
||||
}
|
||||
} else if (prop.name === `bind`) {
|
||||
const { arg, exp } = prop
|
||||
if (
|
||||
arg &&
|
||||
exp &&
|
||||
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
|
||||
arg.isStatic &&
|
||||
arg.content === `name`
|
||||
) {
|
||||
// dynamic :name="xxx"
|
||||
slotName = exp
|
||||
nameIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const slotArgs: CallExpression['arguments'] = [
|
||||
context.prefixIdentifiers ? `_ctx.$slots` : `$slots`,
|
||||
slotName
|
||||
]
|
||||
|
||||
const slotArgs: CallExpression['arguments'] = [$slots, slotName]
|
||||
const propsWithoutName =
|
||||
nameIndex > -1
|
||||
? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
|
||||
: props
|
||||
let hasProps = propsWithoutName.length > 0
|
||||
if (hasProps) {
|
||||
const { props: propsExpression, directives } = buildProps(
|
||||
node,
|
||||
context,
|
||||
propsWithoutName
|
||||
)
|
||||
if (directives.length) {
|
||||
context.onError(
|
||||
createCompilerError(
|
||||
ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
|
||||
directives[0].loc
|
||||
)
|
||||
)
|
||||
}
|
||||
if (propsExpression) {
|
||||
slotArgs.push(propsExpression)
|
||||
} else {
|
||||
hasProps = false
|
||||
}
|
||||
if (slotProps) {
|
||||
slotArgs.push(slotProps)
|
||||
}
|
||||
|
||||
if (children.length) {
|
||||
if (!hasProps) {
|
||||
if (!slotProps) {
|
||||
slotArgs.push(`{}`)
|
||||
}
|
||||
slotArgs.push(children)
|
||||
@@ -85,3 +39,49 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface SlotOutletProcessResult {
|
||||
slotName: string | ExpressionNode
|
||||
slotProps: PropsExpression | undefined
|
||||
}
|
||||
|
||||
export function processSlotOutlet(
|
||||
node: SlotOutletNode,
|
||||
context: TransformContext
|
||||
): SlotOutletProcessResult {
|
||||
let slotName: string | ExpressionNode = `"default"`
|
||||
let slotProps: PropsExpression | undefined = undefined
|
||||
|
||||
// check for <slot name="xxx" OR :name="xxx" />
|
||||
const name = findProp(node, 'name')
|
||||
if (name) {
|
||||
if (name.type === NodeTypes.ATTRIBUTE && name.value) {
|
||||
// static name
|
||||
slotName = JSON.stringify(name.value.content)
|
||||
} else if (name.type === NodeTypes.DIRECTIVE && name.exp) {
|
||||
// dynamic name
|
||||
slotName = name.exp
|
||||
}
|
||||
}
|
||||
|
||||
const propsWithoutName = name
|
||||
? node.props.filter(p => p !== name)
|
||||
: node.props
|
||||
if (propsWithoutName.length > 0) {
|
||||
const { props, directives } = buildProps(node, context, propsWithoutName)
|
||||
slotProps = props
|
||||
if (directives.length) {
|
||||
context.onError(
|
||||
createCompilerError(
|
||||
ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
|
||||
directives[0].loc
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
slotName,
|
||||
slotProps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import { NodeTransform } from '../transform'
|
||||
import {
|
||||
NodeTypes,
|
||||
TemplateChildNode,
|
||||
TextNode,
|
||||
InterpolationNode,
|
||||
CompoundExpressionNode,
|
||||
createCallExpression,
|
||||
CallExpression,
|
||||
ElementTypes
|
||||
} from '../ast'
|
||||
import { isText } from '../utils'
|
||||
import { CREATE_TEXT } from '../runtimeHelpers'
|
||||
import { PatchFlags, PatchFlagNames } from '@vue/shared'
|
||||
|
||||
const isText = (
|
||||
node: TemplateChildNode
|
||||
): node is TextNode | InterpolationNode =>
|
||||
node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
|
||||
|
||||
// Merge adjacent text nodes and expressions into a single expression
|
||||
// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
|
||||
export const transformText: NodeTransform = (node, context) => {
|
||||
if (
|
||||
node.type === NodeTypes.ROOT ||
|
||||
node.type === NodeTypes.ELEMENT ||
|
||||
node.type === NodeTypes.FOR
|
||||
node.type === NodeTypes.FOR ||
|
||||
node.type === NodeTypes.IF_BRANCH
|
||||
) {
|
||||
// perform the transform on node exit so that all expressions have already
|
||||
// been processed.
|
||||
@@ -84,7 +78,7 @@ export const transformText: NodeTransform = (node, context) => {
|
||||
callArgs.push(child)
|
||||
}
|
||||
// mark dynamic text with flag so it gets patched inside a block
|
||||
if (child.type !== NodeTypes.TEXT) {
|
||||
if (!context.ssr && child.type !== NodeTypes.TEXT) {
|
||||
callArgs.push(
|
||||
`${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */`
|
||||
)
|
||||
|
||||
@@ -30,7 +30,6 @@ export const transformBind: DirectiveTransform = (dir, node, context) => {
|
||||
return {
|
||||
props: [
|
||||
createObjectProperty(arg!, exp || createSimpleExpression('', true, loc))
|
||||
],
|
||||
needRuntime: false
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,22 +8,28 @@ import {
|
||||
createSimpleExpression,
|
||||
SourceLocation,
|
||||
SimpleExpressionNode,
|
||||
createSequenceExpression,
|
||||
createCallExpression,
|
||||
createFunctionExpression,
|
||||
ElementTypes,
|
||||
createObjectExpression,
|
||||
createObjectProperty,
|
||||
ForCodegenNode,
|
||||
ElementCodegenNode,
|
||||
SlotOutletCodegenNode,
|
||||
SlotOutletNode
|
||||
RenderSlotCall,
|
||||
SlotOutletNode,
|
||||
ElementNode,
|
||||
DirectiveNode,
|
||||
ForNode,
|
||||
PlainElementNode,
|
||||
createVNodeCall,
|
||||
VNodeCall,
|
||||
ForRenderListExpression,
|
||||
BlockCodegenNode,
|
||||
ForIteratorExpression
|
||||
} from '../ast'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import {
|
||||
getInnerRange,
|
||||
findProp,
|
||||
createBlockExpression,
|
||||
isTemplateNode,
|
||||
isSlotOutlet,
|
||||
injectProp
|
||||
@@ -32,8 +38,7 @@ import {
|
||||
RENDER_LIST,
|
||||
OPEN_BLOCK,
|
||||
CREATE_BLOCK,
|
||||
FRAGMENT,
|
||||
WITH_DIRECTIVES
|
||||
FRAGMENT
|
||||
} from '../runtimeHelpers'
|
||||
import { processExpression } from './transformExpression'
|
||||
import { PatchFlags, PatchFlagNames } from '@vue/shared'
|
||||
@@ -41,141 +46,163 @@ import { PatchFlags, PatchFlagNames } from '@vue/shared'
|
||||
export const transformFor = createStructuralDirectiveTransform(
|
||||
'for',
|
||||
(node, dir, context) => {
|
||||
if (!dir.exp) {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const parseResult = parseForExpression(
|
||||
// can only be simple expression because vFor transform is applied
|
||||
// before expression transform.
|
||||
dir.exp as SimpleExpressionNode,
|
||||
context
|
||||
)
|
||||
|
||||
if (!parseResult) {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const { helper, addIdentifiers, removeIdentifiers, scopes } = context
|
||||
const { source, value, key, index } = parseResult
|
||||
|
||||
// create the loop render function expression now, and add the
|
||||
// iterator on exit after all children have been traversed
|
||||
const renderExp = createCallExpression(helper(RENDER_LIST), [source])
|
||||
const keyProp = findProp(node, `key`)
|
||||
const fragmentFlag = keyProp
|
||||
? PatchFlags.KEYED_FRAGMENT
|
||||
: PatchFlags.UNKEYED_FRAGMENT
|
||||
const codegenNode = createSequenceExpression([
|
||||
// fragment blocks disable tracking since they always diff their children
|
||||
createCallExpression(helper(OPEN_BLOCK), [`false`]),
|
||||
createCallExpression(helper(CREATE_BLOCK), [
|
||||
const { helper } = context
|
||||
return processFor(node, dir, context, forNode => {
|
||||
// create the loop render function expression now, and add the
|
||||
// iterator on exit after all children have been traversed
|
||||
const renderExp = createCallExpression(helper(RENDER_LIST), [
|
||||
forNode.source
|
||||
]) as ForRenderListExpression
|
||||
const keyProp = findProp(node, `key`)
|
||||
const fragmentFlag = keyProp
|
||||
? PatchFlags.KEYED_FRAGMENT
|
||||
: PatchFlags.UNKEYED_FRAGMENT
|
||||
forNode.codegenNode = createVNodeCall(
|
||||
context,
|
||||
helper(FRAGMENT),
|
||||
`null`,
|
||||
undefined,
|
||||
renderExp,
|
||||
`${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`
|
||||
])
|
||||
]) as ForCodegenNode
|
||||
`${fragmentFlag} /* ${PatchFlagNames[fragmentFlag]} */`,
|
||||
undefined,
|
||||
undefined,
|
||||
true /* isBlock */,
|
||||
true /* isForBlock */,
|
||||
node.loc
|
||||
) as ForCodegenNode
|
||||
|
||||
context.replaceNode({
|
||||
type: NodeTypes.FOR,
|
||||
loc: dir.loc,
|
||||
source,
|
||||
valueAlias: value,
|
||||
keyAlias: key,
|
||||
objectIndexAlias: index,
|
||||
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node],
|
||||
codegenNode
|
||||
})
|
||||
|
||||
// bookkeeping
|
||||
scopes.vFor++
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
// scope management
|
||||
// inject identifiers to context
|
||||
value && addIdentifiers(value)
|
||||
key && addIdentifiers(key)
|
||||
index && addIdentifiers(index)
|
||||
}
|
||||
|
||||
return () => {
|
||||
scopes.vFor--
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
value && removeIdentifiers(value)
|
||||
key && removeIdentifiers(key)
|
||||
index && removeIdentifiers(index)
|
||||
}
|
||||
|
||||
// finish the codegen now that all children have been traversed
|
||||
let childBlock
|
||||
const isTemplate = isTemplateNode(node)
|
||||
const slotOutlet = isSlotOutlet(node)
|
||||
? node
|
||||
: isTemplate &&
|
||||
node.children.length === 1 &&
|
||||
isSlotOutlet(node.children[0])
|
||||
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
|
||||
return () => {
|
||||
// finish the codegen now that all children have been traversed
|
||||
let childBlock: BlockCodegenNode
|
||||
const isTemplate = isTemplateNode(node)
|
||||
const { children } = forNode
|
||||
const needFragmentWrapper =
|
||||
children.length > 1 || children[0].type !== NodeTypes.ELEMENT
|
||||
const slotOutlet = isSlotOutlet(node)
|
||||
? node
|
||||
: isTemplate &&
|
||||
node.children.length === 1 &&
|
||||
isSlotOutlet(node.children[0])
|
||||
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
|
||||
: null
|
||||
const keyProperty = keyProp
|
||||
? createObjectProperty(
|
||||
`key`,
|
||||
keyProp.type === NodeTypes.ATTRIBUTE
|
||||
? createSimpleExpression(keyProp.value!.content, true)
|
||||
: keyProp.exp!
|
||||
)
|
||||
: null
|
||||
const keyProperty = keyProp
|
||||
? createObjectProperty(
|
||||
`key`,
|
||||
keyProp.type === NodeTypes.ATTRIBUTE
|
||||
? createSimpleExpression(keyProp.value!.content, true)
|
||||
: keyProp.exp!
|
||||
)
|
||||
: null
|
||||
if (slotOutlet) {
|
||||
// <slot v-for="..."> or <template v-for="..."><slot/></template>
|
||||
childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
|
||||
if (isTemplate && keyProperty) {
|
||||
// <template v-for="..." :key="..."><slot/></template>
|
||||
// we need to inject the key to the renderSlot() call.
|
||||
// the props for renderSlot is passed as the 3rd argument.
|
||||
injectProp(childBlock, keyProperty, context)
|
||||
}
|
||||
} else if (isTemplate) {
|
||||
// <template v-for="...">
|
||||
// should generate a fragment block for each loop
|
||||
childBlock = createBlockExpression(
|
||||
createCallExpression(helper(CREATE_BLOCK), [
|
||||
if (slotOutlet) {
|
||||
// <slot v-for="..."> or <template v-for="..."><slot/></template>
|
||||
childBlock = slotOutlet.codegenNode as RenderSlotCall
|
||||
if (isTemplate && keyProperty) {
|
||||
// <template v-for="..." :key="..."><slot/></template>
|
||||
// we need to inject the key to the renderSlot() call.
|
||||
// the props for renderSlot is passed as the 3rd argument.
|
||||
injectProp(childBlock, keyProperty, context)
|
||||
}
|
||||
} else if (needFragmentWrapper) {
|
||||
// <template v-for="..."> with text or multi-elements
|
||||
// should generate a fragment block for each loop
|
||||
childBlock = createVNodeCall(
|
||||
context,
|
||||
helper(FRAGMENT),
|
||||
keyProperty ? createObjectExpression([keyProperty]) : `null`,
|
||||
keyProperty ? createObjectExpression([keyProperty]) : undefined,
|
||||
node.children,
|
||||
`${PatchFlags.STABLE_FRAGMENT} /* ${
|
||||
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
|
||||
} */`
|
||||
]),
|
||||
context
|
||||
)
|
||||
} else {
|
||||
// Normal element v-for. Directly use the child's codegenNode
|
||||
// arguments, but replace createVNode() with createBlock()
|
||||
let codegenNode = node.codegenNode as ElementCodegenNode
|
||||
if (codegenNode.callee === WITH_DIRECTIVES) {
|
||||
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
|
||||
} */`,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
codegenNode.callee = helper(CREATE_BLOCK)
|
||||
// Normal element v-for. Directly use the child's codegenNode
|
||||
// but mark it as a block.
|
||||
childBlock = (children[0] as PlainElementNode)
|
||||
.codegenNode as VNodeCall
|
||||
childBlock.isBlock = true
|
||||
helper(OPEN_BLOCK)
|
||||
helper(CREATE_BLOCK)
|
||||
}
|
||||
childBlock = createBlockExpression(codegenNode, context)
|
||||
}
|
||||
|
||||
renderExp.arguments.push(
|
||||
createFunctionExpression(
|
||||
createForLoopParams(parseResult),
|
||||
renderExp.arguments.push(createFunctionExpression(
|
||||
createForLoopParams(forNode.parseResult),
|
||||
childBlock,
|
||||
true /* force newline */
|
||||
)
|
||||
)
|
||||
}
|
||||
) as ForIteratorExpression)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// target-agnostic transform used for both Client and SSR
|
||||
export function processFor(
|
||||
node: ElementNode,
|
||||
dir: DirectiveNode,
|
||||
context: TransformContext,
|
||||
processCodegen?: (forNode: ForNode) => (() => void) | undefined
|
||||
) {
|
||||
if (!dir.exp) {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const parseResult = parseForExpression(
|
||||
// can only be simple expression because vFor transform is applied
|
||||
// before expression transform.
|
||||
dir.exp as SimpleExpressionNode,
|
||||
context
|
||||
)
|
||||
|
||||
if (!parseResult) {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const { addIdentifiers, removeIdentifiers, scopes } = context
|
||||
const { source, value, key, index } = parseResult
|
||||
|
||||
const forNode: ForNode = {
|
||||
type: NodeTypes.FOR,
|
||||
loc: dir.loc,
|
||||
source,
|
||||
valueAlias: value,
|
||||
keyAlias: key,
|
||||
objectIndexAlias: index,
|
||||
parseResult,
|
||||
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
|
||||
}
|
||||
|
||||
context.replaceNode(forNode)
|
||||
|
||||
// bookkeeping
|
||||
scopes.vFor++
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
// scope management
|
||||
// inject identifiers to context
|
||||
value && addIdentifiers(value)
|
||||
key && addIdentifiers(key)
|
||||
index && addIdentifiers(index)
|
||||
}
|
||||
|
||||
const onExit = processCodegen && processCodegen(forNode)
|
||||
|
||||
return () => {
|
||||
scopes.vFor--
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
value && removeIdentifiers(value)
|
||||
key && removeIdentifiers(key)
|
||||
index && removeIdentifiers(index)
|
||||
}
|
||||
if (onExit) onExit()
|
||||
}
|
||||
}
|
||||
|
||||
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
|
||||
// This regex doesn't cover the case if key or index aliases have destructuring,
|
||||
// but those do not make sense in the first place, so this works in practice.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
createStructuralDirectiveTransform,
|
||||
traverseChildren,
|
||||
TransformContext
|
||||
TransformContext,
|
||||
traverseNode
|
||||
} from '../transform'
|
||||
import {
|
||||
NodeTypes,
|
||||
@@ -10,130 +10,139 @@ import {
|
||||
DirectiveNode,
|
||||
IfBranchNode,
|
||||
SimpleExpressionNode,
|
||||
createSequenceExpression,
|
||||
createCallExpression,
|
||||
createConditionalExpression,
|
||||
ConditionalExpression,
|
||||
CallExpression,
|
||||
createSimpleExpression,
|
||||
createObjectProperty,
|
||||
createObjectExpression,
|
||||
IfCodegenNode,
|
||||
IfConditionalExpression,
|
||||
BlockCodegenNode,
|
||||
SlotOutletCodegenNode,
|
||||
ElementCodegenNode,
|
||||
ComponentCodegenNode
|
||||
IfNode,
|
||||
createVNodeCall
|
||||
} from '../ast'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { processExpression } from './transformExpression'
|
||||
import {
|
||||
OPEN_BLOCK,
|
||||
CREATE_BLOCK,
|
||||
FRAGMENT,
|
||||
WITH_DIRECTIVES,
|
||||
CREATE_VNODE,
|
||||
CREATE_COMMENT
|
||||
CREATE_COMMENT,
|
||||
OPEN_BLOCK,
|
||||
TELEPORT
|
||||
} from '../runtimeHelpers'
|
||||
import { injectProp } from '../utils'
|
||||
import { PatchFlags, PatchFlagNames } from '@vue/shared'
|
||||
|
||||
export const transformIf = createStructuralDirectiveTransform(
|
||||
/^(if|else|else-if)$/,
|
||||
(node, dir, context) => {
|
||||
if (
|
||||
dir.name !== 'else' &&
|
||||
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
|
||||
) {
|
||||
const loc = dir.exp ? dir.exp.loc : node.loc
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc)
|
||||
)
|
||||
dir.exp = createSimpleExpression(`true`, false, loc)
|
||||
}
|
||||
|
||||
if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) {
|
||||
// dir.exp can only be simple expression because vIf transform is applied
|
||||
// before expression transform.
|
||||
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
|
||||
}
|
||||
|
||||
if (dir.name === 'if') {
|
||||
const branch = createIfBranch(node, dir)
|
||||
const codegenNode = createSequenceExpression([
|
||||
createCallExpression(context.helper(OPEN_BLOCK))
|
||||
]) as IfCodegenNode
|
||||
|
||||
context.replaceNode({
|
||||
type: NodeTypes.IF,
|
||||
loc: node.loc,
|
||||
branches: [branch],
|
||||
codegenNode
|
||||
})
|
||||
|
||||
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
|
||||
// Exit callback. Complete the codegenNode when all children have been
|
||||
// transformed.
|
||||
return () => {
|
||||
codegenNode.expressions.push(createCodegenNodeForBranch(
|
||||
branch,
|
||||
0,
|
||||
context
|
||||
) as IfConditionalExpression)
|
||||
}
|
||||
} else {
|
||||
// locate the adjacent v-if
|
||||
const siblings = context.parent!.children
|
||||
const comments = []
|
||||
let i = siblings.indexOf(node)
|
||||
while (i-- >= -1) {
|
||||
const sibling = siblings[i]
|
||||
if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) {
|
||||
context.removeNode(sibling)
|
||||
comments.unshift(sibling)
|
||||
continue
|
||||
}
|
||||
if (sibling && sibling.type === NodeTypes.IF) {
|
||||
// move the node to the if node's branches
|
||||
context.removeNode()
|
||||
const branch = createIfBranch(node, dir)
|
||||
if (__DEV__ && comments.length) {
|
||||
branch.children = [...comments, ...branch.children]
|
||||
}
|
||||
sibling.branches.push(branch)
|
||||
// since the branch was removed, it will not be traversed.
|
||||
// make sure to traverse here.
|
||||
traverseChildren(branch, context)
|
||||
// make sure to reset currentNode after traversal to indicate this
|
||||
// node has been removed.
|
||||
context.currentNode = null
|
||||
// attach this branch's codegen node to the v-if root.
|
||||
let parentCondition = sibling.codegenNode
|
||||
.expressions[1] as ConditionalExpression
|
||||
while (true) {
|
||||
if (
|
||||
parentCondition.alternate.type ===
|
||||
NodeTypes.JS_CONDITIONAL_EXPRESSION
|
||||
) {
|
||||
parentCondition = parentCondition.alternate
|
||||
} else {
|
||||
parentCondition.alternate = createCodegenNodeForBranch(
|
||||
branch,
|
||||
sibling.branches.length - 1,
|
||||
context
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isRoot) {
|
||||
ifNode.codegenNode = createCodegenNodeForBranch(
|
||||
branch,
|
||||
0,
|
||||
context
|
||||
) as IfConditionalExpression
|
||||
} else {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc)
|
||||
// attach this branch's codegen node to the v-if root.
|
||||
let parentCondition = ifNode.codegenNode!
|
||||
while (
|
||||
parentCondition.alternate.type ===
|
||||
NodeTypes.JS_CONDITIONAL_EXPRESSION
|
||||
) {
|
||||
parentCondition = parentCondition.alternate
|
||||
}
|
||||
parentCondition.alternate = createCodegenNodeForBranch(
|
||||
branch,
|
||||
ifNode.branches.length - 1,
|
||||
context
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// target-agnostic transform used for both Client and SSR
|
||||
export function processIf(
|
||||
node: ElementNode,
|
||||
dir: DirectiveNode,
|
||||
context: TransformContext,
|
||||
processCodegen?: (
|
||||
node: IfNode,
|
||||
branch: IfBranchNode,
|
||||
isRoot: boolean
|
||||
) => (() => void) | undefined
|
||||
) {
|
||||
if (
|
||||
dir.name !== 'else' &&
|
||||
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
|
||||
) {
|
||||
const loc = dir.exp ? dir.exp.loc : node.loc
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc)
|
||||
)
|
||||
dir.exp = createSimpleExpression(`true`, false, loc)
|
||||
}
|
||||
|
||||
if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) {
|
||||
// dir.exp can only be simple expression because vIf transform is applied
|
||||
// before expression transform.
|
||||
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
|
||||
}
|
||||
|
||||
if (dir.name === 'if') {
|
||||
const branch = createIfBranch(node, dir)
|
||||
const ifNode: IfNode = {
|
||||
type: NodeTypes.IF,
|
||||
loc: node.loc,
|
||||
branches: [branch]
|
||||
}
|
||||
context.replaceNode(ifNode)
|
||||
if (processCodegen) {
|
||||
return processCodegen(ifNode, branch, true)
|
||||
}
|
||||
} else {
|
||||
// locate the adjacent v-if
|
||||
const siblings = context.parent!.children
|
||||
const comments = []
|
||||
let i = siblings.indexOf(node)
|
||||
while (i-- >= -1) {
|
||||
const sibling = siblings[i]
|
||||
if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) {
|
||||
context.removeNode(sibling)
|
||||
comments.unshift(sibling)
|
||||
continue
|
||||
}
|
||||
if (sibling && sibling.type === NodeTypes.IF) {
|
||||
// move the node to the if node's branches
|
||||
context.removeNode()
|
||||
const branch = createIfBranch(node, dir)
|
||||
if (__DEV__ && comments.length) {
|
||||
branch.children = [...comments, ...branch.children]
|
||||
}
|
||||
sibling.branches.push(branch)
|
||||
const onExit = processCodegen && processCodegen(sibling, branch, false)
|
||||
// since the branch was removed, it will not be traversed.
|
||||
// make sure to traverse here.
|
||||
traverseNode(branch, context)
|
||||
// call on exit
|
||||
if (onExit) onExit()
|
||||
// make sure to reset currentNode after traversal to indicate this
|
||||
// node has been removed.
|
||||
context.currentNode = null
|
||||
} else {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc)
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
|
||||
return {
|
||||
type: NodeTypes.IF_BRANCH,
|
||||
@@ -160,7 +169,7 @@ function createCodegenNodeForBranch(
|
||||
])
|
||||
) as IfConditionalExpression
|
||||
} else {
|
||||
return createChildrenCodegenNode(branch, index, context) as BlockCodegenNode
|
||||
return createChildrenCodegenNode(branch, index, context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,46 +177,56 @@ function createChildrenCodegenNode(
|
||||
branch: IfBranchNode,
|
||||
index: number,
|
||||
context: TransformContext
|
||||
): CallExpression {
|
||||
): BlockCodegenNode {
|
||||
const { helper } = context
|
||||
const keyProperty = createObjectProperty(
|
||||
`key`,
|
||||
createSimpleExpression(index + '', false)
|
||||
)
|
||||
const { children } = branch
|
||||
const child = children[0]
|
||||
const firstChild = children[0]
|
||||
const needFragmentWrapper =
|
||||
children.length !== 1 || child.type !== NodeTypes.ELEMENT
|
||||
children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
|
||||
if (needFragmentWrapper) {
|
||||
const blockArgs: CallExpression['arguments'] = [
|
||||
helper(FRAGMENT),
|
||||
createObjectExpression([keyProperty]),
|
||||
children
|
||||
]
|
||||
if (children.length === 1 && child.type === NodeTypes.FOR) {
|
||||
if (children.length === 1 && firstChild.type === NodeTypes.FOR) {
|
||||
// optimize away nested fragments when child is a ForNode
|
||||
const forBlockArgs = child.codegenNode.expressions[1].arguments
|
||||
// directly use the for block's children and patchFlag
|
||||
blockArgs[2] = forBlockArgs[2]
|
||||
blockArgs[3] = forBlockArgs[3]
|
||||
const vnodeCall = firstChild.codegenNode!
|
||||
injectProp(vnodeCall, keyProperty, context)
|
||||
return vnodeCall
|
||||
} else {
|
||||
return createVNodeCall(
|
||||
context,
|
||||
helper(FRAGMENT),
|
||||
createObjectExpression([keyProperty]),
|
||||
children,
|
||||
`${PatchFlags.STABLE_FRAGMENT} /* ${
|
||||
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
|
||||
} */`,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
false,
|
||||
branch.loc
|
||||
)
|
||||
}
|
||||
return createCallExpression(helper(CREATE_BLOCK), blockArgs)
|
||||
} else {
|
||||
const childCodegen = (child as ElementNode).codegenNode as
|
||||
| ElementCodegenNode
|
||||
| ComponentCodegenNode
|
||||
| SlotOutletCodegenNode
|
||||
let vnodeCall = childCodegen
|
||||
// Element with custom directives. Locate the actual createVNode() call.
|
||||
if (vnodeCall.callee === WITH_DIRECTIVES) {
|
||||
vnodeCall = vnodeCall.arguments[0]
|
||||
}
|
||||
const vnodeCall = (firstChild as ElementNode)
|
||||
.codegenNode as BlockCodegenNode
|
||||
// Change createVNode to createBlock.
|
||||
if (vnodeCall.callee === CREATE_VNODE) {
|
||||
vnodeCall.callee = helper(CREATE_BLOCK)
|
||||
if (
|
||||
vnodeCall.type === NodeTypes.VNODE_CALL &&
|
||||
// component vnodes are always tracked and its children are
|
||||
// compiled into slots so no need to make it a block
|
||||
((firstChild as ElementNode).tagType !== ElementTypes.COMPONENT ||
|
||||
// teleport has component type but isn't always tracked
|
||||
vnodeCall.tag === TELEPORT)
|
||||
) {
|
||||
vnodeCall.isBlock = true
|
||||
helper(OPEN_BLOCK)
|
||||
helper(CREATE_BLOCK)
|
||||
}
|
||||
// inject branch key
|
||||
injectProp(vnodeCall, keyProperty, context)
|
||||
return childCodegen
|
||||
return vnodeCall
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
|
||||
const eventName = arg
|
||||
? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
|
||||
? `onUpdate:${arg.content}`
|
||||
: createCompoundExpression([
|
||||
'"onUpdate:" + ',
|
||||
...(arg.type === NodeTypes.SIMPLE_EXPRESSION ? [arg] : arg.children)
|
||||
])
|
||||
: createCompoundExpression(['"onUpdate:" + ', arg])
|
||||
: `onUpdate:modelValue`
|
||||
|
||||
const props = [
|
||||
@@ -56,11 +53,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
|
||||
// "onUpdate:modelValue": $event => (foo = $event)
|
||||
createObjectProperty(
|
||||
eventName,
|
||||
createCompoundExpression([
|
||||
`$event => (`,
|
||||
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
|
||||
` = $event)`
|
||||
])
|
||||
createCompoundExpression([`$event => (`, exp, ` = $event)`])
|
||||
)
|
||||
]
|
||||
|
||||
@@ -82,12 +75,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
|
||||
const modifiersKey = arg
|
||||
? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
|
||||
? `${arg.content}Modifiers`
|
||||
: createCompoundExpression([
|
||||
...(arg.type === NodeTypes.SIMPLE_EXPRESSION
|
||||
? [arg]
|
||||
: arg.children),
|
||||
' + "Modifiers"'
|
||||
])
|
||||
: createCompoundExpression([arg, ' + "Modifiers"'])
|
||||
: `modelModifiers`
|
||||
props.push(
|
||||
createObjectProperty(
|
||||
@@ -101,5 +89,5 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
|
||||
}
|
||||
|
||||
function createTransformProps(props: Property[] = []) {
|
||||
return { props, needRuntime: false }
|
||||
return { props }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createCompoundExpression,
|
||||
SimpleExpressionNode
|
||||
} from '../ast'
|
||||
import { capitalize } from '@vue/shared'
|
||||
import { capitalize, camelize } from '@vue/shared'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { processExpression } from './transformExpression'
|
||||
import { isMemberExpression, hasScopeRef } from '../utils'
|
||||
@@ -26,23 +26,24 @@ export interface VOnDirectiveNode extends DirectiveNode {
|
||||
}
|
||||
|
||||
export const transformOn: DirectiveTransform = (
|
||||
dir: VOnDirectiveNode,
|
||||
dir,
|
||||
node,
|
||||
context,
|
||||
augmentor
|
||||
) => {
|
||||
const { loc, modifiers, arg } = dir
|
||||
const { loc, modifiers, arg } = dir as VOnDirectiveNode
|
||||
if (!dir.exp && !modifiers.length) {
|
||||
context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc))
|
||||
}
|
||||
let eventName: ExpressionNode
|
||||
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
|
||||
if (arg.isStatic) {
|
||||
eventName = createSimpleExpression(
|
||||
`on${capitalize(arg.content)}`,
|
||||
true,
|
||||
arg.loc
|
||||
)
|
||||
const rawName = arg.content
|
||||
// for @vnode-xxx event listeners, auto convert it to camelCase
|
||||
const normalizedName = rawName.startsWith(`vnode`)
|
||||
? capitalize(camelize(rawName))
|
||||
: capitalize(rawName)
|
||||
eventName = createSimpleExpression(`on${normalizedName}`, true, arg.loc)
|
||||
} else {
|
||||
eventName = createCompoundExpression([`"on" + (`, arg, `)`])
|
||||
}
|
||||
@@ -54,16 +55,22 @@ export const transformOn: DirectiveTransform = (
|
||||
}
|
||||
|
||||
// handler processing
|
||||
let exp: ExpressionNode | undefined = dir.exp
|
||||
let exp: ExpressionNode | undefined = dir.exp as
|
||||
| SimpleExpressionNode
|
||||
| undefined
|
||||
if (exp && !exp.content.trim()) {
|
||||
exp = undefined
|
||||
}
|
||||
let isCacheable: boolean = !exp
|
||||
if (exp) {
|
||||
const isMemberExp = isMemberExpression(exp.content)
|
||||
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
|
||||
const hasMultipleStatements = exp.content.includes(`;`)
|
||||
|
||||
// process the expression since it's been skipped
|
||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||
context.addIdentifiers(`$event`)
|
||||
exp = processExpression(exp, context)
|
||||
exp = processExpression(exp, context, false, hasMultipleStatements)
|
||||
context.removeIdentifiers(`$event`)
|
||||
// with scope analysis, the function is hoistable if it has no reference
|
||||
// to scope variables.
|
||||
@@ -85,9 +92,9 @@ export const transformOn: DirectiveTransform = (
|
||||
if (isInlineStatement || (isCacheable && isMemberExp)) {
|
||||
// wrap inline statement in a function expression
|
||||
exp = createCompoundExpression([
|
||||
`$event => (`,
|
||||
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
|
||||
`)`
|
||||
`$event => ${hasMultipleStatements ? `{` : `(`}`,
|
||||
exp,
|
||||
hasMultipleStatements ? `}` : `)`
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -98,8 +105,7 @@ export const transformOn: DirectiveTransform = (
|
||||
eventName,
|
||||
exp || createSimpleExpression(`() => {}`, false, loc)
|
||||
)
|
||||
],
|
||||
needRuntime: false
|
||||
]
|
||||
}
|
||||
|
||||
// apply extended compiler augmentor
|
||||
|
||||
@@ -19,12 +19,13 @@ import {
|
||||
FunctionExpression,
|
||||
CallExpression,
|
||||
createCallExpression,
|
||||
createArrayExpression
|
||||
createArrayExpression,
|
||||
SlotsExpression
|
||||
} from '../ast'
|
||||
import { TransformContext, NodeTransform } from '../transform'
|
||||
import { createCompilerError, ErrorCodes } from '../errors'
|
||||
import { findDir, isTemplateNode, assert, isVSlot, hasScopeRef } from '../utils'
|
||||
import { CREATE_SLOTS, RENDER_LIST } from '../runtimeHelpers'
|
||||
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
|
||||
import { parseForExpression, createForLoopParams } from './vFor'
|
||||
|
||||
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
|
||||
@@ -93,19 +94,42 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
|
||||
}
|
||||
}
|
||||
|
||||
export type SlotFnBuilder = (
|
||||
slotProps: ExpressionNode | undefined,
|
||||
slotChildren: TemplateChildNode[],
|
||||
loc: SourceLocation
|
||||
) => FunctionExpression
|
||||
|
||||
const buildClientSlotFn: SlotFnBuilder = (props, children, loc) =>
|
||||
createFunctionExpression(
|
||||
props,
|
||||
children,
|
||||
false /* newline */,
|
||||
true /* isSlot */,
|
||||
children.length ? children[0].loc : loc
|
||||
)
|
||||
|
||||
// Instead of being a DirectiveTransform, v-slot processing is called during
|
||||
// transformElement to build the slots object for a component.
|
||||
export function buildSlots(
|
||||
node: ElementNode,
|
||||
context: TransformContext
|
||||
context: TransformContext,
|
||||
buildSlotFn: SlotFnBuilder = buildClientSlotFn
|
||||
): {
|
||||
slots: ObjectExpression | CallExpression
|
||||
slots: SlotsExpression
|
||||
hasDynamicSlots: boolean
|
||||
} {
|
||||
context.helper(WITH_CTX)
|
||||
|
||||
const { children, loc } = node
|
||||
const slotsProperties: Property[] = []
|
||||
const dynamicSlots: (ConditionalExpression | CallExpression)[] = []
|
||||
|
||||
const buildDefaultSlotProperty = (
|
||||
props: ExpressionNode | undefined,
|
||||
children: TemplateChildNode[]
|
||||
) => createObjectProperty(`default`, buildSlotFn(props, children, loc))
|
||||
|
||||
// If the slot is inside a v-for or another v-slot, force it to be dynamic
|
||||
// since it likely uses a scope variable.
|
||||
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0
|
||||
@@ -115,24 +139,26 @@ export function buildSlots(
|
||||
hasDynamicSlots = hasScopeRef(node, context.identifiers)
|
||||
}
|
||||
|
||||
// 1. Check for default slot with slotProps on component itself.
|
||||
// 1. Check for slot with slotProps on component itself.
|
||||
// <Comp v-slot="{ prop }"/>
|
||||
const explicitDefaultSlot = findDir(node, 'slot', true)
|
||||
if (explicitDefaultSlot) {
|
||||
const { arg, exp, loc } = explicitDefaultSlot
|
||||
if (arg) {
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT, loc)
|
||||
const onComponentSlot = findDir(node, 'slot', true)
|
||||
if (onComponentSlot) {
|
||||
const { arg, exp } = onComponentSlot
|
||||
slotsProperties.push(
|
||||
createObjectProperty(
|
||||
arg || createSimpleExpression('default', true),
|
||||
buildSlotFn(exp, children, loc)
|
||||
)
|
||||
}
|
||||
slotsProperties.push(buildDefaultSlot(exp, children, loc))
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Iterate through children and check for template slots
|
||||
// <template v-slot:foo="{ prop }">
|
||||
let hasTemplateSlots = false
|
||||
let extraneousChild: TemplateChildNode | undefined = undefined
|
||||
let hasNamedDefaultSlot = false
|
||||
const implicitDefaultChildren: TemplateChildNode[] = []
|
||||
const seenSlotNames = new Set<string>()
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const slotElement = children[i]
|
||||
let slotDir
|
||||
@@ -142,14 +168,14 @@ export function buildSlots(
|
||||
!(slotDir = findDir(slotElement, 'slot', true))
|
||||
) {
|
||||
// not a <template v-slot>, skip.
|
||||
if (slotElement.type !== NodeTypes.COMMENT && !extraneousChild) {
|
||||
extraneousChild = slotElement
|
||||
if (slotElement.type !== NodeTypes.COMMENT) {
|
||||
implicitDefaultChildren.push(slotElement)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (explicitDefaultSlot) {
|
||||
// already has on-component default slot - this is incorrect usage.
|
||||
if (onComponentSlot) {
|
||||
// already has on-component slot - this is incorrect usage.
|
||||
context.onError(
|
||||
createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc)
|
||||
)
|
||||
@@ -172,13 +198,7 @@ export function buildSlots(
|
||||
hasDynamicSlots = true
|
||||
}
|
||||
|
||||
const slotFunction = createFunctionExpression(
|
||||
slotProps,
|
||||
slotChildren,
|
||||
false,
|
||||
slotChildren.length ? slotChildren[0].loc : slotLoc
|
||||
)
|
||||
|
||||
const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc)
|
||||
// check if this slot is conditional (v-if/v-for)
|
||||
let vIf: DirectiveNode | undefined
|
||||
let vElse: DirectiveNode | undefined
|
||||
@@ -244,7 +264,7 @@ export function buildSlots(
|
||||
createFunctionExpression(
|
||||
createForLoopParams(parseResult),
|
||||
buildDynamicSlot(slotName, slotFunction),
|
||||
true
|
||||
true /* force newline */
|
||||
)
|
||||
])
|
||||
)
|
||||
@@ -266,36 +286,46 @@ export function buildSlots(
|
||||
continue
|
||||
}
|
||||
seenSlotNames.add(staticSlotName)
|
||||
if (staticSlotName === 'default') {
|
||||
hasNamedDefaultSlot = true
|
||||
}
|
||||
}
|
||||
slotsProperties.push(createObjectProperty(slotName, slotFunction))
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTemplateSlots && extraneousChild) {
|
||||
context.onError(
|
||||
createCompilerError(
|
||||
ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
|
||||
extraneousChild.loc
|
||||
)
|
||||
)
|
||||
if (!onComponentSlot) {
|
||||
if (!hasTemplateSlots) {
|
||||
// implicit default slot (on component)
|
||||
slotsProperties.push(buildDefaultSlotProperty(undefined, children))
|
||||
} else if (implicitDefaultChildren.length) {
|
||||
// implicit default slot (mixed with named slots)
|
||||
if (hasNamedDefaultSlot) {
|
||||
context.onError(
|
||||
createCompilerError(
|
||||
ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
|
||||
implicitDefaultChildren[0].loc
|
||||
)
|
||||
)
|
||||
} else {
|
||||
slotsProperties.push(
|
||||
buildDefaultSlotProperty(undefined, implicitDefaultChildren)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!explicitDefaultSlot && !hasTemplateSlots) {
|
||||
// implicit default slot.
|
||||
slotsProperties.push(buildDefaultSlot(undefined, children, loc))
|
||||
}
|
||||
|
||||
let slots: ObjectExpression | CallExpression = createObjectExpression(
|
||||
let slots = createObjectExpression(
|
||||
slotsProperties.concat(
|
||||
createObjectProperty(`_compiled`, createSimpleExpression(`true`, false))
|
||||
createObjectProperty(`_`, createSimpleExpression(`1`, false))
|
||||
),
|
||||
loc
|
||||
)
|
||||
) as SlotsExpression
|
||||
if (dynamicSlots.length) {
|
||||
slots = createCallExpression(context.helper(CREATE_SLOTS), [
|
||||
slots,
|
||||
createArrayExpression(dynamicSlots)
|
||||
])
|
||||
]) as SlotsExpression
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -304,22 +334,6 @@ export function buildSlots(
|
||||
}
|
||||
}
|
||||
|
||||
function buildDefaultSlot(
|
||||
slotProps: ExpressionNode | undefined,
|
||||
children: TemplateChildNode[],
|
||||
loc: SourceLocation
|
||||
): Property {
|
||||
return createObjectProperty(
|
||||
`default`,
|
||||
createFunctionExpression(
|
||||
slotProps,
|
||||
children,
|
||||
false,
|
||||
children.length ? children[0].loc : loc
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function buildDynamicSlot(
|
||||
name: ExpressionNode,
|
||||
fn: FunctionExpression
|
||||
|
||||
@@ -4,8 +4,6 @@ import {
|
||||
ElementNode,
|
||||
NodeTypes,
|
||||
CallExpression,
|
||||
SequenceExpression,
|
||||
createSequenceExpression,
|
||||
createCallExpression,
|
||||
DirectiveNode,
|
||||
ElementTypes,
|
||||
@@ -17,33 +15,31 @@ import {
|
||||
createObjectExpression,
|
||||
SlotOutletNode,
|
||||
TemplateNode,
|
||||
BlockCodegenNode,
|
||||
ElementCodegenNode,
|
||||
SlotOutletCodegenNode,
|
||||
ComponentCodegenNode,
|
||||
RenderSlotCall,
|
||||
ExpressionNode,
|
||||
IfBranchNode
|
||||
IfBranchNode,
|
||||
TextNode,
|
||||
InterpolationNode,
|
||||
VNodeCall
|
||||
} from './ast'
|
||||
import { parse } from 'acorn'
|
||||
import { walk } from 'estree-walker'
|
||||
import { TransformContext } from './transform'
|
||||
import {
|
||||
OPEN_BLOCK,
|
||||
MERGE_PROPS,
|
||||
RENDER_SLOT,
|
||||
PORTAL,
|
||||
TELEPORT,
|
||||
SUSPENSE,
|
||||
KEEP_ALIVE,
|
||||
BASE_TRANSITION
|
||||
} from './runtimeHelpers'
|
||||
import { isString, isFunction, isObject, hyphenate } from '@vue/shared'
|
||||
import { parse } from '@babel/parser'
|
||||
import { Node } from '@babel/types'
|
||||
|
||||
export const isBuiltInType = (tag: string, expected: string): boolean =>
|
||||
tag === expected || tag === hyphenate(expected)
|
||||
|
||||
export function isCoreComponent(tag: string): symbol | void {
|
||||
if (isBuiltInType(tag, 'Portal')) {
|
||||
return PORTAL
|
||||
if (isBuiltInType(tag, 'Teleport')) {
|
||||
return TELEPORT
|
||||
} else if (isBuiltInType(tag, 'Suspense')) {
|
||||
return SUSPENSE
|
||||
} else if (isBuiltInType(tag, 'KeepAlive')) {
|
||||
@@ -57,10 +53,10 @@ export function isCoreComponent(tag: string): symbol | void {
|
||||
// lazy require dependencies so that they don't end up in rollup's dep graph
|
||||
// and thus can be tree-shaken in browser builds.
|
||||
let _parse: typeof parse
|
||||
let _walk: typeof walk
|
||||
let _walk: any
|
||||
|
||||
export function loadDep(name: string) {
|
||||
if (typeof process !== 'undefined' && isFunction(require)) {
|
||||
if (!__BROWSER__ && typeof process !== 'undefined' && isFunction(require)) {
|
||||
return require(name)
|
||||
} else {
|
||||
// This is only used when we are building a dev-only build of the compiler
|
||||
@@ -74,11 +70,18 @@ export const parseJS: typeof parse = (code, options) => {
|
||||
!__BROWSER__,
|
||||
`Expression AST analysis can only be performed in non-browser builds.`
|
||||
)
|
||||
const parse = _parse || (_parse = loadDep('acorn').parse)
|
||||
return parse(code, options)
|
||||
if (!_parse) {
|
||||
_parse = loadDep('@babel/parser').parse
|
||||
}
|
||||
return _parse(code, options)
|
||||
}
|
||||
|
||||
export const walkJS: typeof walk = (ast, walker) => {
|
||||
interface Walker {
|
||||
enter?(node: Node, parent: Node): void
|
||||
leave?(node: Node): void
|
||||
}
|
||||
|
||||
export const walkJS = (ast: Node, walker: Walker) => {
|
||||
assert(
|
||||
!__BROWSER__,
|
||||
`Expression AST analysis can only be performed in non-browser builds.`
|
||||
@@ -149,7 +152,7 @@ export function advancePositionWithMutation(
|
||||
pos.column =
|
||||
lastNewLinePos === -1
|
||||
? pos.column + numberOfCharacters
|
||||
: Math.max(1, numberOfCharacters - lastNewLinePos)
|
||||
: numberOfCharacters - lastNewLinePos
|
||||
|
||||
return pos
|
||||
}
|
||||
@@ -181,36 +184,46 @@ export function findDir(
|
||||
export function findProp(
|
||||
node: ElementNode,
|
||||
name: string,
|
||||
dynamicOnly: boolean = false
|
||||
dynamicOnly: boolean = false,
|
||||
allowEmpty: boolean = false
|
||||
): ElementNode['props'][0] | undefined {
|
||||
for (let i = 0; i < node.props.length; i++) {
|
||||
const p = node.props[i]
|
||||
if (p.type === NodeTypes.ATTRIBUTE) {
|
||||
if (dynamicOnly) continue
|
||||
if (p.name === name && p.value) {
|
||||
if (p.name === name && (p.value || allowEmpty)) {
|
||||
return p
|
||||
}
|
||||
} else if (
|
||||
p.name === 'bind' &&
|
||||
p.arg &&
|
||||
p.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
|
||||
p.arg.isStatic &&
|
||||
p.arg.content === name &&
|
||||
p.exp
|
||||
) {
|
||||
} else if (p.name === 'bind' && p.exp && isBindKey(p.arg, name)) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlockExpression(
|
||||
blockExp: BlockCodegenNode,
|
||||
context: TransformContext
|
||||
): SequenceExpression {
|
||||
return createSequenceExpression([
|
||||
createCallExpression(context.helper(OPEN_BLOCK)),
|
||||
blockExp
|
||||
])
|
||||
export function isBindKey(arg: DirectiveNode['arg'], name: string): boolean {
|
||||
return !!(
|
||||
arg &&
|
||||
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
|
||||
arg.isStatic &&
|
||||
arg.content === name
|
||||
)
|
||||
}
|
||||
|
||||
export function hasDynamicKeyVBind(node: ElementNode): boolean {
|
||||
return node.props.some(
|
||||
p =>
|
||||
p.type === NodeTypes.DIRECTIVE &&
|
||||
p.name === 'bind' &&
|
||||
(!p.arg || // v-bind="obj"
|
||||
p.arg.type !== NodeTypes.SIMPLE_EXPRESSION || // v-bind:[_ctx.foo]
|
||||
!p.arg.isStatic) // v-bind:[foo]
|
||||
)
|
||||
}
|
||||
|
||||
export function isText(
|
||||
node: TemplateChildNode
|
||||
): node is TextNode | InterpolationNode {
|
||||
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
|
||||
}
|
||||
|
||||
export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
|
||||
@@ -232,13 +245,13 @@ export function isSlotOutlet(
|
||||
}
|
||||
|
||||
export function injectProp(
|
||||
node: ElementCodegenNode | ComponentCodegenNode | SlotOutletCodegenNode,
|
||||
node: VNodeCall | RenderSlotCall,
|
||||
prop: Property,
|
||||
context: TransformContext
|
||||
) {
|
||||
let propsWithInjection: ObjectExpression | CallExpression
|
||||
const props =
|
||||
node.callee === RENDER_SLOT ? node.arguments[2] : node.arguments[1]
|
||||
node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
|
||||
if (props == null || isString(props)) {
|
||||
propsWithInjection = createObjectExpression([prop])
|
||||
} else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
|
||||
@@ -253,7 +266,19 @@ export function injectProp(
|
||||
}
|
||||
propsWithInjection = props
|
||||
} else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
|
||||
props.properties.unshift(prop)
|
||||
let alreadyExists = false
|
||||
// check existing key to avoid overriding user provided keys
|
||||
if (prop.key.type === NodeTypes.SIMPLE_EXPRESSION) {
|
||||
const propKeyName = prop.key.content
|
||||
alreadyExists = props.properties.some(
|
||||
p =>
|
||||
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
|
||||
p.key.content === propKeyName
|
||||
)
|
||||
}
|
||||
if (!alreadyExists) {
|
||||
props.properties.unshift(prop)
|
||||
}
|
||||
propsWithInjection = props
|
||||
} else {
|
||||
// single v-bind with expression, return a merged replacement
|
||||
@@ -262,10 +287,10 @@ export function injectProp(
|
||||
props
|
||||
])
|
||||
}
|
||||
if (node.callee === RENDER_SLOT) {
|
||||
node.arguments[2] = propsWithInjection
|
||||
if (node.type === NodeTypes.VNODE_CALL) {
|
||||
node.props = propsWithInjection
|
||||
} else {
|
||||
node.arguments[1] = propsWithInjection
|
||||
node.arguments[2] = propsWithInjection
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user