codegen.ts 15 KB

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