codegen.ts 17 KB

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