transform.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. import type {
  2. NodeTypes,
  3. RootNode,
  4. Node,
  5. TemplateChildNode,
  6. ElementNode,
  7. AttributeNode,
  8. InterpolationNode,
  9. TransformOptions,
  10. DirectiveNode,
  11. ExpressionNode,
  12. } from '@vue/compiler-dom'
  13. import {
  14. type OperationNode,
  15. type RootIRNode,
  16. IRNodeTypes,
  17. DynamicInfo,
  18. } from './ir'
  19. import { isVoidTag } from '@vue/shared'
  20. import {
  21. ErrorCodes,
  22. createCompilerError,
  23. defaultOnError,
  24. defaultOnWarn,
  25. } from './errors'
  26. export interface TransformContext<T extends Node = Node> {
  27. node: T
  28. parent: TransformContext | null
  29. root: TransformContext<RootNode>
  30. index: number
  31. options: TransformOptions
  32. template: string
  33. dynamic: DynamicInfo
  34. once: boolean
  35. reference(): number
  36. increaseId(): number
  37. registerTemplate(): number
  38. registerEffect(expr: string, operation: OperationNode): void
  39. registerOperation(...operations: OperationNode[]): void
  40. helper(name: string): string
  41. }
  42. function createRootContext(
  43. ir: RootIRNode,
  44. node: RootNode,
  45. options: TransformOptions,
  46. ): TransformContext<RootNode> {
  47. let globalId = 0
  48. const { effect, operation: operation, helpers, vaporHelpers } = ir
  49. const ctx: TransformContext<RootNode> = {
  50. node,
  51. parent: null,
  52. index: 0,
  53. root: null!, // set later
  54. options,
  55. dynamic: ir.dynamic,
  56. once: false,
  57. increaseId: () => globalId++,
  58. reference() {
  59. if (this.dynamic.id !== null) return this.dynamic.id
  60. this.dynamic.referenced = true
  61. return (this.dynamic.id = this.increaseId())
  62. },
  63. registerEffect(expr, operation) {
  64. if (this.once) {
  65. return this.registerOperation(operation)
  66. }
  67. if (!effect[expr]) effect[expr] = []
  68. effect[expr].push(operation)
  69. },
  70. template: '',
  71. registerTemplate() {
  72. if (!ctx.template) return -1
  73. const idx = ir.template.findIndex(
  74. (t) =>
  75. t.type === IRNodeTypes.TEMPLATE_FACTORY &&
  76. t.template === ctx.template,
  77. )
  78. if (idx !== -1) return idx
  79. ir.template.push({
  80. type: IRNodeTypes.TEMPLATE_FACTORY,
  81. template: ctx.template,
  82. loc: node.loc,
  83. })
  84. return ir.template.length - 1
  85. },
  86. registerOperation(...node) {
  87. operation.push(...node)
  88. },
  89. // TODO not used yet
  90. helper(name, vapor = true) {
  91. ;(vapor ? vaporHelpers : helpers).add(name)
  92. return name
  93. },
  94. }
  95. ctx.root = ctx
  96. ctx.reference()
  97. return ctx
  98. }
  99. function createContext<T extends TemplateChildNode>(
  100. node: T,
  101. parent: TransformContext,
  102. index: number,
  103. ): TransformContext<T> {
  104. const ctx: TransformContext<T> = {
  105. ...parent,
  106. node,
  107. parent,
  108. index,
  109. template: '',
  110. dynamic: {
  111. id: null,
  112. referenced: false,
  113. ghost: false,
  114. placeholder: null,
  115. children: {},
  116. },
  117. }
  118. return ctx
  119. }
  120. // AST -> IR
  121. export function transform(
  122. root: RootNode,
  123. options: TransformOptions = {},
  124. ): RootIRNode {
  125. options.onError ||= defaultOnError
  126. options.onWarn ||= defaultOnWarn
  127. const ir: RootIRNode = {
  128. type: IRNodeTypes.ROOT,
  129. loc: root.loc,
  130. template: [],
  131. dynamic: {
  132. id: null,
  133. referenced: true,
  134. ghost: true,
  135. placeholder: null,
  136. children: {},
  137. },
  138. effect: Object.create(null),
  139. operation: [],
  140. helpers: new Set([]),
  141. vaporHelpers: new Set([]),
  142. }
  143. const ctx = createRootContext(ir, root, options)
  144. // TODO: transform presets, see packages/compiler-core/src/transforms
  145. transformChildren(ctx, true)
  146. if (ir.template.length === 0) {
  147. ir.template.push({
  148. type: IRNodeTypes.FRAGMENT_FACTORY,
  149. loc: root.loc,
  150. })
  151. }
  152. return ir
  153. }
  154. function transformChildren(
  155. ctx: TransformContext<RootNode | ElementNode>,
  156. root?: boolean,
  157. ) {
  158. const {
  159. node: { children },
  160. } = ctx
  161. const childrenTemplate: string[] = []
  162. children.forEach((child, i) => walkNode(child, i))
  163. processDynamicChildren()
  164. ctx.template += childrenTemplate.join('')
  165. if (root) ctx.registerTemplate()
  166. function processDynamicChildren() {
  167. let prevChildren: DynamicInfo[] = []
  168. let hasStatic = false
  169. for (let index = 0; index < children.length; index++) {
  170. const child = ctx.dynamic.children[index]
  171. if (!child || !child.ghost) {
  172. if (prevChildren.length)
  173. if (hasStatic) {
  174. childrenTemplate[index - prevChildren.length] = `<!>`
  175. const anchor = (prevChildren[0].placeholder = ctx.increaseId())
  176. ctx.registerOperation({
  177. type: IRNodeTypes.INSERT_NODE,
  178. loc: ctx.node.loc,
  179. element: prevChildren.map((child) => child.id!),
  180. parent: ctx.reference(),
  181. anchor,
  182. })
  183. } else {
  184. ctx.registerOperation({
  185. type: IRNodeTypes.PREPEND_NODE,
  186. loc: ctx.node.loc,
  187. elements: prevChildren.map((child) => child.id!),
  188. parent: ctx.reference(),
  189. })
  190. }
  191. hasStatic = true
  192. prevChildren = []
  193. continue
  194. }
  195. prevChildren.push(child)
  196. if (index === children.length - 1) {
  197. ctx.registerOperation({
  198. type: IRNodeTypes.APPEND_NODE,
  199. loc: ctx.node.loc,
  200. elements: prevChildren.map((child) => child.id!),
  201. parent: ctx.reference(),
  202. })
  203. }
  204. }
  205. }
  206. function walkNode(node: TemplateChildNode, index: number) {
  207. const child = createContext(node, ctx, index)
  208. const isFirst = index === 0
  209. const isLast = index === children.length - 1
  210. switch (node.type) {
  211. case 1 satisfies NodeTypes.ELEMENT: {
  212. transformElement(child as TransformContext<ElementNode>)
  213. break
  214. }
  215. case 2 satisfies NodeTypes.TEXT: {
  216. child.template += node.content
  217. break
  218. }
  219. case 3 satisfies NodeTypes.COMMENT: {
  220. child.template += `<!--${node.content}-->`
  221. break
  222. }
  223. case 5 satisfies NodeTypes.INTERPOLATION: {
  224. transformInterpolation(
  225. child as TransformContext<InterpolationNode>,
  226. isFirst,
  227. isLast,
  228. )
  229. break
  230. }
  231. case 12 satisfies NodeTypes.TEXT_CALL:
  232. // never?
  233. break
  234. default: {
  235. // TODO handle other types
  236. // CompoundExpressionNode
  237. // IfNode
  238. // IfBranchNode
  239. // ForNode
  240. child.template += `[type: ${node.type}]`
  241. }
  242. }
  243. childrenTemplate.push(child.template)
  244. if (
  245. child.dynamic.ghost ||
  246. child.dynamic.referenced ||
  247. child.dynamic.placeholder ||
  248. Object.keys(child.dynamic.children).length
  249. ) {
  250. ctx.dynamic.children[index] = child.dynamic
  251. }
  252. }
  253. }
  254. function transformElement(ctx: TransformContext<ElementNode>) {
  255. const { node } = ctx
  256. const { tag, props, children } = node
  257. ctx.template += `<${tag}`
  258. props.forEach((prop) => transformProp(prop, ctx))
  259. ctx.template += `>`
  260. if (children.length) transformChildren(ctx)
  261. // TODO remove unnecessary close tag, e.g. if it's the last element of the template
  262. if (!isVoidTag(tag)) {
  263. ctx.template += `</${tag}>`
  264. }
  265. }
  266. function transformInterpolation(
  267. ctx: TransformContext<InterpolationNode>,
  268. isFirst: boolean,
  269. isLast: boolean,
  270. ) {
  271. const { node } = ctx
  272. if (node.content.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
  273. // TODO: CompoundExpressionNode: {{ count + 1 }}
  274. return
  275. }
  276. const expr = processExpression(ctx, node.content)!
  277. if (isFirst && isLast) {
  278. const parent = ctx.parent!
  279. const parentId = parent.reference()
  280. ctx.registerEffect(expr, {
  281. type: IRNodeTypes.SET_TEXT,
  282. loc: node.loc,
  283. element: parentId,
  284. value: expr,
  285. })
  286. } else {
  287. const id = ctx.reference()
  288. ctx.dynamic.ghost = true
  289. ctx.registerOperation({
  290. type: IRNodeTypes.CREATE_TEXT_NODE,
  291. loc: node.loc,
  292. id,
  293. value: expr,
  294. })
  295. ctx.registerEffect(expr, {
  296. type: IRNodeTypes.SET_TEXT,
  297. loc: node.loc,
  298. element: id,
  299. value: expr,
  300. })
  301. }
  302. }
  303. function transformProp(
  304. node: DirectiveNode | AttributeNode,
  305. ctx: TransformContext<ElementNode>,
  306. ): void {
  307. const { name } = node
  308. if (node.type === (6 satisfies NodeTypes.ATTRIBUTE)) {
  309. if (node.value) {
  310. ctx.template += ` ${name}="${node.value.content}"`
  311. } else {
  312. ctx.template += ` ${name}`
  313. }
  314. return
  315. }
  316. const { exp, loc, modifiers } = node
  317. const expr = processExpression(ctx, exp)
  318. switch (name) {
  319. case 'bind': {
  320. if (
  321. !exp ||
  322. (exp.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION) &&
  323. !exp.content.trim())
  324. ) {
  325. ctx.options.onError!(
  326. createCompilerError(ErrorCodes.VAPOR_BIND_NO_EXPRESSION, loc),
  327. )
  328. return
  329. }
  330. if (expr === null) {
  331. // TODO: Vue 3.4 supported shorthand syntax
  332. // https://github.com/vuejs/core/pull/9451
  333. return
  334. } else if (!node.arg) {
  335. // TODO support v-bind="{}"
  336. return
  337. } else if (
  338. node.arg.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)
  339. ) {
  340. // TODO support :[foo]="bar"
  341. return
  342. }
  343. ctx.registerEffect(expr, {
  344. type: IRNodeTypes.SET_PROP,
  345. loc: node.loc,
  346. element: ctx.reference(),
  347. name: node.arg.content,
  348. value: expr,
  349. })
  350. break
  351. }
  352. case 'on': {
  353. if (!exp && !modifiers.length) {
  354. ctx.options.onError!(
  355. createCompilerError(ErrorCodes.VAPOR_ON_NO_EXPRESSION, loc),
  356. )
  357. return
  358. }
  359. if (!node.arg) {
  360. // TODO support v-on="{}"
  361. return
  362. } else if (
  363. node.arg.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)
  364. ) {
  365. // TODO support @[foo]="bar"
  366. return
  367. } else if (expr === null) {
  368. // TODO: support @foo
  369. // https://github.com/vuejs/core/pull/9451
  370. return
  371. }
  372. ctx.registerEffect(expr, {
  373. type: IRNodeTypes.SET_EVENT,
  374. loc: node.loc,
  375. element: ctx.reference(),
  376. name: node.arg.content,
  377. value: expr,
  378. })
  379. break
  380. }
  381. case 'html': {
  382. const value = expr || '""'
  383. ctx.registerEffect(value, {
  384. type: IRNodeTypes.SET_HTML,
  385. loc: node.loc,
  386. element: ctx.reference(),
  387. value,
  388. })
  389. break
  390. }
  391. case 'text': {
  392. const value = expr || '""'
  393. ctx.registerEffect(value, {
  394. type: IRNodeTypes.SET_TEXT,
  395. loc: node.loc,
  396. element: ctx.reference(),
  397. value,
  398. })
  399. break
  400. }
  401. case 'once': {
  402. ctx.once = true
  403. break
  404. }
  405. case 'cloak': {
  406. // do nothing
  407. break
  408. }
  409. }
  410. }
  411. // TODO: reuse packages/compiler-core/src/transforms/transformExpression.ts
  412. function processExpression(
  413. ctx: TransformContext,
  414. expr: ExpressionNode | undefined,
  415. ): string | null {
  416. if (!expr) return null
  417. if (expr.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
  418. // TODO
  419. return ''
  420. }
  421. const { content } = expr
  422. if (ctx.options.bindingMetadata?.[content] === 'setup-ref') {
  423. return content + '.value'
  424. }
  425. return content
  426. }