transform.ts 12 KB

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