transform.ts 13 KB

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