transform.ts 14 KB

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