transform.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import {
  2. RootNode,
  3. NodeTypes,
  4. ParentNode,
  5. TemplateChildNode,
  6. ElementNode,
  7. DirectiveNode,
  8. Property,
  9. ExpressionNode,
  10. createSimpleExpression,
  11. JSChildNode,
  12. SimpleExpressionNode,
  13. ElementTypes
  14. } from './ast'
  15. import { isString, isArray } from '@vue/shared'
  16. import { CompilerError, defaultOnError } from './errors'
  17. import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
  18. import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
  19. // There are two types of transforms:
  20. //
  21. // - NodeTransform:
  22. // Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
  23. // replace or remove the node being processed.
  24. export type NodeTransform = (
  25. node: RootNode | TemplateChildNode,
  26. context: TransformContext
  27. ) => void | (() => void) | (() => void)[]
  28. // - DirectiveTransform:
  29. // Transforms that handles a single directive attribute on an element.
  30. // It translates the raw directive into actual props for the VNode.
  31. export type DirectiveTransform = (
  32. dir: DirectiveNode,
  33. context: TransformContext
  34. ) => {
  35. props: Property | Property[]
  36. needRuntime: boolean
  37. }
  38. // A structural directive transform is a techically a NodeTransform;
  39. // Only v-if and v-for fall into this category.
  40. export type StructuralDirectiveTransform = (
  41. node: ElementNode,
  42. dir: DirectiveNode,
  43. context: TransformContext
  44. ) => void | (() => void)
  45. export interface TransformOptions {
  46. nodeTransforms?: NodeTransform[]
  47. directiveTransforms?: { [name: string]: DirectiveTransform }
  48. prefixIdentifiers?: boolean
  49. onError?: (error: CompilerError) => void
  50. }
  51. export interface TransformContext extends Required<TransformOptions> {
  52. root: RootNode
  53. imports: Set<string>
  54. statements: Set<string>
  55. hoists: JSChildNode[]
  56. identifiers: { [name: string]: number | undefined }
  57. scopes: {
  58. vFor: number
  59. vSlot: number
  60. vPre: number
  61. vOnce: number
  62. }
  63. parent: ParentNode | null
  64. childIndex: number
  65. currentNode: RootNode | TemplateChildNode | null
  66. helper(name: string): string
  67. replaceNode(node: TemplateChildNode): void
  68. removeNode(node?: TemplateChildNode): void
  69. onNodeRemoved: () => void
  70. addIdentifiers(exp: ExpressionNode): void
  71. removeIdentifiers(exp: ExpressionNode): void
  72. hoist(exp: JSChildNode): SimpleExpressionNode
  73. }
  74. function createTransformContext(
  75. root: RootNode,
  76. {
  77. prefixIdentifiers = false,
  78. nodeTransforms = [],
  79. directiveTransforms = {},
  80. onError = defaultOnError
  81. }: TransformOptions
  82. ): TransformContext {
  83. const context: TransformContext = {
  84. root,
  85. imports: new Set(),
  86. statements: new Set(),
  87. hoists: [],
  88. identifiers: {},
  89. scopes: {
  90. vFor: 0,
  91. vSlot: 0,
  92. vPre: 0,
  93. vOnce: 0
  94. },
  95. prefixIdentifiers,
  96. nodeTransforms,
  97. directiveTransforms,
  98. onError,
  99. parent: null,
  100. currentNode: root,
  101. childIndex: 0,
  102. helper(name) {
  103. context.imports.add(name)
  104. return prefixIdentifiers ? name : `_${name}`
  105. },
  106. replaceNode(node) {
  107. /* istanbul ignore if */
  108. if (__DEV__) {
  109. if (!context.currentNode) {
  110. throw new Error(`Node being replaced is already removed.`)
  111. }
  112. if (!context.parent) {
  113. throw new Error(`Cannot replace root node.`)
  114. }
  115. }
  116. context.parent!.children[context.childIndex] = context.currentNode = node
  117. },
  118. removeNode(node) {
  119. if (__DEV__ && !context.parent) {
  120. throw new Error(`Cannot remove root node.`)
  121. }
  122. const list = context.parent!.children
  123. const removalIndex = node
  124. ? list.indexOf(node as any)
  125. : context.currentNode
  126. ? context.childIndex
  127. : -1
  128. /* istanbul ignore if */
  129. if (__DEV__ && removalIndex < 0) {
  130. throw new Error(`node being removed is not a child of current parent`)
  131. }
  132. if (!node || node === context.currentNode) {
  133. // current node removed
  134. context.currentNode = null
  135. context.onNodeRemoved()
  136. } else {
  137. // sibling node removed
  138. if (context.childIndex > removalIndex) {
  139. context.childIndex--
  140. context.onNodeRemoved()
  141. }
  142. }
  143. context.parent!.children.splice(removalIndex, 1)
  144. },
  145. onNodeRemoved: () => {},
  146. addIdentifiers(exp) {
  147. // identifier tracking only happens in non-browser builds.
  148. if (!__BROWSER__) {
  149. if (exp.identifiers) {
  150. exp.identifiers.forEach(addId)
  151. } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
  152. addId(exp.content)
  153. }
  154. }
  155. },
  156. removeIdentifiers(exp) {
  157. if (!__BROWSER__) {
  158. if (exp.identifiers) {
  159. exp.identifiers.forEach(removeId)
  160. } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
  161. removeId(exp.content)
  162. }
  163. }
  164. },
  165. hoist(exp) {
  166. context.hoists.push(exp)
  167. return createSimpleExpression(
  168. `_hoisted_${context.hoists.length}`,
  169. false,
  170. exp.loc
  171. )
  172. }
  173. }
  174. function addId(id: string) {
  175. const { identifiers } = context
  176. if (identifiers[id] === undefined) {
  177. identifiers[id] = 0
  178. }
  179. ;(identifiers[id] as number)++
  180. }
  181. function removeId(id: string) {
  182. ;(context.identifiers[id] as number)--
  183. }
  184. return context
  185. }
  186. export function transform(root: RootNode, options: TransformOptions) {
  187. const context = createTransformContext(root, options)
  188. traverseNode(root, context)
  189. finalizeRoot(root, context)
  190. }
  191. function finalizeRoot(root: RootNode, context: TransformContext) {
  192. const { helper } = context
  193. const { children } = root
  194. if (children.length === 1) {
  195. const child = children[0]
  196. if (
  197. child.type === NodeTypes.ELEMENT &&
  198. !isSlotOutlet(child) &&
  199. child.codegenNode
  200. ) {
  201. // turn root element into a block
  202. root.codegenNode = createBlockExpression(
  203. child.codegenNode!.arguments,
  204. context
  205. )
  206. } else {
  207. // - single <slot/>, IfNode, ForNode: already blocks.
  208. // - single text node: always patched.
  209. // - transform calls without transformElement (only during tests)
  210. // Just generate the node as-is
  211. root.codegenNode = child
  212. }
  213. } else if (children.length > 1) {
  214. // root has multiple nodes - return a fragment block.
  215. root.codegenNode = createBlockExpression(
  216. [helper(FRAGMENT), `null`, root.children],
  217. context
  218. )
  219. }
  220. // finalize meta information
  221. root.imports = [...context.imports]
  222. root.statements = [...context.statements]
  223. root.hoists = context.hoists
  224. }
  225. export function traverseChildren(
  226. parent: ParentNode,
  227. context: TransformContext
  228. ) {
  229. let i = 0
  230. const nodeRemoved = () => {
  231. i--
  232. }
  233. for (; i < parent.children.length; i++) {
  234. const child = parent.children[i]
  235. if (isString(child)) continue
  236. context.currentNode = child
  237. context.parent = parent
  238. context.childIndex = i
  239. context.onNodeRemoved = nodeRemoved
  240. traverseNode(child, context)
  241. }
  242. }
  243. export function traverseNode(
  244. node: RootNode | TemplateChildNode,
  245. context: TransformContext
  246. ) {
  247. // apply transform plugins
  248. const { nodeTransforms } = context
  249. const exitFns = []
  250. for (let i = 0; i < nodeTransforms.length; i++) {
  251. const onExit = nodeTransforms[i](node, context)
  252. if (onExit) {
  253. if (isArray(onExit)) {
  254. exitFns.push(...onExit)
  255. } else {
  256. exitFns.push(onExit)
  257. }
  258. }
  259. if (!context.currentNode) {
  260. // node was removed
  261. return
  262. } else {
  263. // node may have been replaced
  264. node = context.currentNode
  265. }
  266. }
  267. switch (node.type) {
  268. case NodeTypes.COMMENT:
  269. // inject import for the Comment symbol, which is needed for creating
  270. // comment nodes with `createVNode`
  271. context.helper(CREATE_VNODE)
  272. context.helper(COMMENT)
  273. break
  274. case NodeTypes.INTERPOLATION:
  275. // no need to traverse, but we need to inject toString helper
  276. context.helper(TO_STRING)
  277. break
  278. // for container types, further traverse downwards
  279. case NodeTypes.IF:
  280. for (let i = 0; i < node.branches.length; i++) {
  281. traverseChildren(node.branches[i], context)
  282. }
  283. break
  284. case NodeTypes.FOR:
  285. case NodeTypes.ELEMENT:
  286. case NodeTypes.ROOT:
  287. traverseChildren(node, context)
  288. break
  289. }
  290. // exit transforms
  291. for (let i = 0; i < exitFns.length; i++) {
  292. exitFns[i]()
  293. }
  294. }
  295. export function createStructuralDirectiveTransform(
  296. name: string | RegExp,
  297. fn: StructuralDirectiveTransform
  298. ): NodeTransform {
  299. const matches = isString(name)
  300. ? (n: string) => n === name
  301. : (n: string) => name.test(n)
  302. return (node, context) => {
  303. if (node.type === NodeTypes.ELEMENT) {
  304. const { props } = node
  305. // structural directive transforms are not concerned with slots
  306. // as they are handled separately in vSlot.ts
  307. if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
  308. return
  309. }
  310. const exitFns = []
  311. for (let i = 0; i < props.length; i++) {
  312. const prop = props[i]
  313. if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
  314. // structural directives are removed to avoid infinite recursion
  315. // also we remove them *before* applying so that it can further
  316. // traverse itself in case it moves the node around
  317. props.splice(i, 1)
  318. i--
  319. const onExit = fn(node, prop, context)
  320. if (onExit) exitFns.push(onExit)
  321. }
  322. }
  323. return exitFns
  324. }
  325. }
  326. }