codegen.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import {
  2. RootNode,
  3. ChildNode,
  4. ElementNode,
  5. IfNode,
  6. ForNode,
  7. TextNode,
  8. CommentNode,
  9. ExpressionNode,
  10. NodeTypes,
  11. JSChildNode,
  12. CallExpression,
  13. ArrayExpression,
  14. ObjectExpression,
  15. IfBranchNode
  16. } from './ast'
  17. import { SourceMapGenerator, RawSourceMap } from 'source-map'
  18. import { advancePositionWithMutation, assert } from './utils'
  19. import { isString, isArray } from '@vue/shared'
  20. import { RENDER_LIST_HELPER } from './transforms/vFor'
  21. type CodegenNode = ChildNode | JSChildNode
  22. export interface CodegenOptions {
  23. // will generate import statements for
  24. // runtime helpers; otherwise will grab the helpers from global `Vue`.
  25. // default: false
  26. mode?: 'module' | 'function'
  27. useWith?: boolean
  28. // Filename for source map generation.
  29. filename?: string
  30. }
  31. export interface CodegenResult {
  32. code: string
  33. map?: RawSourceMap
  34. }
  35. export interface CodegenContext extends Required<CodegenOptions> {
  36. source: string
  37. code: string
  38. line: number
  39. column: number
  40. offset: number
  41. indentLevel: number
  42. imports: Set<string>
  43. knownIdentifiers: Set<string>
  44. map?: SourceMapGenerator
  45. push(code: string, node?: CodegenNode): void
  46. indent(): void
  47. deindent(): void
  48. newline(): void
  49. }
  50. function createCodegenContext(
  51. ast: RootNode,
  52. {
  53. mode = 'function',
  54. useWith = true,
  55. filename = `template.vue.html`
  56. }: CodegenOptions
  57. ): CodegenContext {
  58. const context: CodegenContext = {
  59. mode,
  60. useWith,
  61. filename,
  62. source: ast.loc.source,
  63. code: ``,
  64. column: 1,
  65. line: 1,
  66. offset: 0,
  67. indentLevel: 0,
  68. imports: new Set(),
  69. knownIdentifiers: new Set(),
  70. // lazy require source-map implementation, only in non-browser builds!
  71. map: __BROWSER__
  72. ? undefined
  73. : new (require('source-map')).SourceMapGenerator(),
  74. push(code, node?: CodegenNode) {
  75. context.code += code
  76. if (context.map) {
  77. if (node) {
  78. context.map.addMapping({
  79. source: context.filename,
  80. original: {
  81. line: node.loc.start.line,
  82. column: node.loc.start.column - 1 // source-map column is 0 based
  83. },
  84. generated: {
  85. line: context.line,
  86. column: context.column - 1
  87. }
  88. })
  89. }
  90. advancePositionWithMutation(context, code, code.length)
  91. }
  92. },
  93. indent() {
  94. newline(++context.indentLevel)
  95. },
  96. deindent() {
  97. newline(--context.indentLevel)
  98. },
  99. newline() {
  100. newline(context.indentLevel)
  101. }
  102. }
  103. const newline = (n: number) => context.push('\n' + ` `.repeat(n))
  104. if (!__BROWSER__) {
  105. context.map!.setSourceContent(filename, context.source)
  106. }
  107. return context
  108. }
  109. export function generate(
  110. ast: RootNode,
  111. options: CodegenOptions = {}
  112. ): CodegenResult {
  113. const context = createCodegenContext(ast, options)
  114. // TODO handle different output for module mode and IIFE mode
  115. const { mode, push, useWith, indent, deindent } = context
  116. if (mode === 'function') {
  117. // TODO generate const declarations for helpers
  118. push(`return `)
  119. } else {
  120. // TODO generate import statements for helpers
  121. push(`export default `)
  122. }
  123. push(`function render() {`)
  124. if (useWith) {
  125. indent()
  126. push(`with (this) {`)
  127. }
  128. indent()
  129. push(`return `)
  130. genChildren(ast.children, context)
  131. if (useWith) {
  132. deindent()
  133. push(`}`)
  134. }
  135. deindent()
  136. push(`}`)
  137. return {
  138. code: context.code,
  139. map: context.map ? context.map.toJSON() : undefined
  140. }
  141. }
  142. // This will generate a single vnode call if the list has length === 1.
  143. function genChildren(children: ChildNode[], context: CodegenContext) {
  144. if (children.length === 1) {
  145. genNode(children[0], context)
  146. } else {
  147. genNodeListAsArray(children, context)
  148. }
  149. }
  150. function genNodeListAsArray(
  151. nodes: (string | CodegenNode | ChildNode[])[],
  152. context: CodegenContext
  153. ) {
  154. const multilines = nodes.length > 1
  155. context.push(`[`)
  156. multilines && context.indent()
  157. genNodeList(nodes, context, multilines)
  158. multilines && context.deindent()
  159. context.push(`]`)
  160. }
  161. function genNodeList(
  162. nodes: (string | CodegenNode | ChildNode[])[],
  163. context: CodegenContext,
  164. multilines: boolean = false
  165. ) {
  166. const { push, newline } = context
  167. for (let i = 0; i < nodes.length; i++) {
  168. const node = nodes[i]
  169. if (isString(node)) {
  170. // plain code string
  171. // note not adding quotes here because this can be any code,
  172. // not just plain strings.
  173. push(node)
  174. } else if (isArray(node)) {
  175. // child VNodes in a h() call
  176. // not using genChildren here because we want them to always be an array
  177. genNodeListAsArray(node, context)
  178. } else {
  179. genNode(node, context)
  180. }
  181. if (i < nodes.length - 1) {
  182. if (multilines) {
  183. push(',')
  184. newline()
  185. } else {
  186. push(', ')
  187. }
  188. }
  189. }
  190. }
  191. function genNode(node: CodegenNode, context: CodegenContext) {
  192. switch (node.type) {
  193. case NodeTypes.ELEMENT:
  194. genElement(node, context)
  195. break
  196. case NodeTypes.TEXT:
  197. genText(node, context)
  198. break
  199. case NodeTypes.EXPRESSION:
  200. genExpression(node, context)
  201. break
  202. case NodeTypes.COMMENT:
  203. genComment(node, context)
  204. break
  205. case NodeTypes.IF:
  206. genIf(node, context)
  207. break
  208. case NodeTypes.FOR:
  209. genFor(node, context)
  210. break
  211. case NodeTypes.JS_CALL_EXPRESSION:
  212. genCallExpression(node, context)
  213. break
  214. case NodeTypes.JS_OBJECT_EXPRESSION:
  215. genObjectExpression(node, context)
  216. break
  217. case NodeTypes.JS_ARRAY_EXPRESSION:
  218. genArrayExpression(node, context)
  219. }
  220. }
  221. function genElement(node: ElementNode, context: CodegenContext) {
  222. __DEV__ &&
  223. assert(
  224. node.codegenNode != null,
  225. `AST is not transformed for codegen. ` +
  226. `Apply appropriate transforms first.`
  227. )
  228. genCallExpression(node.codegenNode!, context, false)
  229. }
  230. function genText(node: TextNode | ExpressionNode, context: CodegenContext) {
  231. context.push(JSON.stringify(node.content), node)
  232. }
  233. function genExpression(node: ExpressionNode, context: CodegenContext) {
  234. // if (node.codegenNode) {
  235. // TODO handle transformed expression
  236. // }
  237. const text = node.isStatic ? JSON.stringify(node.content) : node.content
  238. context.push(text, node)
  239. }
  240. function genExpressionAsPropertyKey(
  241. node: ExpressionNode,
  242. context: CodegenContext
  243. ) {
  244. // if (node.codegenNode) {
  245. // TODO handle transformed expression
  246. // }
  247. if (node.isStatic) {
  248. // only quote keys if necessary
  249. const text = /^\d|[^\w]/.test(node.content)
  250. ? JSON.stringify(node.content)
  251. : node.content
  252. context.push(text, node)
  253. } else {
  254. context.push(`[${node.content}]`, node)
  255. }
  256. }
  257. function genComment(node: CommentNode, context: CodegenContext) {
  258. context.push(`<!--${node.content}-->`, node)
  259. }
  260. // control flow
  261. function genIf(node: IfNode, context: CodegenContext) {
  262. genIfBranch(node.branches[0], node.branches, 1, context)
  263. }
  264. function genIfBranch(
  265. { condition, children }: IfBranchNode,
  266. branches: IfBranchNode[],
  267. nextIndex: number,
  268. context: CodegenContext
  269. ) {
  270. if (condition) {
  271. // v-if or v-else-if
  272. context.push(`(${condition.content})`, condition)
  273. context.push(`?`)
  274. genChildren(children, context)
  275. context.push(`:`)
  276. if (nextIndex < branches.length) {
  277. genIfBranch(branches[nextIndex], branches, nextIndex + 1, context)
  278. } else {
  279. context.push(`null`)
  280. }
  281. } else {
  282. // v-else
  283. __DEV__ && assert(nextIndex === branches.length)
  284. genChildren(children, context)
  285. }
  286. }
  287. function genFor(node: ForNode, context: CodegenContext) {
  288. const { push } = context
  289. const { source, keyAlias, valueAlias, objectIndexAlias, children } = node
  290. push(`${RENDER_LIST_HELPER}(`, node)
  291. genExpression(source, context)
  292. context.push(`(`)
  293. if (valueAlias) {
  294. // not using genExpression here because these aliases can only be code
  295. // that is valid in the function argument position, so the parse rule can
  296. // be off and they don't need identifier prefixing anyway.
  297. push(valueAlias.content, valueAlias)
  298. push(`, `)
  299. }
  300. if (keyAlias) {
  301. if (!valueAlias) {
  302. push(`_, `)
  303. }
  304. push(keyAlias.content, keyAlias)
  305. push(`, `)
  306. }
  307. if (objectIndexAlias) {
  308. if (!keyAlias) {
  309. if (!valueAlias) {
  310. push(`_, `)
  311. }
  312. push(`_, `)
  313. }
  314. push(objectIndexAlias.content, objectIndexAlias)
  315. }
  316. context.push(`) => `)
  317. genChildren(children, context)
  318. context.push(`)`)
  319. }
  320. // JavaScript
  321. function genCallExpression(
  322. node: CallExpression,
  323. context: CodegenContext,
  324. multilines = node.arguments.length > 1
  325. ) {
  326. context.push(node.callee + `(`, node)
  327. multilines && context.indent()
  328. genNodeList(node.arguments, context, multilines)
  329. multilines && context.deindent()
  330. context.push(`)`)
  331. }
  332. function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
  333. const { push, indent, deindent, newline } = context
  334. const { properties } = node
  335. const multilines = properties.length > 1
  336. push(`{`, node)
  337. multilines && indent()
  338. for (let i = 0; i < properties.length; i++) {
  339. const { key, value } = properties[i]
  340. // key
  341. genExpressionAsPropertyKey(key, context)
  342. push(`: `)
  343. // value
  344. genExpression(value, context)
  345. if (i < properties.length - 1) {
  346. if (multilines) {
  347. push(`,`)
  348. newline()
  349. } else {
  350. push(`, `)
  351. }
  352. }
  353. }
  354. multilines && deindent()
  355. push(`}`)
  356. }
  357. function genArrayExpression(node: ArrayExpression, context: CodegenContext) {
  358. genNodeListAsArray(node.elements, context)
  359. }