transform.ts 14 KB

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