transform.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import {
  2. type AllNode,
  3. type TransformOptions as BaseTransformOptions,
  4. type CompilerCompatOptions,
  5. type ElementNode,
  6. ElementTypes,
  7. NodeTypes,
  8. type ParentNode,
  9. type RootNode,
  10. type TemplateChildNode,
  11. defaultOnError,
  12. defaultOnWarn,
  13. isVSlot,
  14. } from '@vue/compiler-dom'
  15. import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
  16. import {
  17. type BlockIRNode,
  18. DynamicFlag,
  19. type FragmentFactoryIRNode,
  20. type HackOptions,
  21. type IRDynamicInfo,
  22. type IRExpression,
  23. IRNodeTypes,
  24. type OperationNode,
  25. type RootIRNode,
  26. type TemplateFactoryIRNode,
  27. type VaporDirectiveNode,
  28. } from './ir'
  29. export type NodeTransform = (
  30. node: RootNode | TemplateChildNode,
  31. context: TransformContext<RootNode | TemplateChildNode>,
  32. ) => void | (() => void) | (() => void)[]
  33. export type DirectiveTransform = (
  34. dir: VaporDirectiveNode,
  35. node: ElementNode,
  36. context: TransformContext<ElementNode>,
  37. ) => void
  38. // A structural directive transform is technically also a NodeTransform;
  39. // Only v-if and v-for fall into this category.
  40. export type StructuralDirectiveTransform = (
  41. node: ElementNode,
  42. dir: VaporDirectiveNode,
  43. context: TransformContext<ElementNode>,
  44. ) => void | (() => void)
  45. export type TransformOptions = HackOptions<BaseTransformOptions>
  46. export interface TransformContext<T extends AllNode = AllNode> {
  47. node: T
  48. parent: TransformContext<ParentNode> | null
  49. root: TransformContext<RootNode>
  50. index: number
  51. block: BlockIRNode
  52. options: Required<
  53. Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
  54. >
  55. template: string
  56. childrenTemplate: (string | null)[]
  57. dynamic: IRDynamicInfo
  58. inVOnce: boolean
  59. enterBlock(ir: TransformContext['block']): () => void
  60. reference(): number
  61. increaseId(): number
  62. registerTemplate(): number
  63. registerEffect(
  64. expressions: Array<IRExpression | null | undefined>,
  65. operation: OperationNode[],
  66. ): void
  67. registerOperation(...operations: OperationNode[]): void
  68. }
  69. const defaultOptions = {
  70. filename: '',
  71. prefixIdentifiers: false,
  72. hoistStatic: false,
  73. hmr: false,
  74. cacheHandlers: false,
  75. nodeTransforms: [],
  76. directiveTransforms: {},
  77. transformHoist: null,
  78. isBuiltInComponent: NOOP,
  79. isCustomElement: NOOP,
  80. expressionPlugins: [],
  81. scopeId: null,
  82. slotted: true,
  83. ssr: false,
  84. inSSR: false,
  85. ssrCssVars: ``,
  86. bindingMetadata: EMPTY_OBJ,
  87. inline: false,
  88. isTS: false,
  89. onError: defaultOnError,
  90. onWarn: defaultOnWarn,
  91. }
  92. export const genDefaultDynamic = (): IRDynamicInfo => ({
  93. id: null,
  94. dynamicFlags: 0,
  95. placeholder: null,
  96. children: {},
  97. })
  98. // TODO use class for better perf
  99. function createRootContext(
  100. root: RootIRNode,
  101. node: RootNode,
  102. options: TransformOptions = {},
  103. ): TransformContext<RootNode> {
  104. let globalId = 0
  105. const ctx: TransformContext<RootNode> = {
  106. node,
  107. parent: null,
  108. index: 0,
  109. root: null!, // set later
  110. block: root,
  111. enterBlock(ir) {
  112. const { block, template, dynamic, childrenTemplate } = this
  113. this.block = ir
  114. this.dynamic = ir.dynamic
  115. this.template = ''
  116. this.childrenTemplate = []
  117. return () => {
  118. // exit
  119. this.block = block
  120. this.template = template
  121. this.dynamic = dynamic
  122. this.childrenTemplate = childrenTemplate
  123. }
  124. },
  125. options: extend({}, defaultOptions, options),
  126. dynamic: root.dynamic,
  127. inVOnce: false,
  128. increaseId: () => globalId++,
  129. reference() {
  130. if (this.dynamic.id !== null) return this.dynamic.id
  131. this.dynamic.dynamicFlags |= DynamicFlag.REFERENCED
  132. return (this.dynamic.id = this.increaseId())
  133. },
  134. registerEffect(expressions, operations) {
  135. if (
  136. this.inVOnce ||
  137. (expressions = expressions.filter(Boolean)).length === 0
  138. ) {
  139. return this.registerOperation(...operations)
  140. }
  141. const existing = this.block.effect.find(e =>
  142. isSameExpression(e.expressions, expressions as IRExpression[]),
  143. )
  144. if (existing) {
  145. existing.operations.push(...operations)
  146. } else {
  147. this.block.effect.push({
  148. expressions: expressions as IRExpression[],
  149. operations,
  150. })
  151. }
  152. function isSameExpression(a: IRExpression[], b: IRExpression[]) {
  153. if (a.length !== b.length) return false
  154. return a.every(
  155. (exp, i) => identifyExpression(exp) === identifyExpression(b[i]),
  156. )
  157. }
  158. function identifyExpression(exp: IRExpression) {
  159. return typeof exp === 'string' ? exp : exp.content
  160. }
  161. },
  162. template: '',
  163. childrenTemplate: [],
  164. registerTemplate() {
  165. let templateNode: TemplateFactoryIRNode | FragmentFactoryIRNode
  166. const existing = root.template.findIndex(t =>
  167. this.template
  168. ? t.type === IRNodeTypes.TEMPLATE_FACTORY &&
  169. t.template === this.template
  170. : t.type === IRNodeTypes.FRAGMENT_FACTORY,
  171. )
  172. if (existing !== -1) {
  173. return (this.block.templateIndex = existing)
  174. }
  175. if (this.template) {
  176. templateNode = {
  177. type: IRNodeTypes.TEMPLATE_FACTORY,
  178. template: this.template,
  179. loc: node.loc,
  180. }
  181. } else {
  182. templateNode = {
  183. type: IRNodeTypes.FRAGMENT_FACTORY,
  184. loc: node.loc,
  185. }
  186. }
  187. root.template.push(templateNode)
  188. return (this.block.templateIndex = root.template.length - 1)
  189. },
  190. registerOperation(...node) {
  191. this.block.operation.push(...node)
  192. },
  193. }
  194. ctx.root = ctx
  195. ctx.reference()
  196. return ctx
  197. }
  198. function createContext<T extends TemplateChildNode>(
  199. node: T,
  200. parent: TransformContext<ParentNode>,
  201. index: number,
  202. ): TransformContext<T> {
  203. const ctx: TransformContext<T> = extend({}, parent, {
  204. node,
  205. parent,
  206. index,
  207. template: '',
  208. childrenTemplate: [],
  209. dynamic: genDefaultDynamic(),
  210. } satisfies Partial<TransformContext<T>>)
  211. return ctx
  212. }
  213. // AST -> IR
  214. export function transform(
  215. root: RootNode,
  216. options: TransformOptions = {},
  217. ): RootIRNode {
  218. const ir: RootIRNode = {
  219. type: IRNodeTypes.ROOT,
  220. node: root,
  221. source: root.source,
  222. loc: root.loc,
  223. template: [],
  224. templateIndex: -1,
  225. dynamic: extend(genDefaultDynamic(), {
  226. dynamicFlags: DynamicFlag.REFERENCED | DynamicFlag.INSERT,
  227. } satisfies Partial<IRDynamicInfo>),
  228. effect: [],
  229. operation: [],
  230. }
  231. const ctx = createRootContext(ir, root, options)
  232. transformNode(ctx)
  233. ctx.registerTemplate()
  234. return ir
  235. }
  236. function transformNode(
  237. context: TransformContext<RootNode | TemplateChildNode>,
  238. ) {
  239. let { node } = context
  240. // apply transform plugins
  241. const { nodeTransforms } = context.options
  242. const exitFns = []
  243. for (const nodeTransform of nodeTransforms) {
  244. const onExit = nodeTransform(node, context)
  245. if (onExit) {
  246. if (isArray(onExit)) {
  247. exitFns.push(...onExit)
  248. } else {
  249. exitFns.push(onExit)
  250. }
  251. }
  252. if (!context.node) {
  253. // node was removed
  254. return
  255. } else {
  256. // node may have been replaced
  257. node = context.node
  258. }
  259. }
  260. switch (node.type) {
  261. case NodeTypes.ROOT:
  262. case NodeTypes.ELEMENT: {
  263. transformChildren(context as TransformContext<RootNode | ElementNode>)
  264. break
  265. }
  266. case NodeTypes.TEXT: {
  267. context.template += node.content
  268. break
  269. }
  270. case NodeTypes.COMMENT: {
  271. context.template += `<!--${node.content}-->`
  272. break
  273. }
  274. }
  275. // exit transforms
  276. context.node = node
  277. let i = exitFns.length
  278. while (i--) {
  279. exitFns[i]()
  280. }
  281. if (context.node.type === NodeTypes.ROOT)
  282. context.template += context.childrenTemplate.filter(Boolean).join('')
  283. }
  284. function transformChildren(ctx: TransformContext<RootNode | ElementNode>) {
  285. const { children } = ctx.node
  286. let i = 0
  287. for (; i < children.length; i++) {
  288. const child = children[i]
  289. const childContext = createContext(child, ctx, i)
  290. transformNode(childContext)
  291. ctx.childrenTemplate.push(childContext.template)
  292. ctx.dynamic.children[i] = childContext.dynamic
  293. }
  294. processDynamicChildren(ctx)
  295. }
  296. function processDynamicChildren(ctx: TransformContext<RootNode | ElementNode>) {
  297. const { node } = ctx
  298. let prevChildren: IRDynamicInfo[] = []
  299. let hasStatic = false
  300. for (let index = 0; index < node.children.length; index++) {
  301. const child = ctx.dynamic.children[index]
  302. if (!child || !(child.dynamicFlags & DynamicFlag.INSERT)) {
  303. if (prevChildren.length) {
  304. if (hasStatic) {
  305. ctx.childrenTemplate[index - prevChildren.length] = `<!>`
  306. const anchor = (prevChildren[0].placeholder = ctx.increaseId())
  307. ctx.registerOperation({
  308. type: IRNodeTypes.INSERT_NODE,
  309. loc: node.loc,
  310. element: prevChildren.map(child => child.id!),
  311. parent: ctx.reference(),
  312. anchor,
  313. })
  314. } else {
  315. ctx.registerOperation({
  316. type: IRNodeTypes.PREPEND_NODE,
  317. loc: node.loc,
  318. elements: prevChildren.map(child => child.id!),
  319. parent: ctx.reference(),
  320. })
  321. }
  322. }
  323. hasStatic = true
  324. prevChildren = []
  325. continue
  326. }
  327. prevChildren.push(child)
  328. if (index === node.children.length - 1) {
  329. ctx.registerOperation({
  330. type: IRNodeTypes.APPEND_NODE,
  331. loc: node.loc,
  332. elements: prevChildren.map(child => child.id!),
  333. parent: ctx.reference(),
  334. })
  335. }
  336. }
  337. }
  338. export function createStructuralDirectiveTransform(
  339. name: string | string[],
  340. fn: StructuralDirectiveTransform,
  341. ): NodeTransform {
  342. const matches = (n: string) =>
  343. isString(name) ? n === name : name.includes(n)
  344. return (node, context) => {
  345. if (node.type === NodeTypes.ELEMENT) {
  346. const { props } = node
  347. // structural directive transforms are not concerned with slots
  348. // as they are handled separately in vSlot.ts
  349. if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
  350. return
  351. }
  352. const exitFns = []
  353. for (const prop of props) {
  354. if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
  355. const onExit = fn(
  356. node,
  357. prop as VaporDirectiveNode,
  358. context as TransformContext<ElementNode>,
  359. )
  360. if (onExit) exitFns.push(onExit)
  361. }
  362. }
  363. return exitFns
  364. }
  365. }
  366. }