transform.ts 9.6 KB

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