transform.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  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 | null)[]
  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. expressionPlugins = [],
  114. scopeId = null,
  115. ssr = false,
  116. ssrCssVars = ``,
  117. bindingMetadata = {},
  118. onError = defaultOnError
  119. }: TransformOptions
  120. ): TransformContext {
  121. const context: TransformContext = {
  122. // options
  123. prefixIdentifiers,
  124. hoistStatic,
  125. cacheHandlers,
  126. nodeTransforms,
  127. directiveTransforms,
  128. transformHoist,
  129. isBuiltInComponent,
  130. expressionPlugins,
  131. scopeId,
  132. ssr,
  133. ssrCssVars,
  134. bindingMetadata,
  135. onError,
  136. // state
  137. root,
  138. helpers: new Set(),
  139. components: new Set(),
  140. directives: new Set(),
  141. hoists: [],
  142. imports: new Set(),
  143. temps: 0,
  144. cached: 0,
  145. identifiers: Object.create(null),
  146. scopes: {
  147. vFor: 0,
  148. vSlot: 0,
  149. vPre: 0,
  150. vOnce: 0
  151. },
  152. parent: null,
  153. currentNode: root,
  154. childIndex: 0,
  155. // methods
  156. helper(name) {
  157. context.helpers.add(name)
  158. return name
  159. },
  160. helperString(name) {
  161. return `_${helperNameMap[context.helper(name)]}`
  162. },
  163. replaceNode(node) {
  164. /* istanbul ignore if */
  165. if (__DEV__) {
  166. if (!context.currentNode) {
  167. throw new Error(`Node being replaced is already removed.`)
  168. }
  169. if (!context.parent) {
  170. throw new Error(`Cannot replace root node.`)
  171. }
  172. }
  173. context.parent!.children[context.childIndex] = context.currentNode = node
  174. },
  175. removeNode(node) {
  176. if (__DEV__ && !context.parent) {
  177. throw new Error(`Cannot remove root node.`)
  178. }
  179. const list = context.parent!.children
  180. const removalIndex = node
  181. ? list.indexOf(node)
  182. : context.currentNode
  183. ? context.childIndex
  184. : -1
  185. /* istanbul ignore if */
  186. if (__DEV__ && removalIndex < 0) {
  187. throw new Error(`node being removed is not a child of current parent`)
  188. }
  189. if (!node || node === context.currentNode) {
  190. // current node removed
  191. context.currentNode = null
  192. context.onNodeRemoved()
  193. } else {
  194. // sibling node removed
  195. if (context.childIndex > removalIndex) {
  196. context.childIndex--
  197. context.onNodeRemoved()
  198. }
  199. }
  200. context.parent!.children.splice(removalIndex, 1)
  201. },
  202. onNodeRemoved: () => {},
  203. addIdentifiers(exp) {
  204. // identifier tracking only happens in non-browser builds.
  205. if (!__BROWSER__) {
  206. if (isString(exp)) {
  207. addId(exp)
  208. } else if (exp.identifiers) {
  209. exp.identifiers.forEach(addId)
  210. } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
  211. addId(exp.content)
  212. }
  213. }
  214. },
  215. removeIdentifiers(exp) {
  216. if (!__BROWSER__) {
  217. if (isString(exp)) {
  218. removeId(exp)
  219. } else if (exp.identifiers) {
  220. exp.identifiers.forEach(removeId)
  221. } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
  222. removeId(exp.content)
  223. }
  224. }
  225. },
  226. hoist(exp) {
  227. context.hoists.push(exp)
  228. const identifier = createSimpleExpression(
  229. `_hoisted_${context.hoists.length}`,
  230. false,
  231. exp.loc,
  232. true
  233. )
  234. identifier.hoisted = exp
  235. return identifier
  236. },
  237. cache(exp, isVNode = false) {
  238. return createCacheExpression(++context.cached, exp, isVNode)
  239. }
  240. }
  241. function addId(id: string) {
  242. const { identifiers } = context
  243. if (identifiers[id] === undefined) {
  244. identifiers[id] = 0
  245. }
  246. identifiers[id]!++
  247. }
  248. function removeId(id: string) {
  249. context.identifiers[id]!--
  250. }
  251. return context
  252. }
  253. export function transform(root: RootNode, options: TransformOptions) {
  254. const context = createTransformContext(root, options)
  255. traverseNode(root, context)
  256. if (options.hoistStatic) {
  257. hoistStatic(root, context)
  258. }
  259. if (!options.ssr) {
  260. createRootCodegen(root, context)
  261. }
  262. // finalize meta information
  263. root.helpers = [...context.helpers]
  264. root.components = [...context.components]
  265. root.directives = [...context.directives]
  266. root.imports = [...context.imports]
  267. root.hoists = context.hoists
  268. root.temps = context.temps
  269. root.cached = context.cached
  270. }
  271. function createRootCodegen(root: RootNode, context: TransformContext) {
  272. const { helper } = context
  273. const { children } = root
  274. const child = children[0]
  275. if (children.length === 1) {
  276. // if the single child is an element, turn it into a block.
  277. if (isSingleElementRoot(root, child) && child.codegenNode) {
  278. // single element root is never hoisted so codegenNode will never be
  279. // SimpleExpressionNode
  280. const codegenNode = child.codegenNode
  281. if (codegenNode.type === NodeTypes.VNODE_CALL) {
  282. codegenNode.isBlock = true
  283. helper(OPEN_BLOCK)
  284. helper(CREATE_BLOCK)
  285. }
  286. root.codegenNode = codegenNode
  287. } else {
  288. // - single <slot/>, IfNode, ForNode: already blocks.
  289. // - single text node: always patched.
  290. // root codegen falls through via genNode()
  291. root.codegenNode = child
  292. }
  293. } else if (children.length > 1) {
  294. // root has multiple nodes - return a fragment block.
  295. root.codegenNode = createVNodeCall(
  296. context,
  297. helper(FRAGMENT),
  298. undefined,
  299. root.children,
  300. `${PatchFlags.STABLE_FRAGMENT} /* ${
  301. PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
  302. } */`,
  303. undefined,
  304. undefined,
  305. true
  306. )
  307. } else {
  308. // no children = noop. codegen will return null.
  309. }
  310. }
  311. export function traverseChildren(
  312. parent: ParentNode,
  313. context: TransformContext
  314. ) {
  315. let i = 0
  316. const nodeRemoved = () => {
  317. i--
  318. }
  319. for (; i < parent.children.length; i++) {
  320. const child = parent.children[i]
  321. if (isString(child)) continue
  322. context.parent = parent
  323. context.childIndex = i
  324. context.onNodeRemoved = nodeRemoved
  325. traverseNode(child, context)
  326. }
  327. }
  328. export function traverseNode(
  329. node: RootNode | TemplateChildNode,
  330. context: TransformContext
  331. ) {
  332. context.currentNode = node
  333. // apply transform plugins
  334. const { nodeTransforms } = context
  335. const exitFns = []
  336. for (let i = 0; i < nodeTransforms.length; i++) {
  337. const onExit = nodeTransforms[i](node, context)
  338. if (onExit) {
  339. if (isArray(onExit)) {
  340. exitFns.push(...onExit)
  341. } else {
  342. exitFns.push(onExit)
  343. }
  344. }
  345. if (!context.currentNode) {
  346. // node was removed
  347. return
  348. } else {
  349. // node may have been replaced
  350. node = context.currentNode
  351. }
  352. }
  353. switch (node.type) {
  354. case NodeTypes.COMMENT:
  355. if (!context.ssr) {
  356. // inject import for the Comment symbol, which is needed for creating
  357. // comment nodes with `createVNode`
  358. context.helper(CREATE_COMMENT)
  359. }
  360. break
  361. case NodeTypes.INTERPOLATION:
  362. // no need to traverse, but we need to inject toString helper
  363. if (!context.ssr) {
  364. context.helper(TO_DISPLAY_STRING)
  365. }
  366. break
  367. // for container types, further traverse downwards
  368. case NodeTypes.IF:
  369. for (let i = 0; i < node.branches.length; i++) {
  370. traverseNode(node.branches[i], context)
  371. }
  372. break
  373. case NodeTypes.IF_BRANCH:
  374. case NodeTypes.FOR:
  375. case NodeTypes.ELEMENT:
  376. case NodeTypes.ROOT:
  377. traverseChildren(node, context)
  378. break
  379. }
  380. // exit transforms
  381. let i = exitFns.length
  382. while (i--) {
  383. exitFns[i]()
  384. }
  385. }
  386. export function createStructuralDirectiveTransform(
  387. name: string | RegExp,
  388. fn: StructuralDirectiveTransform
  389. ): NodeTransform {
  390. const matches = isString(name)
  391. ? (n: string) => n === name
  392. : (n: string) => name.test(n)
  393. return (node, context) => {
  394. if (node.type === NodeTypes.ELEMENT) {
  395. const { props } = node
  396. // structural directive transforms are not concerned with slots
  397. // as they are handled separately in vSlot.ts
  398. if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
  399. return
  400. }
  401. const exitFns = []
  402. for (let i = 0; i < props.length; i++) {
  403. const prop = props[i]
  404. if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
  405. // structural directives are removed to avoid infinite recursion
  406. // also we remove them *before* applying so that it can further
  407. // traverse itself in case it moves the node around
  408. props.splice(i, 1)
  409. i--
  410. const onExit = fn(node, prop, context)
  411. if (onExit) exitFns.push(onExit)
  412. }
  413. }
  414. return exitFns
  415. }
  416. }
  417. }