transform.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. import { TransformOptions } from './options'
  2. import {
  3. RootNode,
  4. NodeTypes,
  5. ParentNode,
  6. TemplateChildNode,
  7. ElementNode,
  8. DirectiveNode,
  9. Property,
  10. ExpressionNode,
  11. createSimpleExpression,
  12. JSChildNode,
  13. SimpleExpressionNode,
  14. ElementTypes,
  15. CacheExpression,
  16. createCacheExpression,
  17. TemplateLiteral,
  18. createVNodeCall
  19. } from './ast'
  20. import {
  21. isString,
  22. isArray,
  23. NOOP,
  24. PatchFlags,
  25. PatchFlagNames
  26. } from '@vue/shared'
  27. import { defaultOnError } from './errors'
  28. import {
  29. TO_DISPLAY_STRING,
  30. FRAGMENT,
  31. helperNameMap,
  32. CREATE_BLOCK,
  33. CREATE_COMMENT,
  34. OPEN_BLOCK
  35. } from './runtimeHelpers'
  36. import { isVSlot } from './utils'
  37. import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
  38. // There are two types of transforms:
  39. //
  40. // - NodeTransform:
  41. // Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
  42. // replace or remove the node being processed.
  43. export type NodeTransform = (
  44. node: RootNode | TemplateChildNode,
  45. context: TransformContext
  46. ) => void | (() => void) | (() => void)[]
  47. // - DirectiveTransform:
  48. // Transforms that handles a single directive attribute on an element.
  49. // It translates the raw directive into actual props for the VNode.
  50. export type DirectiveTransform = (
  51. dir: DirectiveNode,
  52. node: ElementNode,
  53. context: TransformContext,
  54. // a platform specific compiler can import the base transform and augment
  55. // it by passing in this optional argument.
  56. augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult
  57. ) => DirectiveTransformResult
  58. export interface DirectiveTransformResult {
  59. props: Property[]
  60. needRuntime?: boolean | symbol
  61. ssrTagParts?: TemplateLiteral['elements']
  62. }
  63. // A structural directive transform is a technically a NodeTransform;
  64. // Only v-if and v-for fall into this category.
  65. export type StructuralDirectiveTransform = (
  66. node: ElementNode,
  67. dir: DirectiveNode,
  68. context: TransformContext
  69. ) => void | (() => void)
  70. export interface ImportItem {
  71. exp: string | ExpressionNode
  72. path: string
  73. }
  74. export interface TransformContext extends Required<TransformOptions> {
  75. root: RootNode
  76. helpers: Set<symbol>
  77. components: Set<string>
  78. directives: Set<string>
  79. hoists: JSChildNode[]
  80. imports: Set<ImportItem>
  81. temps: number
  82. cached: number
  83. identifiers: { [name: string]: number | undefined }
  84. scopes: {
  85. vFor: number
  86. vSlot: number
  87. vPre: number
  88. vOnce: number
  89. }
  90. parent: ParentNode | null
  91. childIndex: number
  92. currentNode: RootNode | TemplateChildNode | null
  93. helper<T extends symbol>(name: T): T
  94. helperString(name: symbol): string
  95. replaceNode(node: TemplateChildNode): void
  96. removeNode(node?: TemplateChildNode): void
  97. onNodeRemoved(): void
  98. addIdentifiers(exp: ExpressionNode | string): void
  99. removeIdentifiers(exp: ExpressionNode | string): void
  100. hoist(exp: JSChildNode): SimpleExpressionNode
  101. cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
  102. }
  103. export function createTransformContext(
  104. root: RootNode,
  105. {
  106. prefixIdentifiers = false,
  107. hoistStatic = false,
  108. cacheHandlers = false,
  109. nodeTransforms = [],
  110. directiveTransforms = {},
  111. transformHoist = null,
  112. isBuiltInComponent = NOOP,
  113. scopeId = null,
  114. ssr = false,
  115. onError = defaultOnError
  116. }: TransformOptions
  117. ): TransformContext {
  118. const context: TransformContext = {
  119. // options
  120. prefixIdentifiers,
  121. hoistStatic,
  122. cacheHandlers,
  123. nodeTransforms,
  124. directiveTransforms,
  125. transformHoist,
  126. isBuiltInComponent,
  127. scopeId,
  128. ssr,
  129. onError,
  130. // state
  131. root,
  132. helpers: new Set(),
  133. components: new Set(),
  134. directives: new Set(),
  135. hoists: [],
  136. imports: new Set(),
  137. temps: 0,
  138. cached: 0,
  139. identifiers: {},
  140. scopes: {
  141. vFor: 0,
  142. vSlot: 0,
  143. vPre: 0,
  144. vOnce: 0
  145. },
  146. parent: null,
  147. currentNode: root,
  148. childIndex: 0,
  149. // methods
  150. helper(name) {
  151. context.helpers.add(name)
  152. return name
  153. },
  154. helperString(name) {
  155. return (
  156. (context.prefixIdentifiers ? `` : `_`) +
  157. helperNameMap[context.helper(name)]
  158. )
  159. },
  160. replaceNode(node) {
  161. /* istanbul ignore if */
  162. if (__DEV__) {
  163. if (!context.currentNode) {
  164. throw new Error(`Node being replaced is already removed.`)
  165. }
  166. if (!context.parent) {
  167. throw new Error(`Cannot replace root node.`)
  168. }
  169. }
  170. context.parent!.children[context.childIndex] = context.currentNode = node
  171. },
  172. removeNode(node) {
  173. if (__DEV__ && !context.parent) {
  174. throw new Error(`Cannot remove root node.`)
  175. }
  176. const list = context.parent!.children
  177. const removalIndex = node
  178. ? list.indexOf(node)
  179. : context.currentNode
  180. ? context.childIndex
  181. : -1
  182. /* istanbul ignore if */
  183. if (__DEV__ && removalIndex < 0) {
  184. throw new Error(`node being removed is not a child of current parent`)
  185. }
  186. if (!node || node === context.currentNode) {
  187. // current node removed
  188. context.currentNode = null
  189. context.onNodeRemoved()
  190. } else {
  191. // sibling node removed
  192. if (context.childIndex > removalIndex) {
  193. context.childIndex--
  194. context.onNodeRemoved()
  195. }
  196. }
  197. context.parent!.children.splice(removalIndex, 1)
  198. },
  199. onNodeRemoved: () => {},
  200. addIdentifiers(exp) {
  201. // identifier tracking only happens in non-browser builds.
  202. if (!__BROWSER__) {
  203. if (isString(exp)) {
  204. addId(exp)
  205. } else if (exp.identifiers) {
  206. exp.identifiers.forEach(addId)
  207. } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
  208. addId(exp.content)
  209. }
  210. }
  211. },
  212. removeIdentifiers(exp) {
  213. if (!__BROWSER__) {
  214. if (isString(exp)) {
  215. removeId(exp)
  216. } else if (exp.identifiers) {
  217. exp.identifiers.forEach(removeId)
  218. } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
  219. removeId(exp.content)
  220. }
  221. }
  222. },
  223. hoist(exp) {
  224. context.hoists.push(exp)
  225. return createSimpleExpression(
  226. `_hoisted_${context.hoists.length}`,
  227. false,
  228. exp.loc,
  229. true
  230. )
  231. },
  232. cache(exp, isVNode = false) {
  233. return createCacheExpression(++context.cached, exp, isVNode)
  234. }
  235. }
  236. function addId(id: string) {
  237. const { identifiers } = context
  238. if (identifiers[id] === undefined) {
  239. identifiers[id] = 0
  240. }
  241. identifiers[id]!++
  242. }
  243. function removeId(id: string) {
  244. context.identifiers[id]!--
  245. }
  246. return context
  247. }
  248. export function transform(root: RootNode, options: TransformOptions) {
  249. const context = createTransformContext(root, options)
  250. traverseNode(root, context)
  251. if (options.hoistStatic) {
  252. hoistStatic(root, context)
  253. }
  254. if (!options.ssr) {
  255. createRootCodegen(root, context)
  256. }
  257. // finalize meta information
  258. root.helpers = [...context.helpers]
  259. root.components = [...context.components]
  260. root.directives = [...context.directives]
  261. root.imports = [...context.imports]
  262. root.hoists = context.hoists
  263. root.temps = context.temps
  264. root.cached = context.cached
  265. }
  266. function createRootCodegen(root: RootNode, context: TransformContext) {
  267. const { helper } = context
  268. const { children } = root
  269. const child = children[0]
  270. if (children.length === 1) {
  271. // if the single child is an element, turn it into a block.
  272. if (isSingleElementRoot(root, child) && child.codegenNode) {
  273. // single element root is never hoisted so codegenNode will never be
  274. // SimpleExpressionNode
  275. const codegenNode = child.codegenNode
  276. if (codegenNode.type === NodeTypes.VNODE_CALL) {
  277. codegenNode.isBlock = true
  278. helper(OPEN_BLOCK)
  279. helper(CREATE_BLOCK)
  280. }
  281. root.codegenNode = codegenNode
  282. } else {
  283. // - single <slot/>, IfNode, ForNode: already blocks.
  284. // - single text node: always patched.
  285. // root codegen falls through via genNode()
  286. root.codegenNode = child
  287. }
  288. } else if (children.length > 1) {
  289. // root has multiple nodes - return a fragment block.
  290. root.codegenNode = createVNodeCall(
  291. context,
  292. helper(FRAGMENT),
  293. undefined,
  294. root.children,
  295. `${PatchFlags.STABLE_FRAGMENT} /* ${
  296. PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
  297. } */`,
  298. undefined,
  299. undefined,
  300. true
  301. )
  302. } else {
  303. // no children = noop. codegen will return null.
  304. }
  305. }
  306. export function traverseChildren(
  307. parent: ParentNode,
  308. context: TransformContext
  309. ) {
  310. let i = 0
  311. const nodeRemoved = () => {
  312. i--
  313. }
  314. for (; i < parent.children.length; i++) {
  315. const child = parent.children[i]
  316. if (isString(child)) continue
  317. context.parent = parent
  318. context.childIndex = i
  319. context.onNodeRemoved = nodeRemoved
  320. traverseNode(child, context)
  321. }
  322. }
  323. export function traverseNode(
  324. node: RootNode | TemplateChildNode,
  325. context: TransformContext
  326. ) {
  327. context.currentNode = node
  328. // apply transform plugins
  329. const { nodeTransforms } = context
  330. const exitFns = []
  331. for (let i = 0; i < nodeTransforms.length; i++) {
  332. const onExit = nodeTransforms[i](node, context)
  333. if (onExit) {
  334. if (isArray(onExit)) {
  335. exitFns.push(...onExit)
  336. } else {
  337. exitFns.push(onExit)
  338. }
  339. }
  340. if (!context.currentNode) {
  341. // node was removed
  342. return
  343. } else {
  344. // node may have been replaced
  345. node = context.currentNode
  346. }
  347. }
  348. switch (node.type) {
  349. case NodeTypes.COMMENT:
  350. if (!context.ssr) {
  351. // inject import for the Comment symbol, which is needed for creating
  352. // comment nodes with `createVNode`
  353. context.helper(CREATE_COMMENT)
  354. }
  355. break
  356. case NodeTypes.INTERPOLATION:
  357. // no need to traverse, but we need to inject toString helper
  358. if (!context.ssr) {
  359. context.helper(TO_DISPLAY_STRING)
  360. }
  361. break
  362. // for container types, further traverse downwards
  363. case NodeTypes.IF:
  364. for (let i = 0; i < node.branches.length; i++) {
  365. traverseNode(node.branches[i], context)
  366. }
  367. break
  368. case NodeTypes.IF_BRANCH:
  369. case NodeTypes.FOR:
  370. case NodeTypes.ELEMENT:
  371. case NodeTypes.ROOT:
  372. traverseChildren(node, context)
  373. break
  374. }
  375. // exit transforms
  376. let i = exitFns.length
  377. while (i--) {
  378. exitFns[i]()
  379. }
  380. }
  381. export function createStructuralDirectiveTransform(
  382. name: string | RegExp,
  383. fn: StructuralDirectiveTransform
  384. ): NodeTransform {
  385. const matches = isString(name)
  386. ? (n: string) => n === name
  387. : (n: string) => name.test(n)
  388. return (node, context) => {
  389. if (node.type === NodeTypes.ELEMENT) {
  390. const { props } = node
  391. // structural directive transforms are not concerned with slots
  392. // as they are handled separately in vSlot.ts
  393. if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
  394. return
  395. }
  396. const exitFns = []
  397. for (let i = 0; i < props.length; i++) {
  398. const prop = props[i]
  399. if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
  400. // structural directives are removed to avoid infinite recursion
  401. // also we remove them *before* applying so that it can further
  402. // traverse itself in case it moves the node around
  403. props.splice(i, 1)
  404. i--
  405. const onExit = fn(node, prop, context)
  406. if (onExit) exitFns.push(onExit)
  407. }
  408. }
  409. return exitFns
  410. }
  411. }
  412. }