codegen.ts 9.7 KB

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