codegen.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. import { CodegenOptions } from './options'
  2. import {
  3. RootNode,
  4. TemplateChildNode,
  5. TextNode,
  6. CommentNode,
  7. ExpressionNode,
  8. NodeTypes,
  9. JSChildNode,
  10. CallExpression,
  11. ArrayExpression,
  12. ObjectExpression,
  13. Position,
  14. InterpolationNode,
  15. CompoundExpressionNode,
  16. SimpleExpressionNode,
  17. FunctionExpression,
  18. SequenceExpression,
  19. ConditionalExpression,
  20. CacheExpression,
  21. locStub
  22. } from './ast'
  23. import { SourceMapGenerator, RawSourceMap } from 'source-map'
  24. import {
  25. advancePositionWithMutation,
  26. assert,
  27. isSimpleIdentifier,
  28. loadDep,
  29. toValidAssetId
  30. } from './utils'
  31. import { isString, isArray, isSymbol } from '@vue/shared'
  32. import {
  33. helperNameMap,
  34. TO_STRING,
  35. CREATE_VNODE,
  36. RESOLVE_COMPONENT,
  37. RESOLVE_DIRECTIVE,
  38. SET_BLOCK_TRACKING,
  39. CREATE_COMMENT,
  40. CREATE_TEXT
  41. } from './runtimeHelpers'
  42. import { ImportItem } from './transform'
  43. type CodegenNode = TemplateChildNode | JSChildNode
  44. export interface CodegenResult {
  45. code: string
  46. ast: RootNode
  47. map?: RawSourceMap
  48. }
  49. export interface CodegenContext extends Required<CodegenOptions> {
  50. source: string
  51. code: string
  52. line: number
  53. column: number
  54. offset: number
  55. indentLevel: number
  56. map?: SourceMapGenerator
  57. helper(key: symbol): string
  58. push(code: string, node?: CodegenNode): void
  59. indent(): void
  60. deindent(withoutNewLine?: boolean): void
  61. newline(): void
  62. }
  63. function createCodegenContext(
  64. ast: RootNode,
  65. {
  66. mode = 'function',
  67. prefixIdentifiers = mode === 'module',
  68. sourceMap = false,
  69. filename = `template.vue.html`
  70. }: CodegenOptions
  71. ): CodegenContext {
  72. const context: CodegenContext = {
  73. mode,
  74. prefixIdentifiers,
  75. sourceMap,
  76. filename,
  77. source: ast.loc.source,
  78. code: ``,
  79. column: 1,
  80. line: 1,
  81. offset: 0,
  82. indentLevel: 0,
  83. map: undefined,
  84. helper(key) {
  85. const name = helperNameMap[key]
  86. return prefixIdentifiers ? name : `_${name}`
  87. },
  88. push(code, node) {
  89. context.code += code
  90. if (!__BROWSER__ && context.map) {
  91. if (node) {
  92. let name
  93. if (node.type === NodeTypes.SIMPLE_EXPRESSION && !node.isStatic) {
  94. const content = node.content.replace(/^_ctx\./, '')
  95. if (content !== node.content && isSimpleIdentifier(content)) {
  96. name = content
  97. }
  98. }
  99. addMapping(node.loc.start, name)
  100. }
  101. advancePositionWithMutation(context, code)
  102. if (node && node.loc !== locStub) {
  103. addMapping(node.loc.end)
  104. }
  105. }
  106. },
  107. indent() {
  108. newline(++context.indentLevel)
  109. },
  110. deindent(withoutNewLine = false) {
  111. if (withoutNewLine) {
  112. --context.indentLevel
  113. } else {
  114. newline(--context.indentLevel)
  115. }
  116. },
  117. newline() {
  118. newline(context.indentLevel)
  119. }
  120. }
  121. function newline(n: number) {
  122. context.push('\n' + ` `.repeat(n))
  123. }
  124. function addMapping(loc: Position, name?: string) {
  125. context.map!.addMapping({
  126. name,
  127. source: context.filename,
  128. original: {
  129. line: loc.line,
  130. column: loc.column - 1 // source-map column is 0 based
  131. },
  132. generated: {
  133. line: context.line,
  134. column: context.column - 1
  135. }
  136. })
  137. }
  138. if (!__BROWSER__ && sourceMap) {
  139. // lazy require source-map implementation, only in non-browser builds
  140. context.map = new (loadDep('source-map')).SourceMapGenerator()
  141. context.map!.setSourceContent(filename, context.source)
  142. }
  143. return context
  144. }
  145. export function generate(
  146. ast: RootNode,
  147. options: CodegenOptions = {}
  148. ): CodegenResult {
  149. const context = createCodegenContext(ast, options)
  150. const {
  151. mode,
  152. push,
  153. helper,
  154. prefixIdentifiers,
  155. indent,
  156. deindent,
  157. newline
  158. } = context
  159. const hasHelpers = ast.helpers.length > 0
  160. const useWithBlock = !prefixIdentifiers && mode !== 'module'
  161. // preambles
  162. if (mode === 'function') {
  163. // Generate const declaration for helpers
  164. // In prefix mode, we place the const declaration at top so it's done
  165. // only once; But if we not prefixing, we place the declaration inside the
  166. // with block so it doesn't incur the `in` check cost for every helper access.
  167. if (hasHelpers) {
  168. if (prefixIdentifiers) {
  169. push(`const { ${ast.helpers.map(helper).join(', ')} } = Vue\n`)
  170. } else {
  171. // "with" mode.
  172. // save Vue in a separate variable to avoid collision
  173. push(`const _Vue = Vue\n`)
  174. // in "with" mode, helpers are declared inside the with block to avoid
  175. // has check cost, but hoists are lifted out of the function - we need
  176. // to provide the helper here.
  177. if (ast.hoists.length) {
  178. const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
  179. .filter(helper => ast.helpers.includes(helper))
  180. .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
  181. .join(', ')
  182. push(`const { ${staticHelpers} } = Vue\n`)
  183. }
  184. }
  185. }
  186. genHoists(ast.hoists, context)
  187. newline()
  188. push(`return `)
  189. } else {
  190. // generate import statements for helpers
  191. if (hasHelpers) {
  192. push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
  193. }
  194. if (ast.imports.length) {
  195. genImports(ast.imports, context)
  196. newline()
  197. }
  198. genHoists(ast.hoists, context)
  199. newline()
  200. push(`export default `)
  201. }
  202. // enter render function
  203. push(`function render() {`)
  204. indent()
  205. if (useWithBlock) {
  206. push(`with (this) {`)
  207. indent()
  208. // function mode const declarations should be inside with block
  209. // also they should be renamed to avoid collision with user properties
  210. if (hasHelpers) {
  211. push(
  212. `const { ${ast.helpers
  213. .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
  214. .join(', ')} } = _Vue`
  215. )
  216. newline()
  217. if (ast.cached > 0) {
  218. push(`const _cache = $cache`)
  219. newline()
  220. }
  221. newline()
  222. }
  223. } else {
  224. push(`const _ctx = this`)
  225. if (ast.cached > 0) {
  226. newline()
  227. push(`const _cache = _ctx.$cache`)
  228. }
  229. newline()
  230. }
  231. // generate asset resolution statements
  232. if (ast.components.length) {
  233. genAssets(ast.components, 'component', context)
  234. }
  235. if (ast.directives.length) {
  236. genAssets(ast.directives, 'directive', context)
  237. }
  238. if (ast.components.length || ast.directives.length) {
  239. newline()
  240. }
  241. // generate the VNode tree expression
  242. push(`return `)
  243. if (ast.codegenNode) {
  244. genNode(ast.codegenNode, context)
  245. } else {
  246. push(`null`)
  247. }
  248. if (useWithBlock) {
  249. deindent()
  250. push(`}`)
  251. }
  252. deindent()
  253. push(`}`)
  254. return {
  255. ast,
  256. code: context.code,
  257. // SourceMapGenerator does have toJSON() method but it's not in the types
  258. map: context.map ? (context.map as any).toJSON() : undefined
  259. }
  260. }
  261. function genAssets(
  262. assets: string[],
  263. type: 'component' | 'directive',
  264. context: CodegenContext
  265. ) {
  266. const resolver = context.helper(
  267. type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE
  268. )
  269. for (let i = 0; i < assets.length; i++) {
  270. const id = assets[i]
  271. context.push(
  272. `const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})`
  273. )
  274. context.newline()
  275. }
  276. }
  277. function genHoists(hoists: JSChildNode[], context: CodegenContext) {
  278. if (!hoists.length) {
  279. return
  280. }
  281. context.newline()
  282. hoists.forEach((exp, i) => {
  283. context.push(`const _hoisted_${i + 1} = `)
  284. genNode(exp, context)
  285. context.newline()
  286. })
  287. }
  288. function genImports(importsOptions: ImportItem[], context: CodegenContext) {
  289. if (!importsOptions.length) {
  290. return
  291. }
  292. importsOptions.forEach(imports => {
  293. context.push(`import `)
  294. genNode(imports.exp, context)
  295. context.push(` from '${imports.path}'`)
  296. context.newline()
  297. })
  298. }
  299. function isText(n: string | CodegenNode) {
  300. return (
  301. isString(n) ||
  302. n.type === NodeTypes.SIMPLE_EXPRESSION ||
  303. n.type === NodeTypes.TEXT ||
  304. n.type === NodeTypes.INTERPOLATION ||
  305. n.type === NodeTypes.COMPOUND_EXPRESSION
  306. )
  307. }
  308. function genNodeListAsArray(
  309. nodes: (string | CodegenNode | TemplateChildNode[])[],
  310. context: CodegenContext
  311. ) {
  312. const multilines =
  313. nodes.length > 3 ||
  314. ((!__BROWSER__ || __DEV__) && nodes.some(n => isArray(n) || !isText(n)))
  315. context.push(`[`)
  316. multilines && context.indent()
  317. genNodeList(nodes, context, multilines)
  318. multilines && context.deindent()
  319. context.push(`]`)
  320. }
  321. function genNodeList(
  322. nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
  323. context: CodegenContext,
  324. multilines: boolean = false
  325. ) {
  326. const { push, newline } = context
  327. for (let i = 0; i < nodes.length; i++) {
  328. const node = nodes[i]
  329. if (isString(node)) {
  330. push(node)
  331. } else if (isArray(node)) {
  332. genNodeListAsArray(node, context)
  333. } else {
  334. genNode(node, context)
  335. }
  336. if (i < nodes.length - 1) {
  337. if (multilines) {
  338. push(',')
  339. newline()
  340. } else {
  341. push(', ')
  342. }
  343. }
  344. }
  345. }
  346. function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
  347. if (isString(node)) {
  348. context.push(node)
  349. return
  350. }
  351. if (isSymbol(node)) {
  352. context.push(context.helper(node))
  353. return
  354. }
  355. switch (node.type) {
  356. case NodeTypes.ELEMENT:
  357. case NodeTypes.IF:
  358. case NodeTypes.FOR:
  359. __DEV__ &&
  360. assert(
  361. node.codegenNode != null,
  362. `Codegen node is missing for element/if/for node. ` +
  363. `Apply appropriate transforms first.`
  364. )
  365. genNode(node.codegenNode!, context)
  366. break
  367. case NodeTypes.TEXT:
  368. genText(node, context)
  369. break
  370. case NodeTypes.SIMPLE_EXPRESSION:
  371. genExpression(node, context)
  372. break
  373. case NodeTypes.INTERPOLATION:
  374. genInterpolation(node, context)
  375. break
  376. case NodeTypes.TEXT_CALL:
  377. genNode(node.codegenNode, context)
  378. break
  379. case NodeTypes.COMPOUND_EXPRESSION:
  380. genCompoundExpression(node, context)
  381. break
  382. case NodeTypes.COMMENT:
  383. genComment(node, context)
  384. break
  385. case NodeTypes.JS_CALL_EXPRESSION:
  386. genCallExpression(node, context)
  387. break
  388. case NodeTypes.JS_OBJECT_EXPRESSION:
  389. genObjectExpression(node, context)
  390. break
  391. case NodeTypes.JS_ARRAY_EXPRESSION:
  392. genArrayExpression(node, context)
  393. break
  394. case NodeTypes.JS_FUNCTION_EXPRESSION:
  395. genFunctionExpression(node, context)
  396. break
  397. case NodeTypes.JS_SEQUENCE_EXPRESSION:
  398. genSequenceExpression(node, context)
  399. break
  400. case NodeTypes.JS_CONDITIONAL_EXPRESSION:
  401. genConditionalExpression(node, context)
  402. break
  403. case NodeTypes.JS_CACHE_EXPRESSION:
  404. genCacheExpression(node, context)
  405. break
  406. /* istanbul ignore next */
  407. default:
  408. if (__DEV__) {
  409. assert(false, `unhandled codegen node type: ${(node as any).type}`)
  410. // make sure we exhaust all possible types
  411. const exhaustiveCheck: never = node
  412. return exhaustiveCheck
  413. }
  414. }
  415. }
  416. function genText(
  417. node: TextNode | SimpleExpressionNode,
  418. context: CodegenContext
  419. ) {
  420. context.push(JSON.stringify(node.content), node)
  421. }
  422. function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
  423. const { content, isStatic } = node
  424. context.push(isStatic ? JSON.stringify(content) : content, node)
  425. }
  426. function genInterpolation(node: InterpolationNode, context: CodegenContext) {
  427. const { push, helper } = context
  428. push(`${helper(TO_STRING)}(`)
  429. genNode(node.content, context)
  430. push(`)`)
  431. }
  432. function genCompoundExpression(
  433. node: CompoundExpressionNode,
  434. context: CodegenContext
  435. ) {
  436. for (let i = 0; i < node.children!.length; i++) {
  437. const child = node.children![i]
  438. if (isString(child)) {
  439. context.push(child)
  440. } else {
  441. genNode(child, context)
  442. }
  443. }
  444. }
  445. function genExpressionAsPropertyKey(
  446. node: ExpressionNode,
  447. context: CodegenContext
  448. ) {
  449. const { push } = context
  450. if (node.type === NodeTypes.COMPOUND_EXPRESSION) {
  451. push(`[`)
  452. genCompoundExpression(node, context)
  453. push(`]`)
  454. } else if (node.isStatic) {
  455. // only quote keys if necessary
  456. const text = isSimpleIdentifier(node.content)
  457. ? node.content
  458. : JSON.stringify(node.content)
  459. push(text, node)
  460. } else {
  461. push(`[${node.content}]`, node)
  462. }
  463. }
  464. function genComment(node: CommentNode, context: CodegenContext) {
  465. if (__DEV__) {
  466. const { push, helper } = context
  467. push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node)
  468. }
  469. }
  470. // JavaScript
  471. function genCallExpression(node: CallExpression, context: CodegenContext) {
  472. const callee = isString(node.callee)
  473. ? node.callee
  474. : context.helper(node.callee)
  475. context.push(callee + `(`, node)
  476. genNodeList(node.arguments, context)
  477. context.push(`)`)
  478. }
  479. function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
  480. const { push, indent, deindent, newline } = context
  481. const { properties } = node
  482. if (!properties.length) {
  483. push(`{}`, node)
  484. return
  485. }
  486. const multilines =
  487. properties.length > 1 ||
  488. ((!__BROWSER__ || __DEV__) &&
  489. properties.some(p => p.value.type !== NodeTypes.SIMPLE_EXPRESSION))
  490. push(multilines ? `{` : `{ `)
  491. multilines && indent()
  492. for (let i = 0; i < properties.length; i++) {
  493. const { key, value } = properties[i]
  494. // key
  495. genExpressionAsPropertyKey(key, context)
  496. push(`: `)
  497. // value
  498. genNode(value, context)
  499. if (i < properties.length - 1) {
  500. // will only reach this if it's multilines
  501. push(`,`)
  502. newline()
  503. }
  504. }
  505. multilines && deindent()
  506. push(multilines ? `}` : ` }`)
  507. }
  508. function genArrayExpression(node: ArrayExpression, context: CodegenContext) {
  509. genNodeListAsArray(node.elements, context)
  510. }
  511. function genFunctionExpression(
  512. node: FunctionExpression,
  513. context: CodegenContext
  514. ) {
  515. const { push, indent, deindent } = context
  516. const { params, returns, newline } = node
  517. push(`(`, node)
  518. if (isArray(params)) {
  519. genNodeList(params, context)
  520. } else if (params) {
  521. genNode(params, context)
  522. }
  523. push(`) => `)
  524. if (newline) {
  525. push(`{`)
  526. indent()
  527. push(`return `)
  528. }
  529. if (isArray(returns)) {
  530. genNodeListAsArray(returns, context)
  531. } else {
  532. genNode(returns, context)
  533. }
  534. if (newline) {
  535. deindent()
  536. push(`}`)
  537. }
  538. }
  539. function genConditionalExpression(
  540. node: ConditionalExpression,
  541. context: CodegenContext
  542. ) {
  543. const { test, consequent, alternate } = node
  544. const { push, indent, deindent, newline } = context
  545. if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
  546. const needsParens = !isSimpleIdentifier(test.content)
  547. needsParens && push(`(`)
  548. genExpression(test, context)
  549. needsParens && push(`)`)
  550. } else {
  551. push(`(`)
  552. genCompoundExpression(test, context)
  553. push(`)`)
  554. }
  555. indent()
  556. context.indentLevel++
  557. push(`? `)
  558. genNode(consequent, context)
  559. context.indentLevel--
  560. newline()
  561. push(`: `)
  562. const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
  563. if (!isNested) {
  564. context.indentLevel++
  565. }
  566. genNode(alternate, context)
  567. if (!isNested) {
  568. context.indentLevel--
  569. }
  570. deindent(true /* without newline */)
  571. }
  572. function genSequenceExpression(
  573. node: SequenceExpression,
  574. context: CodegenContext
  575. ) {
  576. context.push(`(`)
  577. genNodeList(node.expressions, context)
  578. context.push(`)`)
  579. }
  580. function genCacheExpression(node: CacheExpression, context: CodegenContext) {
  581. const { push, helper, indent, deindent, newline } = context
  582. push(`_cache[${node.index}] || (`)
  583. if (node.isVNode) {
  584. indent()
  585. push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
  586. newline()
  587. }
  588. push(`_cache[${node.index}] = `)
  589. genNode(node.value, context)
  590. if (node.isVNode) {
  591. push(`,`)
  592. newline()
  593. push(`${helper(SET_BLOCK_TRACKING)}(1),`)
  594. newline()
  595. push(`_cache[${node.index}]`)
  596. deindent()
  597. }
  598. push(`)`)
  599. }