transform.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import {
  2. RootNode,
  3. NodeTypes,
  4. ParentNode,
  5. ChildNode,
  6. ElementNode,
  7. DirectiveNode,
  8. Property,
  9. ExpressionNode
  10. } from './ast'
  11. import { isString, isArray } from '@vue/shared'
  12. import { CompilerError, defaultOnError } from './errors'
  13. import { TO_STRING, COMMENT, CREATE_VNODE } from './runtimeConstants'
  14. // There are two types of transforms:
  15. //
  16. // - NodeTransform:
  17. // Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
  18. // replace or remove the node being processed.
  19. export type NodeTransform = (
  20. node: ChildNode,
  21. context: TransformContext
  22. ) => void | (() => void) | (() => void)[]
  23. // - DirectiveTransform:
  24. // Transforms that handles a single directive attribute on an element.
  25. // It translates the raw directive into actual props for the VNode.
  26. export type DirectiveTransform = (
  27. dir: DirectiveNode,
  28. context: TransformContext
  29. ) => {
  30. props: Property | Property[]
  31. needRuntime: boolean
  32. }
  33. // A structural directive transform is a techically a NodeTransform;
  34. // Only v-if and v-for fall into this category.
  35. export type StructuralDirectiveTransform = (
  36. node: ElementNode,
  37. dir: DirectiveNode,
  38. context: TransformContext
  39. ) => void | (() => void)
  40. export interface TransformOptions {
  41. nodeTransforms?: NodeTransform[]
  42. directiveTransforms?: { [name: string]: DirectiveTransform }
  43. prefixIdentifiers?: boolean
  44. onError?: (error: CompilerError) => void
  45. }
  46. export interface TransformContext extends Required<TransformOptions> {
  47. root: RootNode
  48. imports: Set<string>
  49. statements: string[]
  50. identifiers: { [name: string]: number | undefined }
  51. parent: ParentNode
  52. childIndex: number
  53. currentNode: ChildNode | null
  54. replaceNode(node: ChildNode): void
  55. removeNode(node?: ChildNode): void
  56. onNodeRemoved: () => void
  57. addIdentifier(exp: ExpressionNode): void
  58. removeIdentifier(exp: ExpressionNode): void
  59. }
  60. function createTransformContext(
  61. root: RootNode,
  62. {
  63. prefixIdentifiers = false,
  64. nodeTransforms = [],
  65. directiveTransforms = {},
  66. onError = defaultOnError
  67. }: TransformOptions
  68. ): TransformContext {
  69. const context: TransformContext = {
  70. root,
  71. imports: new Set(),
  72. statements: [],
  73. identifiers: {},
  74. prefixIdentifiers,
  75. nodeTransforms,
  76. directiveTransforms,
  77. onError,
  78. parent: root,
  79. childIndex: 0,
  80. currentNode: null,
  81. replaceNode(node) {
  82. /* istanbul ignore if */
  83. if (__DEV__ && !context.currentNode) {
  84. throw new Error(`node being replaced is already removed.`)
  85. }
  86. context.parent.children[context.childIndex] = context.currentNode = node
  87. },
  88. removeNode(node) {
  89. const list = context.parent.children
  90. const removalIndex = node
  91. ? list.indexOf(node as any)
  92. : context.currentNode
  93. ? context.childIndex
  94. : -1
  95. /* istanbul ignore if */
  96. if (__DEV__ && removalIndex < 0) {
  97. throw new Error(`node being removed is not a child of current parent`)
  98. }
  99. if (!node || node === context.currentNode) {
  100. // current node removed
  101. context.currentNode = null
  102. context.onNodeRemoved()
  103. } else {
  104. // sibling node removed
  105. if (context.childIndex > removalIndex) {
  106. context.childIndex--
  107. context.onNodeRemoved()
  108. }
  109. }
  110. context.parent.children.splice(removalIndex, 1)
  111. },
  112. onNodeRemoved: () => {},
  113. addIdentifier({ content }) {
  114. const { identifiers } = context
  115. if (identifiers[content] === undefined) {
  116. identifiers[content] = 0
  117. }
  118. ;(identifiers[content] as number)++
  119. },
  120. removeIdentifier({ content }) {
  121. ;(context.identifiers[content] as number)--
  122. }
  123. }
  124. return context
  125. }
  126. export function transform(root: RootNode, options: TransformOptions) {
  127. const context = createTransformContext(root, options)
  128. traverseChildren(root, context)
  129. root.imports = [...context.imports]
  130. root.statements = context.statements
  131. }
  132. export function traverseChildren(
  133. parent: ParentNode,
  134. context: TransformContext
  135. ) {
  136. let i = 0
  137. const nodeRemoved = () => {
  138. i--
  139. }
  140. for (; i < parent.children.length; i++) {
  141. const child = parent.children[i]
  142. if (isString(child)) continue
  143. context.currentNode = child
  144. context.parent = parent
  145. context.childIndex = i
  146. context.onNodeRemoved = nodeRemoved
  147. traverseNode(child, context)
  148. }
  149. }
  150. export function traverseNode(node: ChildNode, context: TransformContext) {
  151. // apply transform plugins
  152. const { nodeTransforms } = context
  153. const exitFns = []
  154. for (let i = 0; i < nodeTransforms.length; i++) {
  155. const plugin = nodeTransforms[i]
  156. const onExit = plugin(node, context)
  157. if (onExit) {
  158. if (isArray(onExit)) {
  159. exitFns.push(...onExit)
  160. } else {
  161. exitFns.push(onExit)
  162. }
  163. }
  164. if (!context.currentNode) {
  165. // node was removed
  166. return
  167. } else {
  168. // node may have been replaced
  169. node = context.currentNode
  170. }
  171. }
  172. switch (node.type) {
  173. case NodeTypes.COMMENT:
  174. context.imports.add(CREATE_VNODE)
  175. // inject import for the Comment symbol, which is needed for creating
  176. // comment nodes with `createVNode`
  177. context.imports.add(COMMENT)
  178. break
  179. case NodeTypes.EXPRESSION:
  180. // no need to traverse, but we need to inject toString helper
  181. if (node.isInterpolation) {
  182. context.imports.add(TO_STRING)
  183. }
  184. break
  185. // for container types, further traverse downwards
  186. case NodeTypes.IF:
  187. for (let i = 0; i < node.branches.length; i++) {
  188. traverseChildren(node.branches[i], context)
  189. }
  190. break
  191. case NodeTypes.FOR:
  192. case NodeTypes.ELEMENT:
  193. traverseChildren(node, context)
  194. break
  195. }
  196. // exit transforms
  197. for (let i = 0; i < exitFns.length; i++) {
  198. exitFns[i]()
  199. }
  200. }
  201. export function createStructuralDirectiveTransform(
  202. name: string | RegExp,
  203. fn: StructuralDirectiveTransform
  204. ): NodeTransform {
  205. const matches = isString(name)
  206. ? (n: string) => n === name
  207. : (n: string) => name.test(n)
  208. return (node, context) => {
  209. if (node.type === NodeTypes.ELEMENT) {
  210. const { props } = node
  211. const exitFns = []
  212. for (let i = 0; i < props.length; i++) {
  213. const prop = props[i]
  214. if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
  215. // structural directives are removed to avoid infinite recursion
  216. // also we remove them *before* applying so that it can further
  217. // traverse itself in case it moves the node around
  218. props.splice(i, 1)
  219. i--
  220. const onExit = fn(node, prop, context)
  221. if (onExit) exitFns.push(onExit)
  222. }
  223. }
  224. return exitFns
  225. }
  226. }
  227. }