generate.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. import {
  2. type CodegenOptions,
  3. type CodegenResult,
  4. type Position,
  5. type SourceLocation,
  6. NewlineType,
  7. advancePositionWithMutation,
  8. locStub,
  9. BindingTypes,
  10. createSimpleExpression,
  11. walkIdentifiers,
  12. advancePositionWithClone,
  13. isSimpleIdentifier,
  14. } from '@vue/compiler-dom'
  15. import {
  16. type IRDynamicChildren,
  17. type RootIRNode,
  18. type SetPropIRNode,
  19. type IRExpression,
  20. type OperationNode,
  21. type VaporHelper,
  22. type SetEventIRNode,
  23. type WithDirectiveIRNode,
  24. type SetTextIRNode,
  25. type SetHtmlIRNode,
  26. type CreateTextNodeIRNode,
  27. type InsertNodeIRNode,
  28. type PrependNodeIRNode,
  29. type AppendNodeIRNode,
  30. IRNodeTypes,
  31. } from './ir'
  32. import { SourceMapGenerator } from 'source-map-js'
  33. import { camelize, isGloballyAllowed, isString, makeMap } from '@vue/shared'
  34. import type { Identifier } from '@babel/types'
  35. // remove when stable
  36. // @ts-expect-error
  37. function checkNever(x: never): never {}
  38. export interface CodegenContext extends Required<CodegenOptions> {
  39. source: string
  40. code: string
  41. line: number
  42. column: number
  43. offset: number
  44. indentLevel: number
  45. map?: SourceMapGenerator
  46. push(
  47. code: string,
  48. newlineIndex?: number,
  49. loc?: SourceLocation,
  50. name?: string,
  51. ): void
  52. pushWithNewline(
  53. code: string,
  54. newlineIndex?: number,
  55. loc?: SourceLocation,
  56. name?: string,
  57. ): void
  58. indent(): void
  59. deindent(): void
  60. newline(): void
  61. helpers: Set<string>
  62. vaporHelpers: Set<string>
  63. helper(name: string): string
  64. vaporHelper(name: string): string
  65. }
  66. function createCodegenContext(
  67. ir: RootIRNode,
  68. {
  69. mode = 'function',
  70. prefixIdentifiers = mode === 'module',
  71. sourceMap = false,
  72. filename = `template.vue.html`,
  73. scopeId = null,
  74. optimizeImports = false,
  75. runtimeGlobalName = `Vue`,
  76. runtimeModuleName = `vue`,
  77. ssrRuntimeModuleName = 'vue/server-renderer',
  78. ssr = false,
  79. isTS = false,
  80. inSSR = false,
  81. inline = false,
  82. bindingMetadata = {},
  83. }: CodegenOptions,
  84. ) {
  85. const { helpers, vaporHelpers } = ir
  86. const context: CodegenContext = {
  87. mode,
  88. prefixIdentifiers,
  89. sourceMap,
  90. filename,
  91. scopeId,
  92. optimizeImports,
  93. runtimeGlobalName,
  94. runtimeModuleName,
  95. ssrRuntimeModuleName,
  96. ssr,
  97. isTS,
  98. inSSR,
  99. bindingMetadata,
  100. inline,
  101. source: ir.source,
  102. code: ``,
  103. column: 1,
  104. line: 1,
  105. offset: 0,
  106. indentLevel: 0,
  107. helpers,
  108. vaporHelpers,
  109. helper(name: string) {
  110. helpers.add(name)
  111. return `_${name}`
  112. },
  113. vaporHelper(name: VaporHelper) {
  114. vaporHelpers.add(name)
  115. return `_${name}`
  116. },
  117. push(code, newlineIndex = NewlineType.None, loc, name) {
  118. context.code += code
  119. if (!__BROWSER__ && context.map) {
  120. if (loc) addMapping(loc.start, name)
  121. if (newlineIndex === NewlineType.Unknown) {
  122. // multiple newlines, full iteration
  123. advancePositionWithMutation(context, code)
  124. } else {
  125. // fast paths
  126. context.offset += code.length
  127. if (newlineIndex === NewlineType.None) {
  128. // no newlines; fast path to avoid newline detection
  129. if (__TEST__ && code.includes('\n')) {
  130. throw new Error(
  131. `CodegenContext.push() called newlineIndex: none, but contains` +
  132. `newlines: ${code.replace(/\n/g, '\\n')}`,
  133. )
  134. }
  135. context.column += code.length
  136. } else {
  137. // single newline at known index
  138. if (newlineIndex === NewlineType.End) {
  139. newlineIndex = code.length - 1
  140. }
  141. if (
  142. __TEST__ &&
  143. (code.charAt(newlineIndex) !== '\n' ||
  144. code.slice(0, newlineIndex).includes('\n') ||
  145. code.slice(newlineIndex + 1).includes('\n'))
  146. ) {
  147. throw new Error(
  148. `CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
  149. `but does not conform: ${code.replace(/\n/g, '\\n')}`,
  150. )
  151. }
  152. context.line++
  153. context.column = code.length - newlineIndex
  154. }
  155. }
  156. if (loc && loc !== locStub) {
  157. addMapping(loc.end)
  158. }
  159. }
  160. },
  161. pushWithNewline(code, newlineIndex, node) {
  162. context.newline()
  163. context.push(code, newlineIndex, node)
  164. },
  165. indent() {
  166. ++context.indentLevel
  167. },
  168. deindent() {
  169. --context.indentLevel
  170. },
  171. newline() {
  172. newline(context.indentLevel)
  173. },
  174. }
  175. function newline(n: number) {
  176. context.push(`\n${` `.repeat(n)}`, NewlineType.Start)
  177. }
  178. function addMapping(loc: Position, name: string | null = null) {
  179. // we use the private property to directly add the mapping
  180. // because the addMapping() implementation in source-map-js has a bunch of
  181. // unnecessary arg and validation checks that are pure overhead in our case.
  182. const { _names, _mappings } = context.map!
  183. if (name !== null && !_names.has(name)) _names.add(name)
  184. _mappings.add({
  185. originalLine: loc.line,
  186. originalColumn: loc.column - 1, // source-map column is 0 based
  187. generatedLine: context.line,
  188. generatedColumn: context.column - 1,
  189. source: filename,
  190. // @ts-ignore it is possible to be null
  191. name,
  192. })
  193. }
  194. if (!__BROWSER__ && sourceMap) {
  195. // lazy require source-map implementation, only in non-browser builds
  196. context.map = new SourceMapGenerator()
  197. context.map.setSourceContent(filename, context.source)
  198. context.map._sources.add(filename)
  199. }
  200. return context
  201. }
  202. // IR -> JS codegen
  203. export function generate(
  204. ir: RootIRNode,
  205. options: CodegenOptions = {},
  206. ): CodegenResult {
  207. const ctx = createCodegenContext(ir, options)
  208. const { push, pushWithNewline, indent, deindent, newline } = ctx
  209. const { vaporHelper, helpers, vaporHelpers } = ctx
  210. const functionName = 'render'
  211. const isSetupInlined = !!options.inline
  212. if (isSetupInlined) {
  213. push(`(() => {`)
  214. } else {
  215. // placeholder for preamble
  216. newline()
  217. pushWithNewline(`export function ${functionName}(_ctx) {`)
  218. }
  219. indent()
  220. ir.template.forEach((template, i) => {
  221. if (template.type === IRNodeTypes.TEMPLATE_FACTORY) {
  222. // TODO source map?
  223. pushWithNewline(
  224. `const t${i} = ${vaporHelper('template')}(${JSON.stringify(
  225. template.template,
  226. )})`,
  227. )
  228. } else {
  229. // fragment
  230. pushWithNewline(
  231. `const t0 = ${vaporHelper('fragment')}()\n`,
  232. NewlineType.End,
  233. )
  234. }
  235. })
  236. {
  237. pushWithNewline(`const n${ir.dynamic.id} = t0()`)
  238. const children = genChildren(ir.dynamic.children)
  239. if (children) {
  240. pushWithNewline(
  241. `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
  242. )
  243. }
  244. for (const oper of ir.operation.filter(
  245. (oper): oper is WithDirectiveIRNode =>
  246. oper.type === IRNodeTypes.WITH_DIRECTIVE,
  247. )) {
  248. genWithDirective(oper, ctx)
  249. }
  250. for (const operation of ir.operation) {
  251. genOperation(operation, ctx)
  252. }
  253. for (const { operations } of ir.effect) {
  254. pushWithNewline(`${vaporHelper('effect')}(() => {`)
  255. indent()
  256. for (const operation of operations) {
  257. genOperation(operation, ctx)
  258. }
  259. deindent()
  260. pushWithNewline('})')
  261. }
  262. // TODO multiple-template
  263. // TODO return statement in IR
  264. pushWithNewline(`return n${ir.dynamic.id}`)
  265. }
  266. deindent()
  267. newline()
  268. if (isSetupInlined) {
  269. push('})()')
  270. } else {
  271. push('}')
  272. }
  273. let preamble = ''
  274. if (vaporHelpers.size)
  275. // TODO: extract import codegen
  276. preamble = `import { ${[...vaporHelpers]
  277. .map((h) => `${h} as _${h}`)
  278. .join(', ')} } from 'vue/vapor';`
  279. if (helpers.size)
  280. preamble = `import { ${[...helpers]
  281. .map((h) => `${h} as _${h}`)
  282. .join(', ')} } from 'vue';`
  283. if (!isSetupInlined) {
  284. ctx.code = preamble + ctx.code
  285. }
  286. return {
  287. code: ctx.code,
  288. ast: ir as any,
  289. preamble,
  290. map: ctx.map ? ctx.map.toJSON() : undefined,
  291. }
  292. }
  293. function genChildren(children: IRDynamicChildren) {
  294. let code = ''
  295. // TODO
  296. let offset = 0
  297. for (const [index, child] of Object.entries(children)) {
  298. const childrenLength = Object.keys(child.children).length
  299. if (child.ghost && child.placeholder === null && childrenLength === 0) {
  300. offset--
  301. continue
  302. }
  303. code += ` ${Number(index) + offset}: [`
  304. const id = child.ghost ? child.placeholder : child.id
  305. if (id !== null) code += `n${id}`
  306. const childrenString = childrenLength && genChildren(child.children)
  307. if (childrenString) code += `, ${childrenString}`
  308. code += '],'
  309. }
  310. if (!code) return ''
  311. return `{${code}}`
  312. }
  313. function genOperation(oper: OperationNode, context: CodegenContext) {
  314. // TODO: cache old value
  315. switch (oper.type) {
  316. case IRNodeTypes.SET_PROP:
  317. return genSetProp(oper, context)
  318. case IRNodeTypes.SET_TEXT:
  319. return genSetText(oper, context)
  320. case IRNodeTypes.SET_EVENT:
  321. return genSetEvent(oper, context)
  322. case IRNodeTypes.SET_HTML:
  323. return genSetHtml(oper, context)
  324. case IRNodeTypes.CREATE_TEXT_NODE:
  325. return genCreateTextNode(oper, context)
  326. case IRNodeTypes.INSERT_NODE:
  327. return genInsertNode(oper, context)
  328. case IRNodeTypes.PREPEND_NODE:
  329. return genPrependNode(oper, context)
  330. case IRNodeTypes.APPEND_NODE:
  331. return genAppendNode(oper, context)
  332. case IRNodeTypes.WITH_DIRECTIVE:
  333. // generated, skip
  334. return
  335. default:
  336. return checkNever(oper)
  337. }
  338. }
  339. function genSetProp(oper: SetPropIRNode, context: CodegenContext) {
  340. const { push, pushWithNewline, vaporHelper, helper } = context
  341. pushWithNewline(`${vaporHelper('setAttr')}(n${oper.element}, `)
  342. if (oper.runtimeCamelize) push(`${helper('camelize')}(`)
  343. genExpression(oper.key, context)
  344. if (oper.runtimeCamelize) push(`)`)
  345. push(`, undefined, `)
  346. genExpression(oper.value, context)
  347. push(')')
  348. }
  349. function genSetText(oper: SetTextIRNode, context: CodegenContext) {
  350. const { push, pushWithNewline, vaporHelper } = context
  351. pushWithNewline(`${vaporHelper('setText')}(n${oper.element}, undefined, `)
  352. genExpression(oper.value, context)
  353. push(')')
  354. }
  355. function genSetHtml(oper: SetHtmlIRNode, context: CodegenContext) {
  356. const { push, pushWithNewline, vaporHelper } = context
  357. pushWithNewline(`${vaporHelper('setHtml')}(n${oper.element}, undefined, `)
  358. genExpression(oper.value, context)
  359. push(')')
  360. }
  361. function genCreateTextNode(
  362. oper: CreateTextNodeIRNode,
  363. context: CodegenContext,
  364. ) {
  365. const { push, pushWithNewline, vaporHelper } = context
  366. pushWithNewline(`const n${oper.id} = ${vaporHelper('createTextNode')}(`)
  367. genExpression(oper.value, context)
  368. push(')')
  369. }
  370. function genInsertNode(oper: InsertNodeIRNode, context: CodegenContext) {
  371. const { pushWithNewline, vaporHelper } = context
  372. const elements = ([] as number[]).concat(oper.element)
  373. let element = elements.map((el) => `n${el}`).join(', ')
  374. if (elements.length > 1) element = `[${element}]`
  375. pushWithNewline(
  376. `${vaporHelper('insert')}(${element}, n${
  377. oper.parent
  378. }${`, n${oper.anchor}`})`,
  379. )
  380. }
  381. function genPrependNode(oper: PrependNodeIRNode, context: CodegenContext) {
  382. const { pushWithNewline, vaporHelper } = context
  383. pushWithNewline(
  384. `${vaporHelper('prepend')}(n${oper.parent}, ${oper.elements
  385. .map((el) => `n${el}`)
  386. .join(', ')})`,
  387. )
  388. }
  389. function genAppendNode(oper: AppendNodeIRNode, context: CodegenContext) {
  390. const { pushWithNewline, vaporHelper } = context
  391. pushWithNewline(
  392. `${vaporHelper('append')}(n${oper.parent}, ${oper.elements
  393. .map((el) => `n${el}`)
  394. .join(', ')})`,
  395. )
  396. }
  397. function genSetEvent(oper: SetEventIRNode, context: CodegenContext) {
  398. const { vaporHelper, push, pushWithNewline } = context
  399. pushWithNewline(`${vaporHelper('on')}(n${oper.element}, `)
  400. // 2nd arg: event name
  401. if (oper.keyOverride) {
  402. const find = JSON.stringify(oper.keyOverride[0])
  403. const replacement = JSON.stringify(oper.keyOverride[1])
  404. push('(')
  405. genExpression(oper.key, context)
  406. push(`) === ${find} ? ${replacement} : (`)
  407. genExpression(oper.key, context)
  408. push(')')
  409. } else {
  410. genExpression(oper.key, context)
  411. }
  412. push(', ')
  413. const { keys, nonKeys, options } = oper.modifiers
  414. // 3rd arg: event handler
  415. if (oper.value && oper.value.content.trim()) {
  416. if (keys.length) {
  417. push(`${vaporHelper('withKeys')}(`)
  418. }
  419. if (nonKeys.length) {
  420. push(`${vaporHelper('withModifiers')}(`)
  421. }
  422. push('(...args) => (')
  423. genExpression(oper.value, context)
  424. push(' && ')
  425. genExpression(oper.value, context)
  426. push('(...args))')
  427. if (nonKeys.length) {
  428. push(`, ${genArrayExpression(nonKeys)})`)
  429. }
  430. if (keys.length) {
  431. push(`, ${genArrayExpression(keys)})`)
  432. }
  433. } else {
  434. push('() => {}')
  435. }
  436. // 4th arg, gen options
  437. if (options.length) {
  438. push(`, { ${options.map((v) => `${v}: true`).join(', ')} }`)
  439. }
  440. push(')')
  441. }
  442. function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) {
  443. const { push, pushWithNewline, vaporHelper, bindingMetadata } = context
  444. const { dir } = oper
  445. // TODO merge directive for the same node
  446. pushWithNewline(`${vaporHelper('withDirectives')}(n${oper.element}, [[`)
  447. if (dir.name === 'show') {
  448. push(vaporHelper('vShow'))
  449. } else {
  450. const directiveReference = camelize(`v-${dir.name}`)
  451. // TODO resolve directive
  452. if (bindingMetadata[directiveReference]) {
  453. const directiveExpression = createSimpleExpression(directiveReference)
  454. directiveExpression.ast = null
  455. genExpression(directiveExpression, context)
  456. }
  457. }
  458. if (dir.exp) {
  459. push(', () => ')
  460. genExpression(dir.exp, context)
  461. } else if (dir.arg || dir.modifiers.length) {
  462. push(', void 0')
  463. }
  464. if (dir.arg) {
  465. push(', ')
  466. genExpression(dir.arg, context)
  467. } else if (dir.modifiers.length) {
  468. push(', void 0')
  469. }
  470. if (dir.modifiers.length) {
  471. push(', ')
  472. push('{ ')
  473. push(genDirectiveModifiers(dir.modifiers))
  474. push(' }')
  475. }
  476. push(']])')
  477. return
  478. }
  479. // TODO: other types (not only string)
  480. function genArrayExpression(elements: string[]) {
  481. return `[${elements.map((it) => JSON.stringify(it)).join(', ')}]`
  482. }
  483. const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
  484. function genExpression(node: IRExpression, context: CodegenContext): void {
  485. const { push } = context
  486. if (isString(node)) return push(node)
  487. const { content: rawExpr, ast, isStatic, loc } = node
  488. if (isStatic) {
  489. return push(JSON.stringify(rawExpr), NewlineType.None, loc)
  490. }
  491. if (
  492. __BROWSER__ ||
  493. !context.prefixIdentifiers ||
  494. !node.content.trim() ||
  495. // there was a parsing error
  496. ast === false ||
  497. isGloballyAllowed(rawExpr) ||
  498. isLiteralWhitelisted(rawExpr)
  499. ) {
  500. return push(rawExpr, NewlineType.None, loc)
  501. }
  502. if (ast === null) {
  503. // the expression is a simple identifier
  504. return genIdentifier(rawExpr, context, loc)
  505. }
  506. const ids: Identifier[] = []
  507. walkIdentifiers(
  508. ast!,
  509. (id) => {
  510. ids.push(id)
  511. },
  512. true,
  513. )
  514. if (ids.length) {
  515. ids.sort((a, b) => a.start! - b.start!)
  516. ids.forEach((id, i) => {
  517. // range is offset by -1 due to the wrapping parens when parsed
  518. const start = id.start! - 1
  519. const end = id.end! - 1
  520. const last = ids[i - 1]
  521. const leadingText = rawExpr.slice(last ? last.end! - 1 : 0, start)
  522. if (leadingText.length) push(leadingText, NewlineType.Unknown)
  523. const source = rawExpr.slice(start, end)
  524. genIdentifier(source, context, {
  525. start: advancePositionWithClone(node.loc.start, source, start),
  526. end: advancePositionWithClone(node.loc.start, source, end),
  527. source,
  528. })
  529. if (i === ids.length - 1 && end < rawExpr.length) {
  530. push(rawExpr.slice(end), NewlineType.Unknown)
  531. }
  532. })
  533. } else {
  534. push(rawExpr, NewlineType.Unknown)
  535. }
  536. }
  537. function genIdentifier(
  538. id: string,
  539. { inline, bindingMetadata, vaporHelper, push }: CodegenContext,
  540. loc?: SourceLocation,
  541. ): void {
  542. let name: string | undefined = id
  543. if (inline) {
  544. switch (bindingMetadata[id]) {
  545. case BindingTypes.SETUP_REF:
  546. name = id += '.value'
  547. break
  548. case BindingTypes.SETUP_MAYBE_REF:
  549. id = `${vaporHelper('unref')}(${id})`
  550. name = undefined
  551. break
  552. }
  553. } else {
  554. id = `_ctx.${id}`
  555. }
  556. push(id, NewlineType.None, loc, name)
  557. }
  558. function genDirectiveModifiers(modifiers: string[]) {
  559. return modifiers
  560. .map(
  561. (value) =>
  562. `${isSimpleIdentifier(value) ? value : JSON.stringify(value)}: true`,
  563. )
  564. .join(', ')
  565. }