| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- import { TransformOptions } from './options'
- import {
- RootNode,
- NodeTypes,
- ParentNode,
- TemplateChildNode,
- ElementNode,
- DirectiveNode,
- Property,
- ExpressionNode,
- createSimpleExpression,
- JSChildNode,
- SimpleExpressionNode,
- ElementTypes,
- CacheExpression,
- createCacheExpression,
- TemplateLiteral,
- createVNodeCall
- } from './ast'
- import {
- isString,
- isArray,
- NOOP,
- PatchFlags,
- PatchFlagNames
- } from '@vue/shared'
- import { defaultOnError } from './errors'
- import {
- TO_DISPLAY_STRING,
- FRAGMENT,
- helperNameMap,
- CREATE_BLOCK,
- CREATE_COMMENT,
- OPEN_BLOCK
- } from './runtimeHelpers'
- import { isVSlot } from './utils'
- import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
- // There are two types of transforms:
- //
- // - NodeTransform:
- // Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
- // replace or remove the node being processed.
- export type NodeTransform = (
- node: RootNode | TemplateChildNode,
- context: TransformContext
- ) => void | (() => void) | (() => void)[]
- // - DirectiveTransform:
- // Transforms that handles a single directive attribute on an element.
- // It translates the raw directive into actual props for the VNode.
- export type DirectiveTransform = (
- dir: DirectiveNode,
- node: ElementNode,
- context: TransformContext,
- // a platform specific compiler can import the base transform and augment
- // it by passing in this optional argument.
- augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult
- ) => DirectiveTransformResult
- export interface DirectiveTransformResult {
- props: Property[]
- needRuntime?: boolean | symbol
- ssrTagParts?: TemplateLiteral['elements']
- }
- // A structural directive transform is a technically a NodeTransform;
- // Only v-if and v-for fall into this category.
- export type StructuralDirectiveTransform = (
- node: ElementNode,
- dir: DirectiveNode,
- context: TransformContext
- ) => void | (() => void)
- export interface ImportItem {
- exp: string | ExpressionNode
- path: string
- }
- export interface TransformContext extends Required<TransformOptions> {
- root: RootNode
- helpers: Set<symbol>
- components: Set<string>
- directives: Set<string>
- hoists: (JSChildNode | null)[]
- imports: Set<ImportItem>
- temps: number
- cached: number
- identifiers: { [name: string]: number | undefined }
- scopes: {
- vFor: number
- vSlot: number
- vPre: number
- vOnce: number
- }
- parent: ParentNode | null
- childIndex: number
- currentNode: RootNode | TemplateChildNode | null
- helper<T extends symbol>(name: T): T
- helperString(name: symbol): string
- replaceNode(node: TemplateChildNode): void
- removeNode(node?: TemplateChildNode): void
- onNodeRemoved(): void
- addIdentifiers(exp: ExpressionNode | string): void
- removeIdentifiers(exp: ExpressionNode | string): void
- hoist(exp: JSChildNode): SimpleExpressionNode
- cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
- }
- export function createTransformContext(
- root: RootNode,
- {
- prefixIdentifiers = false,
- hoistStatic = false,
- cacheHandlers = false,
- nodeTransforms = [],
- directiveTransforms = {},
- transformHoist = null,
- isBuiltInComponent = NOOP,
- expressionPlugins = [],
- scopeId = null,
- ssr = false,
- ssrCssVars = ``,
- bindingMetadata = {},
- onError = defaultOnError
- }: TransformOptions
- ): TransformContext {
- const context: TransformContext = {
- // options
- prefixIdentifiers,
- hoistStatic,
- cacheHandlers,
- nodeTransforms,
- directiveTransforms,
- transformHoist,
- isBuiltInComponent,
- expressionPlugins,
- scopeId,
- ssr,
- ssrCssVars,
- bindingMetadata,
- onError,
- // state
- root,
- helpers: new Set(),
- components: new Set(),
- directives: new Set(),
- hoists: [],
- imports: new Set(),
- temps: 0,
- cached: 0,
- identifiers: Object.create(null),
- scopes: {
- vFor: 0,
- vSlot: 0,
- vPre: 0,
- vOnce: 0
- },
- parent: null,
- currentNode: root,
- childIndex: 0,
- // methods
- helper(name) {
- context.helpers.add(name)
- return name
- },
- helperString(name) {
- return `_${helperNameMap[context.helper(name)]}`
- },
- replaceNode(node) {
- /* istanbul ignore if */
- if (__DEV__) {
- if (!context.currentNode) {
- throw new Error(`Node being replaced is already removed.`)
- }
- if (!context.parent) {
- throw new Error(`Cannot replace root node.`)
- }
- }
- context.parent!.children[context.childIndex] = context.currentNode = node
- },
- removeNode(node) {
- if (__DEV__ && !context.parent) {
- throw new Error(`Cannot remove root node.`)
- }
- const list = context.parent!.children
- const removalIndex = node
- ? list.indexOf(node)
- : context.currentNode
- ? context.childIndex
- : -1
- /* istanbul ignore if */
- if (__DEV__ && removalIndex < 0) {
- throw new Error(`node being removed is not a child of current parent`)
- }
- if (!node || node === context.currentNode) {
- // current node removed
- context.currentNode = null
- context.onNodeRemoved()
- } else {
- // sibling node removed
- if (context.childIndex > removalIndex) {
- context.childIndex--
- context.onNodeRemoved()
- }
- }
- context.parent!.children.splice(removalIndex, 1)
- },
- onNodeRemoved: () => {},
- addIdentifiers(exp) {
- // identifier tracking only happens in non-browser builds.
- if (!__BROWSER__) {
- if (isString(exp)) {
- addId(exp)
- } else if (exp.identifiers) {
- exp.identifiers.forEach(addId)
- } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
- addId(exp.content)
- }
- }
- },
- removeIdentifiers(exp) {
- if (!__BROWSER__) {
- if (isString(exp)) {
- removeId(exp)
- } else if (exp.identifiers) {
- exp.identifiers.forEach(removeId)
- } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
- removeId(exp.content)
- }
- }
- },
- hoist(exp) {
- context.hoists.push(exp)
- const identifier = createSimpleExpression(
- `_hoisted_${context.hoists.length}`,
- false,
- exp.loc,
- true
- )
- identifier.hoisted = exp
- return identifier
- },
- cache(exp, isVNode = false) {
- return createCacheExpression(++context.cached, exp, isVNode)
- }
- }
- function addId(id: string) {
- const { identifiers } = context
- if (identifiers[id] === undefined) {
- identifiers[id] = 0
- }
- identifiers[id]!++
- }
- function removeId(id: string) {
- context.identifiers[id]!--
- }
- return context
- }
- export function transform(root: RootNode, options: TransformOptions) {
- const context = createTransformContext(root, options)
- traverseNode(root, context)
- if (options.hoistStatic) {
- hoistStatic(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 createRootCodegen(root: RootNode, context: TransformContext) {
- const { helper } = context
- const { children } = root
- const child = children[0]
- if (children.length === 1) {
- // if the single child is an element, turn it into a block.
- if (isSingleElementRoot(root, child) && child.codegenNode) {
- // single element root is never hoisted so codegenNode will never be
- // SimpleExpressionNode
- 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.
- // root codegen falls through via genNode()
- root.codegenNode = child
- }
- } else if (children.length > 1) {
- // root has multiple nodes - return a fragment block.
- 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.
- }
- }
- export function traverseChildren(
- parent: ParentNode,
- context: TransformContext
- ) {
- let i = 0
- const nodeRemoved = () => {
- i--
- }
- for (; i < parent.children.length; i++) {
- const child = parent.children[i]
- if (isString(child)) continue
- context.parent = parent
- context.childIndex = i
- context.onNodeRemoved = nodeRemoved
- traverseNode(child, context)
- }
- }
- export function traverseNode(
- node: RootNode | TemplateChildNode,
- context: TransformContext
- ) {
- context.currentNode = node
- // apply transform plugins
- const { nodeTransforms } = context
- const exitFns = []
- for (let i = 0; i < nodeTransforms.length; i++) {
- const onExit = nodeTransforms[i](node, context)
- if (onExit) {
- if (isArray(onExit)) {
- exitFns.push(...onExit)
- } else {
- exitFns.push(onExit)
- }
- }
- if (!context.currentNode) {
- // node was removed
- return
- } else {
- // node may have been replaced
- node = context.currentNode
- }
- }
- switch (node.type) {
- case NodeTypes.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
- 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++) {
- traverseNode(node.branches[i], context)
- }
- break
- case NodeTypes.IF_BRANCH:
- case NodeTypes.FOR:
- case NodeTypes.ELEMENT:
- case NodeTypes.ROOT:
- traverseChildren(node, context)
- break
- }
- // exit transforms
- let i = exitFns.length
- while (i--) {
- exitFns[i]()
- }
- }
- export function createStructuralDirectiveTransform(
- name: string | RegExp,
- fn: StructuralDirectiveTransform
- ): NodeTransform {
- const matches = isString(name)
- ? (n: string) => n === name
- : (n: string) => name.test(n)
- return (node, context) => {
- if (node.type === NodeTypes.ELEMENT) {
- const { props } = node
- // structural directive transforms are not concerned with slots
- // as they are handled separately in vSlot.ts
- if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
- return
- }
- const exitFns = []
- for (let i = 0; i < props.length; i++) {
- const prop = props[i]
- if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
- // structural directives are removed to avoid infinite recursion
- // also we remove them *before* applying so that it can further
- // traverse itself in case it moves the node around
- props.splice(i, 1)
- i--
- const onExit = fn(node, prop, context)
- if (onExit) exitFns.push(onExit)
- }
- }
- return exitFns
- }
- }
- }
|