transform.ts 14 KB

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