generate.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import type {
  2. CodegenOptions as BaseCodegenOptions,
  3. BaseCodegenResult,
  4. SimpleExpressionNode,
  5. } from '@vue/compiler-dom'
  6. import type { BlockIRNode, CoreHelper, RootIRNode, VaporHelper } from './ir'
  7. import { extend, remove } from '@vue/shared'
  8. import { genBlockContent } from './generators/block'
  9. import { genTemplates } from './generators/template'
  10. import {
  11. type CodeFragment,
  12. INDENT_END,
  13. INDENT_START,
  14. LF,
  15. NEWLINE,
  16. buildCodeFragment,
  17. codeFragmentToString,
  18. genCall,
  19. } from './generators/utils'
  20. import { setTemplateRefIdent } from './generators/templateRef'
  21. import { buildNextIdMap, getNextId } from './transform'
  22. export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'>
  23. const idWithTrailingDigitsRE = /^([A-Za-z_$][\w$]*)(\d+)$/
  24. export class CodegenContext {
  25. options: Required<CodegenOptions>
  26. bindingNames: Set<string> = new Set<string>()
  27. helpers: Map<string, string> = new Map()
  28. helper = (name: CoreHelper | VaporHelper): string => {
  29. if (this.helpers.has(name)) {
  30. return this.helpers.get(name)!
  31. }
  32. const base = `_${name}`
  33. if (this.bindingNames.size === 0 || !this.bindingNames.has(base)) {
  34. this.helpers.set(name, base)
  35. return base
  36. }
  37. const map = this.nextIdMap.get(base)
  38. // start from 1 because "base" (no suffix) is already taken.
  39. const alias = `${base}${getNextId(map, 1)}`
  40. this.helpers.set(name, alias)
  41. return alias
  42. }
  43. delegates: Set<string> = new Set<string>()
  44. identifiers: Record<string, (string | SimpleExpressionNode)[]> =
  45. Object.create(null)
  46. seenInlineHandlerNames: Record<string, number> = Object.create(null)
  47. block: BlockIRNode
  48. withId<T>(
  49. fn: () => T,
  50. map: Record<string, string | SimpleExpressionNode | null>,
  51. ): T {
  52. const { identifiers } = this
  53. const ids = Object.keys(map)
  54. for (const id of ids) {
  55. identifiers[id] ||= []
  56. identifiers[id].unshift(map[id] || id)
  57. }
  58. const ret = fn()
  59. ids.forEach(id => remove(identifiers[id], map[id] || id))
  60. return ret
  61. }
  62. enterBlock(block: BlockIRNode) {
  63. const parent = this.block
  64. this.block = block
  65. return (): BlockIRNode => (this.block = parent)
  66. }
  67. scopeLevel: number = 0
  68. enterScope(): [level: number, exit: () => number] {
  69. return [this.scopeLevel++, () => this.scopeLevel--] as const
  70. }
  71. private templateVars: Map<number, string> = new Map()
  72. private nextIdMap: Map<string, Map<number, number>> = new Map()
  73. private lastIdMap: Map<string, number> = new Map()
  74. private lastTIndex: number = -1
  75. private initNextIdMap(): void {
  76. if (this.bindingNames.size === 0) return
  77. // build a map of binding names to their occupied ids
  78. const map = new Map<string, Set<number>>()
  79. for (const name of this.bindingNames) {
  80. const m = idWithTrailingDigitsRE.exec(name)
  81. if (!m) continue
  82. const prefix = m[1]
  83. const num = Number(m[2])
  84. let set = map.get(prefix)
  85. if (!set) map.set(prefix, (set = new Set<number>()))
  86. set.add(num)
  87. }
  88. for (const [prefix, nums] of map) {
  89. this.nextIdMap.set(prefix, buildNextIdMap(nums))
  90. }
  91. }
  92. tName(i: number): string {
  93. let name = this.templateVars.get(i)
  94. if (name) return name
  95. const map = this.nextIdMap.get('t')
  96. let lastId = this.lastIdMap.get('t') || -1
  97. for (let j = this.lastTIndex + 1; j <= i; j++) {
  98. this.templateVars.set(
  99. j,
  100. (name = `t${(lastId = getNextId(map, Math.max(j, lastId + 1)))}`),
  101. )
  102. }
  103. this.lastIdMap.set('t', lastId)
  104. this.lastTIndex = i
  105. return name!
  106. }
  107. pName(i: number): string {
  108. const map = this.nextIdMap.get('p')
  109. let lastId = this.lastIdMap.get('p') || -1
  110. this.lastIdMap.set('p', (lastId = getNextId(map, Math.max(i, lastId + 1))))
  111. return `p${lastId}`
  112. }
  113. constructor(
  114. public ir: RootIRNode,
  115. options: CodegenOptions,
  116. ) {
  117. const defaultOptions: Required<CodegenOptions> = {
  118. mode: 'module',
  119. prefixIdentifiers: true,
  120. sourceMap: false,
  121. filename: `template.vue.html`,
  122. scopeId: null,
  123. runtimeGlobalName: `Vue`,
  124. runtimeModuleName: `vue`,
  125. ssrRuntimeModuleName: 'vue/server-renderer',
  126. ssr: false,
  127. isTS: false,
  128. inSSR: false,
  129. inline: false,
  130. bindingMetadata: {},
  131. expressionPlugins: [],
  132. }
  133. this.options = extend(defaultOptions, options)
  134. this.block = ir.block
  135. this.bindingNames = new Set<string>(
  136. this.options.bindingMetadata
  137. ? Object.keys(this.options.bindingMetadata)
  138. : [],
  139. )
  140. this.initNextIdMap()
  141. }
  142. }
  143. export interface VaporCodegenResult extends BaseCodegenResult {
  144. ast: RootIRNode
  145. helpers: Set<string>
  146. }
  147. // IR -> JS codegen
  148. export function generate(
  149. ir: RootIRNode,
  150. options: CodegenOptions = {},
  151. ): VaporCodegenResult {
  152. const [frag, push] = buildCodeFragment()
  153. const context = new CodegenContext(ir, options)
  154. const { inline, bindingMetadata } = options
  155. const functionName = 'render'
  156. const args = ['_ctx']
  157. if (bindingMetadata && !inline) {
  158. // binding optimization args
  159. args.push('$props', '$emit', '$attrs', '$slots')
  160. }
  161. const signature = (options.isTS ? args.map(arg => `${arg}: any`) : args).join(
  162. ', ',
  163. )
  164. if (!inline) {
  165. push(NEWLINE, `export function ${functionName}(${signature}) {`)
  166. }
  167. push(INDENT_START)
  168. if (ir.hasTemplateRef) {
  169. push(
  170. NEWLINE,
  171. `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`,
  172. )
  173. }
  174. if (ir.hasDeferredVShow) {
  175. push(NEWLINE, `const deferredApplyVShows = []`)
  176. }
  177. push(...genBlockContent(ir.block, context, true))
  178. push(INDENT_END, NEWLINE)
  179. if (!inline) {
  180. push('}')
  181. }
  182. const delegates = genDelegates(context)
  183. const templates = genTemplates(ir.template, ir.rootTemplateIndexes, context)
  184. const imports = genHelperImports(context) + genAssetImports(context)
  185. const preamble = imports + templates + delegates
  186. const newlineCount = [...preamble].filter(c => c === '\n').length
  187. if (newlineCount && !inline) {
  188. frag.unshift(...new Array<CodeFragment>(newlineCount).fill(LF))
  189. }
  190. let [code, map] = codeFragmentToString(frag, context)
  191. if (!inline) {
  192. code = preamble + code
  193. }
  194. return {
  195. code,
  196. ast: ir,
  197. preamble,
  198. map: map && map.toJSON(),
  199. helpers: new Set<string>(Array.from(context.helpers.keys())),
  200. }
  201. }
  202. function genDelegates({ delegates, helper }: CodegenContext) {
  203. return delegates.size
  204. ? genCall(
  205. helper('delegateEvents'),
  206. ...Array.from(delegates).map(v => `"${v}"`),
  207. ).join('') + '\n'
  208. : ''
  209. }
  210. function genHelperImports({ helpers, options }: CodegenContext) {
  211. let imports = ''
  212. if (helpers.size) {
  213. imports += `import { ${Array.from(helpers)
  214. .map(([h, alias]) => `${h} as ${alias}`)
  215. .join(', ')} } from '${options.runtimeModuleName}';\n`
  216. }
  217. return imports
  218. }
  219. function genAssetImports({ ir }: CodegenContext) {
  220. const assetImports = ir.node.imports
  221. let imports = ''
  222. for (const assetImport of assetImports) {
  223. const exp = assetImport.exp
  224. const name = exp.content
  225. imports += `import ${name} from '${assetImport.path}';\n`
  226. }
  227. return imports
  228. }